Skip to content

Instantly share code, notes, and snippets.

@BigRoy
Last active January 12, 2022 12:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BigRoy/69a85e91d6b1a931a5ced5c4f477a4b9 to your computer and use it in GitHub Desktop.
Save BigRoy/69a85e91d6b1a931a5ced5c4f477a4b9 to your computer and use it in GitHub Desktop.
Tips & Tricks for OpenPype

How to make a publish plug-in manageable in Studio Settings?

  1. Edit the related schema. For example: openpype\settings\entities\schemas\projects_schema\schemas\schema_maya_publish.json
  2. Run run_settings script
  3. Save the defaults.
  4. Re-run tray.

For example this could be added to make it possible to disable the ExtractModel plug-in

        {
            "type": "schema_template",
            "name": "template_publish_plugin",
            "template_data": [
                {
                    "key": "ExtractModel",
                    "label": "Extract Model (.mayaAscii)"
                }
            ]
        }

How to add custom actions in the Launcher?

What's the recommended way to register them and where should I put those?

Jakub Trllo (Pype.Club):

Currently there are 2 options:

  1. add them to zips into defaults actions folder
  2. create OpenPype addon which can return path to actions in collect_plugin_paths

I would say that addon is better. But there is not currently way how to automatically deploy addons with zip so all workstations must have access to the addons path.

addons path can be set in system settings modules/addon_paths

Note:

  • openpype/modules is not dynamically loaded but using explicit loading
  • openpype/modules/default_modules is currently dynamically loaded but that should change when we move all modules out of there. Other way how to load addons dynamically is with setting path to directory where addons are in system settings modules/addon_paths
@BigRoy
Copy link
Author

BigRoy commented Dec 30, 2021

Speed up Pyblish UI - reduce defer delay (obsolete with: ynput/OpenPype#2464)

To speed up Pyblish UI (the non-experimental one) reduce the delay that keeps the UI 'responsive'. It's just too high by default.

Here's a good trick:

import os
os.environ["PYBLISH_DELAY"] = "0.1"

Or this in the global environment:

{
    "PYBLISH_DELAY": "0.1"
}

Play with the right value, the lower the less defer time it takes. It might be that even 0.01 feels better to you. At 0, then no defer takes place but it might be that the UI does not update at all until it is done processing.


This might become obsolete with: ynput/OpenPype#2464

@BigRoy
Copy link
Author

BigRoy commented Dec 30, 2021

Customize the maya workspace.mel for OpenPype

OpenPype copies the file placed at: openpype/hosts/maya/resources/workspace.mel

@BigRoy
Copy link
Author

BigRoy commented Dec 31, 2021

Create Work folders based on settings

New PR implements this: ynput/OpenPype#2462

@BigRoy
Copy link
Author

BigRoy commented Jan 4, 2022

Create JSON backup of Mongo

This could create json backup of mongo but can eat whole RAM and will take some time - by Jabuk Trllo (Pype.Club)

import os
import json
import pymongo
from bson.json_util import dumps, CANONICAL_JSON_OPTIONS

dst_dir = "<path/to/dir>"
mongo_url = "<mongo url>"
database_names_to_backup = []

# Make sure directory exists
if not os.path.exists(dst_dir):
    os.makedirs(dst_dir)

# Connect to mongo
mongo_client = pymongo.MongoClient(mongo_url)
# Go through databases that should be stored
for database_name in database_names_to_backup:
    # Prepare filepath where data will be stored
    json_filepath = os.path.join(dst_dir, database_name + ".json")
    # Access database from mongo connection
    database = mongo_client[database_name]
    # Prepare dictionary where data to store will be stored
    database_data = {}
    # Query all docs from each collection
    for col_name in database.collection_names():
        colleciton = database[col_name]
        docs = list(colleciton.find({}))
        database_data[col_name] = dumps(
            docs, json_options=CANONICAL_JSON_OPTIONS
        )

    # Store the data
    with open(json_filepath, "w") as stream:
        json.dump(database_data, stream)

@BigRoy
Copy link
Author

BigRoy commented Jan 5, 2022

What's the easiest approach to getting the work file template for the current project? Plus format it to what variables are available at the current context?

  1. first you need to get template key that should be used with openpype.lib.avalon_context.get_workfile_template_key().
  2. then create anatomy for the project, format it and access the path
from avalon.api import AvalonMongoDB
from openpype.api import Anatomy
from openpype.lib import (
    get_workfile_template_key_from_context,
    get_workdir_data
)

# Prerequirements
project_name = "PROJECT"
asset_name = "ASSET"
task_name = "TASK"
host_name = "MAYA"

# Create mongo connection
dbcon = AvalonMongoDB()
dbcon.Session["AVALON_PROJECT"] = project_name

# Query docs needed for filling template
project_doc = dbcon.find_one({"type": "project"})
asset_doc = dbcon.find_one({"type": "asset", "name": asset_name})
format_data = get_workdir_data(project_doc, asset_doc, task_name, host_name)

# Get template key
template_key = get_workfile_template_key_from_context(
    asset_name, task_name, host_name, project_name, dbcon
)
anatomy = Anatomy(project_name)
result = anatomy.format(format_data)
workdir = result[template_key]["folder"]

@BigRoy
Copy link
Author

BigRoy commented Jan 5, 2022

Implement a "Explore Here" launcher action

Allow opening the browser to the folder of where you've currently browsed in the launcher.

Here's an example on how to expose this to the launcher with an OpenPype module addon.

import os
import string
import getpass

from avalon import api, lib, pipeline
from avalon.vendor import six

STUB = "<<NULL>>"


class ExploreToCurrent(api.Action):
    name = "exploretocurrent"
    label = "Explore Here"
    icon = "external-link"
    color = "#e8770e"
    order = 7

    def is_compatible(self, session):
        return "AVALON_PROJECT" in session

    def process(self, session, **kwargs):

        from avalon.vendor.Qt import QtCore, QtWidgets
        from avalon.api import AvalonMongoDB
        from openpype.api import Anatomy

        # Prerequirements
        project_name = session["AVALON_PROJECT"]
        asset_name = session.get("AVALON_ASSET", None)
        task_name = session.get("AVALON_TASK", None)
        host_name = None    # never present in session

        # Create mongo connection
        dbcon = AvalonMongoDB()
        dbcon.Session["AVALON_PROJECT"] = project_name
        project_doc = dbcon.find_one({"type": "project"})
        assert project_doc, "Project not found. This is a bug."

        asset_doc = None
        if asset_name is not None:
            asset_doc = dbcon.find_one({"name": asset_name, "type": "asset"})

        data = self.get_workdir_data(project_doc=project_doc,
                                     asset_doc=asset_doc,
                                     task_name=task_name,
                                     host_name=host_name)
        anatomy = Anatomy(project_name)
        result = anatomy.format(data)

        # todo: implement custom template keys instead of 'work'
        # Get template key
        #template_key = get_workfile_template_key_from_context(
        #    asset_name, task_name, host_name, project_name, dbcon
        #)
        workdir = result["work"]["folder"]

        # Keep only the part of the path that was formatted y splitting up to
        # the first stub - we can only explore up to there
        valid_dir = workdir.split(STUB, 1)[0]

        # If the unformatted data left us with half a folder name or file name
        # after splitting then we get the dirname.
        if not valid_dir.replace("\\", "/").endswith("/"):
            valid_dir = os.path.dirname(valid_dir)

        path = os.path.normpath(valid_dir)

        app = QtWidgets.QApplication.instance()
        ctrl_pressed = QtCore.Qt.ControlModifier & app.keyboardModifiers()
        if ctrl_pressed:
            # Copy path to clipboard
            self.copy_path_to_clipboard(path)
        else:
            self.open_in_explorer(path)

    def get_workdir_data(self, project_doc, asset_doc, task_name, host_name):
        """Mimic `openpype.lib.get_workdir_data` but require less data.

        This allows for example to have only a valid `project_doc` passed in
        and the rest being None. For all that is None a stub placeholder data
        value will be returned.

        Returns:
            dict: Workdir data.

        """

        # Start with mostly stub placeholder data where we cannot match
        # `openpype.lib.get_workdir_data` due to lack of input variables.
        STUB = "<<NULL>>"
        data = {
            "project": {
                "name": project_doc["name"],
                "code": project_doc["data"].get("code")
            },
            "task": {
                "name": STUB,
                "type": STUB,
                "short": STUB,
            },
            "asset": STUB,
            "parent": STUB,
            "hierarchy": STUB,
            "app": STUB,
            "user": getpass.getuser()
        }

        # Retrieve data similar to `openpype.lib.get_workdir_data` but only
        # up to where we can. First using AVALON_ASSET in session.
        if asset_doc:
            asset_parents = asset_doc["data"]["parents"]
            hierarchy = "/".join(asset_parents)

            parent_name = project_doc["name"]
            if asset_parents:
                parent_name = asset_parents[-1]

            # Insert asset data
            data.update({
                "asset": asset_doc["name"],
                "parent": parent_name,
                "hierarchy": hierarchy,
            })

        # Then insert task data when AVALON_TASK in session
        if asset_doc and task_name is not None:
            asset_tasks = asset_doc['data']['tasks']
            task_type = asset_tasks.get(task_name, {}).get('type')
            project_task_types = project_doc["config"]["tasks"]
            task_code = project_task_types.get(task_type, {}).get(
                "short_name"
            )

            # Insert task data
            data["task"].update({
                "name": task_name,
                "type": task_type,
                "code": task_code
            })

        return data

    @staticmethod
    def open_in_explorer(path):
        import subprocess

        if os.path.exists(path):
            print("Opening Explorer: %s" % path)
            # todo(roy): Make this cross OS compatible (currently windows only)
            subprocess.Popen(r'explorer "{}"'.format(path))

        else:
            print("Path does not exist: %s" % path)
            raise RuntimeError("Folder does not exist.")

    @staticmethod
    def copy_path_to_clipboard(path):
        from avalon.vendor.Qt import QtCore, QtWidgets

        path = path.replace("\\", "/")
        print("Copied to clipboard: %s" % path)
        app = QtWidgets.QApplication.instance()
        assert app, "Must have running QApplication instance"

        # Set to Clipboard
        clipboard = QtWidgets.QApplication.clipboard()
        clipboard.setText(os.path.normpath(path))

@BigRoy
Copy link
Author

BigRoy commented Jan 12, 2022

@BigRoy
Copy link
Author

BigRoy commented Jan 12, 2022

Deadline Rendering with OpenPype

Basic instructions: https://openpype.io/docs/module_deadline


  • OPENPYPE_MONGO must be defined for the Deadline Workers to know which Mongo database URL should be used from the Workers.
    For now we patched it to get a default fallback value from GlobalJobPreLoad.py.

  • Note that you need to set OpenPype Executable paths for BOTH the OpenPype Event Plugin and the Render Plugin individiually.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment