Merge branch 'release/2.11.0'
25
.gitignore
vendored
|
|
@ -5,6 +5,31 @@ __pycache__/
|
|||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Mac Stuff
|
||||
###########
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
# Thumbnails
|
||||
._*
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# Documentation
|
||||
###############
|
||||
/docs/build
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ from pypeapp.lib.mongo import (
|
|||
get_default_components
|
||||
)
|
||||
|
||||
from . import resources
|
||||
|
||||
from .plugin import (
|
||||
Extractor,
|
||||
|
||||
|
|
@ -54,6 +56,8 @@ __all__ = [
|
|||
"compose_url",
|
||||
"get_default_components",
|
||||
|
||||
# Resources
|
||||
"resources",
|
||||
# plugin classes
|
||||
"Extractor",
|
||||
# ordering
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ class CelactionPrelaunchHook(PypeHook):
|
|||
f"--project {project}",
|
||||
f"--asset {asset}",
|
||||
f"--task {task}",
|
||||
"--currentFile \"*SCENE*\"",
|
||||
"--currentFile \\\"\"*SCENE*\"\\\"",
|
||||
"--chunk *CHUNK*",
|
||||
"--frameStart *START*",
|
||||
"--frameEnd *END*",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
import traceback
|
||||
import winreg
|
||||
from avalon import api, io, lib
|
||||
from pype.lib import PypeHook
|
||||
from pype.api import Logger, Anatomy
|
||||
|
|
@ -14,6 +15,12 @@ class PremierePrelaunch(PypeHook):
|
|||
shell script.
|
||||
"""
|
||||
project_code = None
|
||||
reg_string_value = [{
|
||||
"path": r"Software\Adobe\CSXS.9",
|
||||
"name": "PlayerDebugMode",
|
||||
"type": winreg.REG_SZ,
|
||||
"value": "1"
|
||||
}]
|
||||
|
||||
def __init__(self, logger=None):
|
||||
if not logger:
|
||||
|
|
@ -55,6 +62,10 @@ class PremierePrelaunch(PypeHook):
|
|||
# adding project code to env
|
||||
env["AVALON_PROJECT_CODE"] = self.project_code
|
||||
|
||||
# add keys to registry
|
||||
self.modify_registry()
|
||||
|
||||
# start avalon
|
||||
try:
|
||||
__import__("pype.hosts.premiere")
|
||||
__import__("pyblish")
|
||||
|
|
@ -69,6 +80,24 @@ class PremierePrelaunch(PypeHook):
|
|||
|
||||
return True
|
||||
|
||||
def modify_registry(self):
|
||||
# adding key to registry
|
||||
for key in self.reg_string_value:
|
||||
winreg.CreateKey(winreg.HKEY_CURRENT_USER, key["path"])
|
||||
rg_key = winreg.OpenKey(
|
||||
key=winreg.HKEY_CURRENT_USER,
|
||||
sub_key=key["path"],
|
||||
reserved=0,
|
||||
access=winreg.KEY_ALL_ACCESS)
|
||||
|
||||
winreg.SetValueEx(
|
||||
rg_key,
|
||||
key["name"],
|
||||
0,
|
||||
key["type"],
|
||||
key["value"]
|
||||
)
|
||||
|
||||
def get_anatomy_filled(self):
|
||||
root_path = api.registered_root()
|
||||
project_name = self._S["AVALON_PROJECT"]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import traceback
|
|||
from avalon import api as avalon
|
||||
from pyblish import api as pyblish
|
||||
|
||||
import bpy
|
||||
|
||||
from pype import PLUGINS_DIR
|
||||
|
||||
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "blender", "publish")
|
||||
|
|
@ -25,6 +27,9 @@ def install():
|
|||
avalon.register_plugin_path(avalon.Loader, str(LOAD_PATH))
|
||||
avalon.register_plugin_path(avalon.Creator, str(CREATE_PATH))
|
||||
|
||||
avalon.on("new", on_new)
|
||||
avalon.on("open", on_open)
|
||||
|
||||
|
||||
def uninstall():
|
||||
"""Uninstall Blender configuration for Avalon."""
|
||||
|
|
@ -32,3 +37,24 @@ def uninstall():
|
|||
pyblish.deregister_plugin_path(str(PUBLISH_PATH))
|
||||
avalon.deregister_plugin_path(avalon.Loader, str(LOAD_PATH))
|
||||
avalon.deregister_plugin_path(avalon.Creator, str(CREATE_PATH))
|
||||
|
||||
|
||||
def set_start_end_frames():
|
||||
from avalon import io
|
||||
|
||||
asset_name = io.Session["AVALON_ASSET"]
|
||||
asset_doc = io.find_one({
|
||||
"type": "asset",
|
||||
"name": asset_name
|
||||
})
|
||||
|
||||
bpy.context.scene.frame_start = asset_doc["data"]["frameStart"]
|
||||
bpy.context.scene.frame_end = asset_doc["data"]["frameEnd"]
|
||||
|
||||
|
||||
def on_new(arg1, arg2):
|
||||
set_start_end_frames()
|
||||
|
||||
|
||||
def on_open(arg1, arg2):
|
||||
set_start_end_frames()
|
||||
|
|
|
|||
|
|
@ -14,12 +14,42 @@ def asset_name(
|
|||
asset: str, subset: str, namespace: Optional[str] = None
|
||||
) -> str:
|
||||
"""Return a consistent name for an asset."""
|
||||
name = f"{asset}_{subset}"
|
||||
name = f"{asset}"
|
||||
if namespace:
|
||||
name = f"{namespace}:{name}"
|
||||
name = f"{name}_{namespace}"
|
||||
name = f"{name}_{subset}"
|
||||
return name
|
||||
|
||||
|
||||
def get_unique_number(
|
||||
asset: str, subset: str
|
||||
) -> str:
|
||||
"""Return a unique number based on the asset name."""
|
||||
avalon_containers = [
|
||||
c for c in bpy.data.collections
|
||||
if c.name == 'AVALON_CONTAINERS'
|
||||
]
|
||||
loaded_assets = []
|
||||
for c in avalon_containers:
|
||||
loaded_assets.extend(c.children)
|
||||
collections_names = [
|
||||
c.name for c in loaded_assets
|
||||
]
|
||||
count = 1
|
||||
name = f"{asset}_{count:0>2}_{subset}_CON"
|
||||
while name in collections_names:
|
||||
count += 1
|
||||
name = f"{asset}_{count:0>2}_{subset}_CON"
|
||||
return f"{count:0>2}"
|
||||
|
||||
|
||||
def prepare_data(data, container_name):
|
||||
name = data.name
|
||||
local_data = data.make_local()
|
||||
local_data.name = f"{name}:{container_name}"
|
||||
return local_data
|
||||
|
||||
|
||||
def create_blender_context(active: Optional[bpy.types.Object] = None,
|
||||
selected: Optional[bpy.types.Object] = None,):
|
||||
"""Create a new Blender context. If an object is passed as
|
||||
|
|
@ -47,6 +77,25 @@ def create_blender_context(active: Optional[bpy.types.Object] = None,
|
|||
raise Exception("Could not create a custom Blender context.")
|
||||
|
||||
|
||||
def get_parent_collection(collection):
|
||||
"""Get the parent of the input collection"""
|
||||
check_list = [bpy.context.scene.collection]
|
||||
|
||||
for c in check_list:
|
||||
if collection.name in c.children.keys():
|
||||
return c
|
||||
check_list.extend(c.children)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_local_collection_with_name(name):
|
||||
for collection in bpy.data.collections:
|
||||
if collection.name == name and collection.library is None:
|
||||
return collection
|
||||
return None
|
||||
|
||||
|
||||
class AssetLoader(api.Loader):
|
||||
"""A basic AssetLoader for Blender
|
||||
|
||||
|
|
|
|||
|
|
@ -46,9 +46,6 @@ def cli():
|
|||
parser.add_argument("--resolutionHeight",
|
||||
help=("Height of resolution"))
|
||||
|
||||
# parser.add_argument("--programDir",
|
||||
# help=("Directory with celaction program installation"))
|
||||
|
||||
celaction.kwargs = parser.parse_args(sys.argv[1:]).__dict__
|
||||
|
||||
|
||||
|
|
@ -78,7 +75,7 @@ def _prepare_publish_environments():
|
|||
env["AVALON_WORKDIR"] = os.getenv("AVALON_WORKDIR")
|
||||
env["AVALON_HIERARCHY"] = hierarchy
|
||||
env["AVALON_PROJECTCODE"] = project_doc["data"].get("code", "")
|
||||
env["AVALON_APP"] = publish_host
|
||||
env["AVALON_APP"] = f"hosts.{publish_host}"
|
||||
env["AVALON_APP_NAME"] = "celaction_local"
|
||||
|
||||
env["PYBLISH_HOSTS"] = publish_host
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
from avalon import api, harmony
|
||||
from avalon import api, io, harmony
|
||||
from avalon.vendor import Qt
|
||||
import avalon.tools.sceneinventory
|
||||
import pyblish.api
|
||||
from pype import lib
|
||||
|
||||
|
|
@ -92,6 +93,61 @@ def ensure_scene_settings():
|
|||
set_scene_settings(valid_settings)
|
||||
|
||||
|
||||
def check_inventory():
|
||||
if not lib.any_outdated():
|
||||
return
|
||||
|
||||
host = avalon.api.registered_host()
|
||||
outdated_containers = []
|
||||
for container in host.ls():
|
||||
representation = container['representation']
|
||||
representation_doc = io.find_one(
|
||||
{
|
||||
"_id": io.ObjectId(representation),
|
||||
"type": "representation"
|
||||
},
|
||||
projection={"parent": True}
|
||||
)
|
||||
if representation_doc and not lib.is_latest(representation_doc):
|
||||
outdated_containers.append(container)
|
||||
|
||||
# Colour nodes.
|
||||
func = """function func(args){
|
||||
for( var i =0; i <= args[0].length - 1; ++i)
|
||||
{
|
||||
var red_color = new ColorRGBA(255, 0, 0, 255);
|
||||
node.setColor(args[0][i], red_color);
|
||||
}
|
||||
}
|
||||
func
|
||||
"""
|
||||
outdated_nodes = []
|
||||
for container in outdated_containers:
|
||||
if container["loader"] == "ImageSequenceLoader":
|
||||
outdated_nodes.append(
|
||||
harmony.find_node_by_name(container["name"], "READ")
|
||||
)
|
||||
harmony.send({"function": func, "args": [outdated_nodes]})
|
||||
|
||||
# Warn about outdated containers.
|
||||
print("Starting new QApplication..")
|
||||
app = Qt.QtWidgets.QApplication(sys.argv)
|
||||
|
||||
message_box = Qt.QtWidgets.QMessageBox()
|
||||
message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning)
|
||||
msg = "There are outdated containers in the scene."
|
||||
message_box.setText(msg)
|
||||
message_box.exec_()
|
||||
|
||||
# Garbage collect QApplication.
|
||||
del app
|
||||
|
||||
|
||||
def application_launch():
|
||||
ensure_scene_settings()
|
||||
check_inventory()
|
||||
|
||||
|
||||
def export_template(backdrops, nodes, filepath):
|
||||
func = """function func(args)
|
||||
{
|
||||
|
|
@ -161,7 +217,7 @@ def install():
|
|||
"instanceToggled", on_pyblish_instance_toggled
|
||||
)
|
||||
|
||||
api.on("application.launched", ensure_scene_settings)
|
||||
api.on("application.launched", application_launch)
|
||||
|
||||
|
||||
def on_pyblish_instance_toggled(instance, old_value, new_value):
|
||||
|
|
|
|||
|
|
@ -69,17 +69,38 @@ def override_component_mask_commands():
|
|||
|
||||
def override_toolbox_ui():
|
||||
"""Add custom buttons in Toolbox as replacement for Maya web help icon."""
|
||||
inventory = None
|
||||
loader = None
|
||||
launch_workfiles_app = None
|
||||
mayalookassigner = None
|
||||
try:
|
||||
import avalon.tools.sceneinventory as inventory
|
||||
except Exception:
|
||||
log.warning("Could not import SceneInventory tool")
|
||||
|
||||
import pype
|
||||
res = os.path.join(os.path.dirname(os.path.dirname(pype.__file__)),
|
||||
"res")
|
||||
icons = os.path.join(res, "icons")
|
||||
try:
|
||||
import avalon.tools.loader as loader
|
||||
except Exception:
|
||||
log.warning("Could not import Loader tool")
|
||||
|
||||
import avalon.tools.sceneinventory as inventory
|
||||
import avalon.tools.loader as loader
|
||||
from avalon.maya.pipeline import launch_workfiles_app
|
||||
import mayalookassigner
|
||||
try:
|
||||
from avalon.maya.pipeline import launch_workfiles_app
|
||||
except Exception:
|
||||
log.warning("Could not import Workfiles tool")
|
||||
|
||||
try:
|
||||
import mayalookassigner
|
||||
except Exception:
|
||||
log.warning("Could not import Maya Look assigner tool")
|
||||
|
||||
from pype.api import resources
|
||||
|
||||
icons = resources.get_resource("icons")
|
||||
|
||||
if not any((
|
||||
mayalookassigner, launch_workfiles_app, loader, inventory
|
||||
)):
|
||||
return
|
||||
|
||||
# Ensure the maya web icon on toolbox exists
|
||||
web_button = "ToolBox|MainToolboxLayout|mayaWebButton"
|
||||
|
|
@ -99,65 +120,65 @@ def override_toolbox_ui():
|
|||
# Create our controls
|
||||
background_color = (0.267, 0.267, 0.267)
|
||||
controls = []
|
||||
if mayalookassigner:
|
||||
controls.append(
|
||||
mc.iconTextButton(
|
||||
"pype_toolbox_lookmanager",
|
||||
annotation="Look Manager",
|
||||
label="Look Manager",
|
||||
image=os.path.join(icons, "lookmanager.png"),
|
||||
command=lambda: mayalookassigner.show(),
|
||||
bgc=background_color,
|
||||
width=icon_size,
|
||||
height=icon_size,
|
||||
parent=parent
|
||||
)
|
||||
)
|
||||
|
||||
control = mc.iconTextButton(
|
||||
"pype_toolbox_lookmanager",
|
||||
annotation="Look Manager",
|
||||
label="Look Manager",
|
||||
image=os.path.join(icons, "lookmanager.png"),
|
||||
command=lambda: mayalookassigner.show(),
|
||||
bgc=background_color,
|
||||
width=icon_size,
|
||||
height=icon_size,
|
||||
parent=parent)
|
||||
controls.append(control)
|
||||
if launch_workfiles_app:
|
||||
controls.append(
|
||||
mc.iconTextButton(
|
||||
"pype_toolbox_workfiles",
|
||||
annotation="Work Files",
|
||||
label="Work Files",
|
||||
image=os.path.join(icons, "workfiles.png"),
|
||||
command=lambda: launch_workfiles_app(),
|
||||
bgc=background_color,
|
||||
width=icon_size,
|
||||
height=icon_size,
|
||||
parent=parent
|
||||
)
|
||||
)
|
||||
|
||||
control = mc.iconTextButton(
|
||||
"pype_toolbox_workfiles",
|
||||
annotation="Work Files",
|
||||
label="Work Files",
|
||||
image=os.path.join(icons, "workfiles.png"),
|
||||
command=lambda: launch_workfiles_app(),
|
||||
bgc=background_color,
|
||||
width=icon_size,
|
||||
height=icon_size,
|
||||
parent=parent)
|
||||
controls.append(control)
|
||||
if loader:
|
||||
controls.append(
|
||||
mc.iconTextButton(
|
||||
"pype_toolbox_loader",
|
||||
annotation="Loader",
|
||||
label="Loader",
|
||||
image=os.path.join(icons, "loader.png"),
|
||||
command=lambda: loader.show(use_context=True),
|
||||
bgc=background_color,
|
||||
width=icon_size,
|
||||
height=icon_size,
|
||||
parent=parent
|
||||
)
|
||||
)
|
||||
|
||||
control = mc.iconTextButton(
|
||||
"pype_toolbox_loader",
|
||||
annotation="Loader",
|
||||
label="Loader",
|
||||
image=os.path.join(icons, "loader.png"),
|
||||
command=lambda: loader.show(use_context=True),
|
||||
bgc=background_color,
|
||||
width=icon_size,
|
||||
height=icon_size,
|
||||
parent=parent)
|
||||
controls.append(control)
|
||||
|
||||
control = mc.iconTextButton(
|
||||
"pype_toolbox_manager",
|
||||
annotation="Inventory",
|
||||
label="Inventory",
|
||||
image=os.path.join(icons, "inventory.png"),
|
||||
command=lambda: inventory.show(),
|
||||
bgc=background_color,
|
||||
width=icon_size,
|
||||
height=icon_size,
|
||||
parent=parent)
|
||||
controls.append(control)
|
||||
|
||||
# control = mc.iconTextButton(
|
||||
# "pype_toolbox",
|
||||
# annotation="Kredenc",
|
||||
# label="Kredenc",
|
||||
# image=os.path.join(icons, "kredenc_logo.png"),
|
||||
# bgc=background_color,
|
||||
# width=icon_size,
|
||||
# height=icon_size,
|
||||
# parent=parent)
|
||||
# controls.append(control)
|
||||
if inventory:
|
||||
controls.append(
|
||||
mc.iconTextButton(
|
||||
"pype_toolbox_manager",
|
||||
annotation="Inventory",
|
||||
label="Inventory",
|
||||
image=os.path.join(icons, "inventory.png"),
|
||||
command=lambda: inventory.show(),
|
||||
bgc=background_color,
|
||||
width=icon_size,
|
||||
height=icon_size,
|
||||
parent=parent
|
||||
)
|
||||
)
|
||||
|
||||
# Add the buttons on the bottom and stack
|
||||
# them above each other with side padding
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
import nuke
|
||||
from avalon.nuke import lib as anlib
|
||||
from pype.api import resources
|
||||
|
||||
|
||||
def set_context_favorites(favorites={}):
|
||||
|
|
@ -9,9 +10,7 @@ def set_context_favorites(favorites={}):
|
|||
Argumets:
|
||||
favorites (dict): couples of {name:path}
|
||||
"""
|
||||
dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
icon_path = os.path.join(dir, 'res', 'icons', 'folder-favorite3.png')
|
||||
|
||||
icon_path = resources.get_resource("icons", "folder-favorite3.png")
|
||||
for name, path in favorites.items():
|
||||
nuke.addFavoriteDir(
|
||||
name,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ from pype.api import Logger
|
|||
|
||||
log = Logger().get_logger(__name__, "nukestudio")
|
||||
|
||||
|
||||
def file_extensions():
|
||||
return [".hrox"]
|
||||
return api.HOST_WORKFILE_EXTENSIONS["nukestudio"]
|
||||
|
||||
|
||||
def has_unsaved_changes():
|
||||
|
|
|
|||
|
|
@ -1,9 +1,48 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
from avalon import api
|
||||
from avalon import api, io
|
||||
from avalon.vendor import Qt
|
||||
from pype import lib
|
||||
import pyblish.api
|
||||
|
||||
|
||||
def check_inventory():
|
||||
if not lib.any_outdated():
|
||||
return
|
||||
|
||||
host = api.registered_host()
|
||||
outdated_containers = []
|
||||
for container in host.ls():
|
||||
representation = container['representation']
|
||||
representation_doc = io.find_one(
|
||||
{
|
||||
"_id": io.ObjectId(representation),
|
||||
"type": "representation"
|
||||
},
|
||||
projection={"parent": True}
|
||||
)
|
||||
if representation_doc and not lib.is_latest(representation_doc):
|
||||
outdated_containers.append(container)
|
||||
|
||||
# Warn about outdated containers.
|
||||
print("Starting new QApplication..")
|
||||
app = Qt.QtWidgets.QApplication(sys.argv)
|
||||
|
||||
message_box = Qt.QtWidgets.QMessageBox()
|
||||
message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning)
|
||||
msg = "There are outdated containers in the scene."
|
||||
message_box.setText(msg)
|
||||
message_box.exec_()
|
||||
|
||||
# Garbage collect QApplication.
|
||||
del app
|
||||
|
||||
|
||||
def application_launch():
|
||||
check_inventory()
|
||||
|
||||
|
||||
def install():
|
||||
print("Installing Pype config...")
|
||||
|
||||
|
|
@ -27,6 +66,8 @@ def install():
|
|||
"instanceToggled", on_pyblish_instance_toggled
|
||||
)
|
||||
|
||||
api.on("application.launched", application_launch)
|
||||
|
||||
|
||||
def on_pyblish_instance_toggled(instance, old_value, new_value):
|
||||
"""Toggle layer visibility on instance toggles."""
|
||||
|
|
|
|||
|
|
@ -534,7 +534,9 @@ $.pype = {
|
|||
if (instances === null) {
|
||||
return null;
|
||||
}
|
||||
if (audioOnly === true) {
|
||||
|
||||
// make only audio representations
|
||||
if (audioOnly === 'true') {
|
||||
$.pype.log('? looping if audio True');
|
||||
for (var i = 0; i < instances.length; i++) {
|
||||
var subsetToRepresentations = instances[i].subsetToRepresentations;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,7 @@
|
|||
from .clockify_api import ClockifyAPI
|
||||
from .widget_settings import ClockifySettings
|
||||
from .widget_message import MessageWidget
|
||||
from .clockify import ClockifyModule
|
||||
|
||||
__all__ = [
|
||||
"ClockifyAPI",
|
||||
"ClockifySettings",
|
||||
"ClockifyModule",
|
||||
"MessageWidget"
|
||||
]
|
||||
|
||||
CLASS_DEFINIION = ClockifyModule
|
||||
|
||||
|
||||
def tray_init(tray_widget, main_widget):
|
||||
return ClockifyModule(main_widget, tray_widget)
|
||||
|
|
|
|||
|
|
@ -3,17 +3,25 @@ import threading
|
|||
from pype.api import Logger
|
||||
from avalon import style
|
||||
from Qt import QtWidgets
|
||||
from . import ClockifySettings, ClockifyAPI, MessageWidget
|
||||
from .widgets import ClockifySettings, MessageWidget
|
||||
from .clockify_api import ClockifyAPI
|
||||
from .constants import CLOCKIFY_FTRACK_USER_PATH
|
||||
|
||||
|
||||
class ClockifyModule:
|
||||
workspace_name = None
|
||||
|
||||
def __init__(self, main_parent=None, parent=None):
|
||||
if not self.workspace_name:
|
||||
raise Exception("Clockify Workspace is not set in config.")
|
||||
|
||||
os.environ["CLOCKIFY_WORKSPACE"] = self.workspace_name
|
||||
|
||||
self.log = Logger().get_logger(self.__class__.__name__, "PypeTray")
|
||||
|
||||
self.main_parent = main_parent
|
||||
self.parent = parent
|
||||
self.clockapi = ClockifyAPI()
|
||||
self.clockapi = ClockifyAPI(master_parent=self)
|
||||
self.message_widget = None
|
||||
self.widget_settings = ClockifySettings(main_parent, self)
|
||||
self.widget_settings_required = None
|
||||
|
|
@ -24,8 +32,6 @@ class ClockifyModule:
|
|||
self.bool_api_key_set = False
|
||||
self.bool_workspace_set = False
|
||||
self.bool_timer_run = False
|
||||
|
||||
self.clockapi.set_master(self)
|
||||
self.bool_api_key_set = self.clockapi.set_api()
|
||||
|
||||
def tray_start(self):
|
||||
|
|
@ -43,14 +49,12 @@ class ClockifyModule:
|
|||
|
||||
def process_modules(self, modules):
|
||||
if 'FtrackModule' in modules:
|
||||
actions_path = os.path.sep.join([
|
||||
os.path.dirname(__file__),
|
||||
'ftrack_actions'
|
||||
])
|
||||
current = os.environ.get('FTRACK_ACTIONS_PATH', '')
|
||||
if current:
|
||||
current += os.pathsep
|
||||
os.environ['FTRACK_ACTIONS_PATH'] = current + actions_path
|
||||
os.environ['FTRACK_ACTIONS_PATH'] = (
|
||||
current + CLOCKIFY_FTRACK_USER_PATH
|
||||
)
|
||||
|
||||
if 'AvalonApps' in modules:
|
||||
from launcher import lib
|
||||
|
|
@ -188,9 +192,10 @@ class ClockifyModule:
|
|||
).format(project_name))
|
||||
|
||||
msg = (
|
||||
"Project <b>\"{}\"</b> is not in Clockify Workspace <b>\"{}\"</b>."
|
||||
"Project <b>\"{}\"</b> is not"
|
||||
" in Clockify Workspace <b>\"{}\"</b>."
|
||||
"<br><br>Please inform your Project Manager."
|
||||
).format(project_name, str(self.clockapi.workspace))
|
||||
).format(project_name, str(self.clockapi.workspace_name))
|
||||
|
||||
self.message_widget = MessageWidget(
|
||||
self.main_parent, msg, "Clockify - Info Message"
|
||||
|
|
|
|||
|
|
@ -1,35 +1,39 @@
|
|||
import os
|
||||
import re
|
||||
import time
|
||||
import requests
|
||||
import json
|
||||
import datetime
|
||||
import appdirs
|
||||
from .constants import (
|
||||
CLOCKIFY_ENDPOINT, ADMIN_PERMISSION_NAMES, CREDENTIALS_JSON_PATH
|
||||
)
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
def time_check(obj):
|
||||
if obj.request_counter < 10:
|
||||
obj.request_counter += 1
|
||||
return
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super(
|
||||
Singleton, cls
|
||||
).__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
wait_time = 1 - (time.time() - obj.request_time)
|
||||
if wait_time > 0:
|
||||
time.sleep(wait_time)
|
||||
|
||||
obj.request_time = time.time()
|
||||
obj.request_counter = 0
|
||||
|
||||
|
||||
class ClockifyAPI(metaclass=Singleton):
|
||||
endpoint = "https://api.clockify.me/api/"
|
||||
headers = {"X-Api-Key": None}
|
||||
app_dir = os.path.normpath(appdirs.user_data_dir('pype-app', 'pype'))
|
||||
file_name = 'clockify.json'
|
||||
fpath = os.path.join(app_dir, file_name)
|
||||
admin_permission_names = ['WORKSPACE_OWN', 'WORKSPACE_ADMIN']
|
||||
master_parent = None
|
||||
workspace = None
|
||||
workspace_id = None
|
||||
|
||||
def set_master(self, master_parent):
|
||||
class ClockifyAPI:
|
||||
def __init__(self, api_key=None, master_parent=None):
|
||||
self.workspace_name = None
|
||||
self.workspace_id = None
|
||||
self.master_parent = master_parent
|
||||
self.api_key = api_key
|
||||
self.request_counter = 0
|
||||
self.request_time = time.time()
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
return {"X-Api-Key": self.api_key}
|
||||
|
||||
def verify_api(self):
|
||||
for key, value in self.headers.items():
|
||||
|
|
@ -42,7 +46,7 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
api_key = self.get_api_key()
|
||||
|
||||
if api_key is not None and self.validate_api_key(api_key) is True:
|
||||
self.headers["X-Api-Key"] = api_key
|
||||
self.api_key = api_key
|
||||
self.set_workspace()
|
||||
if self.master_parent:
|
||||
self.master_parent.signed_in()
|
||||
|
|
@ -52,8 +56,9 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
def validate_api_key(self, api_key):
|
||||
test_headers = {'X-Api-Key': api_key}
|
||||
action_url = 'workspaces/'
|
||||
time_check(self)
|
||||
response = requests.get(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=test_headers
|
||||
)
|
||||
if response.status_code != 200:
|
||||
|
|
@ -69,25 +74,27 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
action_url = "/workspaces/{}/users/{}/permissions".format(
|
||||
workspace_id, user_id
|
||||
)
|
||||
time_check(self)
|
||||
response = requests.get(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=self.headers
|
||||
)
|
||||
user_permissions = response.json()
|
||||
for perm in user_permissions:
|
||||
if perm['name'] in self.admin_permission_names:
|
||||
if perm['name'] in ADMIN_PERMISSION_NAMES:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_user_id(self):
|
||||
action_url = 'v1/user/'
|
||||
time_check(self)
|
||||
response = requests.get(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=self.headers
|
||||
)
|
||||
# this regex is neccessary: UNICODE strings are crashing
|
||||
# during json serialization
|
||||
id_regex ='\"{1}id\"{1}\:{1}\"{1}\w+\"{1}'
|
||||
id_regex = '\"{1}id\"{1}\:{1}\"{1}\w+\"{1}'
|
||||
result = re.findall(id_regex, str(response.content))
|
||||
if len(result) != 1:
|
||||
# replace with log and better message?
|
||||
|
|
@ -98,9 +105,9 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
def set_workspace(self, name=None):
|
||||
if name is None:
|
||||
name = os.environ.get('CLOCKIFY_WORKSPACE', None)
|
||||
self.workspace = name
|
||||
self.workspace_name = name
|
||||
self.workspace_id = None
|
||||
if self.workspace is None:
|
||||
if self.workspace_name is None:
|
||||
return
|
||||
try:
|
||||
result = self.validate_workspace()
|
||||
|
|
@ -115,7 +122,7 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
|
||||
def validate_workspace(self, name=None):
|
||||
if name is None:
|
||||
name = self.workspace
|
||||
name = self.workspace_name
|
||||
all_workspaces = self.get_workspaces()
|
||||
if name in all_workspaces:
|
||||
return all_workspaces[name]
|
||||
|
|
@ -124,25 +131,26 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
def get_api_key(self):
|
||||
api_key = None
|
||||
try:
|
||||
file = open(self.fpath, 'r')
|
||||
file = open(CREDENTIALS_JSON_PATH, 'r')
|
||||
api_key = json.load(file).get('api_key', None)
|
||||
if api_key == '':
|
||||
api_key = None
|
||||
except Exception:
|
||||
file = open(self.fpath, 'w')
|
||||
file = open(CREDENTIALS_JSON_PATH, 'w')
|
||||
file.close()
|
||||
return api_key
|
||||
|
||||
def save_api_key(self, api_key):
|
||||
data = {'api_key': api_key}
|
||||
file = open(self.fpath, 'w')
|
||||
file = open(CREDENTIALS_JSON_PATH, 'w')
|
||||
file.write(json.dumps(data))
|
||||
file.close()
|
||||
|
||||
def get_workspaces(self):
|
||||
action_url = 'workspaces/'
|
||||
time_check(self)
|
||||
response = requests.get(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=self.headers
|
||||
)
|
||||
return {
|
||||
|
|
@ -153,8 +161,9 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
if workspace_id is None:
|
||||
workspace_id = self.workspace_id
|
||||
action_url = 'workspaces/{}/projects/'.format(workspace_id)
|
||||
time_check(self)
|
||||
response = requests.get(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=self.headers
|
||||
)
|
||||
|
||||
|
|
@ -168,8 +177,9 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
action_url = 'workspaces/{}/projects/{}/'.format(
|
||||
workspace_id, project_id
|
||||
)
|
||||
time_check(self)
|
||||
response = requests.get(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=self.headers
|
||||
)
|
||||
|
||||
|
|
@ -179,8 +189,9 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
if workspace_id is None:
|
||||
workspace_id = self.workspace_id
|
||||
action_url = 'workspaces/{}/tags/'.format(workspace_id)
|
||||
time_check(self)
|
||||
response = requests.get(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=self.headers
|
||||
)
|
||||
|
||||
|
|
@ -194,8 +205,9 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
action_url = 'workspaces/{}/projects/{}/tasks/'.format(
|
||||
workspace_id, project_id
|
||||
)
|
||||
time_check(self)
|
||||
response = requests.get(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=self.headers
|
||||
)
|
||||
|
||||
|
|
@ -276,8 +288,9 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
"taskId": task_id,
|
||||
"tagIds": tag_ids
|
||||
}
|
||||
time_check(self)
|
||||
response = requests.post(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=self.headers,
|
||||
json=body
|
||||
)
|
||||
|
|
@ -293,8 +306,9 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
action_url = 'workspaces/{}/timeEntries/inProgress'.format(
|
||||
workspace_id
|
||||
)
|
||||
time_check(self)
|
||||
response = requests.get(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=self.headers
|
||||
)
|
||||
try:
|
||||
|
|
@ -323,8 +337,9 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
"tagIds": current["tagIds"],
|
||||
"end": self.get_current_time()
|
||||
}
|
||||
time_check(self)
|
||||
response = requests.put(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=self.headers,
|
||||
json=body
|
||||
)
|
||||
|
|
@ -336,8 +351,9 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
if workspace_id is None:
|
||||
workspace_id = self.workspace_id
|
||||
action_url = 'workspaces/{}/timeEntries/'.format(workspace_id)
|
||||
time_check(self)
|
||||
response = requests.get(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=self.headers
|
||||
)
|
||||
return response.json()[:quantity]
|
||||
|
|
@ -348,8 +364,9 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
action_url = 'workspaces/{}/timeEntries/{}'.format(
|
||||
workspace_id, tid
|
||||
)
|
||||
time_check(self)
|
||||
response = requests.delete(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=self.headers
|
||||
)
|
||||
return response.json()
|
||||
|
|
@ -363,14 +380,15 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
"clientId": "",
|
||||
"isPublic": "false",
|
||||
"estimate": {
|
||||
# "estimate": "3600",
|
||||
"estimate": 0,
|
||||
"type": "AUTO"
|
||||
},
|
||||
"color": "#f44336",
|
||||
"billable": "true"
|
||||
}
|
||||
time_check(self)
|
||||
response = requests.post(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=self.headers,
|
||||
json=body
|
||||
)
|
||||
|
|
@ -379,8 +397,9 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
def add_workspace(self, name):
|
||||
action_url = 'workspaces/'
|
||||
body = {"name": name}
|
||||
time_check(self)
|
||||
response = requests.post(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=self.headers,
|
||||
json=body
|
||||
)
|
||||
|
|
@ -398,8 +417,9 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
"name": name,
|
||||
"projectId": project_id
|
||||
}
|
||||
time_check(self)
|
||||
response = requests.post(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=self.headers,
|
||||
json=body
|
||||
)
|
||||
|
|
@ -412,8 +432,9 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
body = {
|
||||
"name": name
|
||||
}
|
||||
time_check(self)
|
||||
response = requests.post(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=self.headers,
|
||||
json=body
|
||||
)
|
||||
|
|
@ -427,8 +448,9 @@ class ClockifyAPI(metaclass=Singleton):
|
|||
action_url = '/workspaces/{}/projects/{}'.format(
|
||||
workspace_id, project_id
|
||||
)
|
||||
time_check(self)
|
||||
response = requests.delete(
|
||||
self.endpoint + action_url,
|
||||
CLOCKIFY_ENDPOINT + action_url,
|
||||
headers=self.headers,
|
||||
)
|
||||
return response.json()
|
||||
|
|
|
|||
17
pype/modules/clockify/constants.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import os
|
||||
import appdirs
|
||||
|
||||
|
||||
CLOCKIFY_FTRACK_SERVER_PATH = os.path.join(
|
||||
os.path.dirname(__file__), "ftrack", "server"
|
||||
)
|
||||
CLOCKIFY_FTRACK_USER_PATH = os.path.join(
|
||||
os.path.dirname(__file__), "ftrack", "user"
|
||||
)
|
||||
CREDENTIALS_JSON_PATH = os.path.normpath(os.path.join(
|
||||
appdirs.user_data_dir("pype-app", "pype"),
|
||||
"clockify.json"
|
||||
))
|
||||
|
||||
ADMIN_PERMISSION_NAMES = ["WORKSPACE_OWN", "WORKSPACE_ADMIN"]
|
||||
CLOCKIFY_ENDPOINT = "https://api.clockify.me/api/"
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
import os
|
||||
import json
|
||||
from pype.modules.ftrack.lib import BaseAction
|
||||
from pype.modules.clockify.clockify_api import ClockifyAPI
|
||||
|
||||
|
||||
class SyncClocifyServer(BaseAction):
|
||||
'''Synchronise project names and task types.'''
|
||||
|
||||
identifier = "clockify.sync.server"
|
||||
label = "Sync To Clockify (server)"
|
||||
description = "Synchronise data to Clockify workspace"
|
||||
|
||||
discover_role_list = ["Pypeclub", "Administrator", "project Manager"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SyncClocifyServer, self).__init__(*args, **kwargs)
|
||||
|
||||
workspace_name = os.environ.get("CLOCKIFY_WORKSPACE")
|
||||
api_key = os.environ.get("CLOCKIFY_API_KEY")
|
||||
self.clockapi = ClockifyAPI(api_key)
|
||||
self.clockapi.set_workspace(workspace_name)
|
||||
if api_key is None:
|
||||
modified_key = "None"
|
||||
else:
|
||||
str_len = int(len(api_key) / 2)
|
||||
start_replace = int(len(api_key) / 4)
|
||||
modified_key = ""
|
||||
for idx in range(len(api_key)):
|
||||
if idx >= start_replace and idx < start_replace + str_len:
|
||||
replacement = "X"
|
||||
else:
|
||||
replacement = api_key[idx]
|
||||
modified_key += replacement
|
||||
|
||||
self.log.info(
|
||||
"Clockify info. Workspace: \"{}\" API key: \"{}\"".format(
|
||||
str(workspace_name), str(modified_key)
|
||||
)
|
||||
)
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
if (
|
||||
len(entities) != 1
|
||||
or entities[0].entity_type.lower() != "project"
|
||||
):
|
||||
return False
|
||||
|
||||
# Get user and check his roles
|
||||
user_id = event.get("source", {}).get("user", {}).get("id")
|
||||
if not user_id:
|
||||
return False
|
||||
|
||||
user = session.query("User where id is \"{}\"".format(user_id)).first()
|
||||
if not user:
|
||||
return False
|
||||
|
||||
for role in user["user_security_roles"]:
|
||||
if role["security_role"]["name"] in self.discover_role_list:
|
||||
return True
|
||||
return False
|
||||
|
||||
def register(self):
|
||||
self.session.event_hub.subscribe(
|
||||
"topic=ftrack.action.discover",
|
||||
self._discover,
|
||||
priority=self.priority
|
||||
)
|
||||
|
||||
launch_subscription = (
|
||||
"topic=ftrack.action.launch and data.actionIdentifier={}"
|
||||
).format(self.identifier)
|
||||
self.session.event_hub.subscribe(launch_subscription, self._launch)
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
if self.clockapi.workspace_id is None:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Clockify Workspace or API key are not set!"
|
||||
}
|
||||
|
||||
if self.clockapi.validate_workspace_perm() is False:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing permissions for this action!"
|
||||
}
|
||||
|
||||
# JOB SETTINGS
|
||||
user_id = event["source"]["user"]["id"]
|
||||
user = session.query("User where id is " + user_id).one()
|
||||
|
||||
job = session.create("Job", {
|
||||
"user": user,
|
||||
"status": "running",
|
||||
"data": json.dumps({"description": "Sync Ftrack to Clockify"})
|
||||
})
|
||||
session.commit()
|
||||
|
||||
project_entity = entities[0]
|
||||
if project_entity.entity_type.lower() != "project":
|
||||
project_entity = self.get_project_from_entity(project_entity)
|
||||
|
||||
project_name = project_entity["full_name"]
|
||||
self.log.info(
|
||||
"Synchronization of project \"{}\" to clockify begins.".format(
|
||||
project_name
|
||||
)
|
||||
)
|
||||
task_types = (
|
||||
project_entity["project_schema"]["_task_type_schema"]["types"]
|
||||
)
|
||||
task_type_names = [
|
||||
task_type["name"] for task_type in task_types
|
||||
]
|
||||
try:
|
||||
clockify_projects = self.clockapi.get_projects()
|
||||
if project_name not in clockify_projects:
|
||||
response = self.clockapi.add_project(project_name)
|
||||
if "id" not in response:
|
||||
self.log.warning(
|
||||
"Project \"{}\" can't be created. Response: {}".format(
|
||||
project_name, response
|
||||
)
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"message": (
|
||||
"Can't create clockify project \"{}\"."
|
||||
" Unexpected error."
|
||||
).format(project_name)
|
||||
}
|
||||
|
||||
clockify_workspace_tags = self.clockapi.get_tags()
|
||||
for task_type_name in task_type_names:
|
||||
if task_type_name in clockify_workspace_tags:
|
||||
self.log.debug(
|
||||
"Task \"{}\" already exist".format(task_type_name)
|
||||
)
|
||||
continue
|
||||
|
||||
response = self.clockapi.add_tag(task_type_name)
|
||||
if "id" not in response:
|
||||
self.log.warning(
|
||||
"Task \"{}\" can't be created. Response: {}".format(
|
||||
task_type_name, response
|
||||
)
|
||||
)
|
||||
|
||||
job["status"] = "done"
|
||||
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Synchronization to clockify failed.",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
finally:
|
||||
if job["status"] != "done":
|
||||
job["status"] = "failed"
|
||||
session.commit()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def register(session, **kw):
|
||||
SyncClocifyServer(session).register()
|
||||
122
pype/modules/clockify/ftrack/user/action_clockify_sync_local.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import json
|
||||
from pype.modules.ftrack.lib import BaseAction, statics_icon
|
||||
from pype.modules.clockify.clockify_api import ClockifyAPI
|
||||
|
||||
|
||||
class SyncClocifyLocal(BaseAction):
|
||||
'''Synchronise project names and task types.'''
|
||||
|
||||
#: Action identifier.
|
||||
identifier = 'clockify.sync.local'
|
||||
#: Action label.
|
||||
label = 'Sync To Clockify (local)'
|
||||
#: Action description.
|
||||
description = 'Synchronise data to Clockify workspace'
|
||||
#: roles that are allowed to register this action
|
||||
role_list = ["Pypeclub", "Administrator", "project Manager"]
|
||||
#: icon
|
||||
icon = statics_icon("app_icons", "clockify-white.png")
|
||||
|
||||
#: CLockifyApi
|
||||
clockapi = ClockifyAPI()
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
if (
|
||||
len(entities) == 1
|
||||
and entities[0].entity_type.lower() == "project"
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
self.clockapi.set_api()
|
||||
if self.clockapi.workspace_id is None:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Clockify Workspace or API key are not set!"
|
||||
}
|
||||
|
||||
if self.clockapi.validate_workspace_perm() is False:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing permissions for this action!"
|
||||
}
|
||||
|
||||
# JOB SETTINGS
|
||||
userId = event['source']['user']['id']
|
||||
user = session.query('User where id is ' + userId).one()
|
||||
|
||||
job = session.create('Job', {
|
||||
'user': user,
|
||||
'status': 'running',
|
||||
'data': json.dumps({
|
||||
'description': 'Sync Ftrack to Clockify'
|
||||
})
|
||||
})
|
||||
session.commit()
|
||||
|
||||
project_entity = entities[0]
|
||||
if project_entity.entity_type.lower() != "project":
|
||||
project_entity = self.get_project_from_entity(project_entity)
|
||||
|
||||
project_name = project_entity["full_name"]
|
||||
self.log.info(
|
||||
"Synchronization of project \"{}\" to clockify begins.".format(
|
||||
project_name
|
||||
)
|
||||
)
|
||||
task_types = (
|
||||
project_entity["project_schema"]["_task_type_schema"]["types"]
|
||||
)
|
||||
task_type_names = [
|
||||
task_type["name"] for task_type in task_types
|
||||
]
|
||||
try:
|
||||
clockify_projects = self.clockapi.get_projects()
|
||||
if project_name not in clockify_projects:
|
||||
response = self.clockapi.add_project(project_name)
|
||||
if "id" not in response:
|
||||
self.log.warning(
|
||||
"Project \"{}\" can't be created. Response: {}".format(
|
||||
project_name, response
|
||||
)
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"message": (
|
||||
"Can't create clockify project \"{}\"."
|
||||
" Unexpected error."
|
||||
).format(project_name)
|
||||
}
|
||||
|
||||
clockify_workspace_tags = self.clockapi.get_tags()
|
||||
for task_type_name in task_type_names:
|
||||
if task_type_name in clockify_workspace_tags:
|
||||
self.log.debug(
|
||||
"Task \"{}\" already exist".format(task_type_name)
|
||||
)
|
||||
continue
|
||||
|
||||
response = self.clockapi.add_tag(task_type_name)
|
||||
if "id" not in response:
|
||||
self.log.warning(
|
||||
"Task \"{}\" can't be created. Response: {}".format(
|
||||
task_type_name, response
|
||||
)
|
||||
)
|
||||
|
||||
job["status"] = "done"
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
finally:
|
||||
if job["status"] != "done":
|
||||
job["status"] = "failed"
|
||||
session.commit()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def register(session, **kw):
|
||||
SyncClocifyLocal(session).register()
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
import json
|
||||
import ftrack_api
|
||||
from pype.modules.ftrack import BaseAction, MissingPermision
|
||||
from pype.modules.clockify import ClockifyAPI
|
||||
|
||||
|
||||
class SyncClocify(BaseAction):
|
||||
'''Synchronise project names and task types.'''
|
||||
|
||||
#: Action identifier.
|
||||
identifier = 'clockify.sync'
|
||||
#: Action label.
|
||||
label = 'Sync To Clockify'
|
||||
#: Action description.
|
||||
description = 'Synchronise data to Clockify workspace'
|
||||
#: roles that are allowed to register this action
|
||||
role_list = ["Pypeclub", "Administrator", "project Manager"]
|
||||
#: icon
|
||||
icon = '{}/app_icons/clockify-white.png'.format(
|
||||
os.environ.get('PYPE_STATICS_SERVER', '')
|
||||
)
|
||||
#: CLockifyApi
|
||||
clockapi = ClockifyAPI()
|
||||
|
||||
def preregister(self):
|
||||
if self.clockapi.workspace_id is None:
|
||||
return "Clockify Workspace or API key are not set!"
|
||||
|
||||
if self.clockapi.validate_workspace_perm() is False:
|
||||
raise MissingPermision('Clockify')
|
||||
|
||||
return True
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
''' Validation '''
|
||||
if len(entities) != 1:
|
||||
return False
|
||||
|
||||
if entities[0].entity_type.lower() != "project":
|
||||
return False
|
||||
return True
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
# JOB SETTINGS
|
||||
userId = event['source']['user']['id']
|
||||
user = session.query('User where id is ' + userId).one()
|
||||
|
||||
job = session.create('Job', {
|
||||
'user': user,
|
||||
'status': 'running',
|
||||
'data': json.dumps({
|
||||
'description': 'Sync Ftrack to Clockify'
|
||||
})
|
||||
})
|
||||
session.commit()
|
||||
try:
|
||||
entity = entities[0]
|
||||
|
||||
if entity.entity_type.lower() == 'project':
|
||||
project = entity
|
||||
else:
|
||||
project = entity['project']
|
||||
project_name = project['full_name']
|
||||
|
||||
task_types = []
|
||||
for task_type in project['project_schema']['_task_type_schema'][
|
||||
'types'
|
||||
]:
|
||||
task_types.append(task_type['name'])
|
||||
|
||||
clockify_projects = self.clockapi.get_projects()
|
||||
|
||||
if project_name not in clockify_projects:
|
||||
response = self.clockapi.add_project(project_name)
|
||||
if 'id' not in response:
|
||||
self.log.error('Project {} can\'t be created'.format(
|
||||
project_name
|
||||
))
|
||||
return {
|
||||
'success': False,
|
||||
'message': 'Can\'t create project, unexpected error'
|
||||
}
|
||||
project_id = response['id']
|
||||
else:
|
||||
project_id = clockify_projects[project_name]
|
||||
|
||||
clockify_workspace_tags = self.clockapi.get_tags()
|
||||
for task_type in task_types:
|
||||
if task_type not in clockify_workspace_tags:
|
||||
response = self.clockapi.add_tag(task_type)
|
||||
if 'id' not in response:
|
||||
self.log.error('Task {} can\'t be created'.format(
|
||||
task_type
|
||||
))
|
||||
continue
|
||||
except Exception:
|
||||
job['status'] = 'failed'
|
||||
session.commit()
|
||||
return False
|
||||
|
||||
job['status'] = 'done'
|
||||
session.commit()
|
||||
return True
|
||||
|
||||
|
||||
def register(session, **kw):
|
||||
'''Register plugin. Called when used as an plugin.'''
|
||||
|
||||
if not isinstance(session, ftrack_api.session.Session):
|
||||
return
|
||||
|
||||
SyncClocify(session).register()
|
||||
|
||||
|
||||
def main(arguments=None):
|
||||
'''Set up logging and register action.'''
|
||||
if arguments is None:
|
||||
arguments = []
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
# Allow setting of logging level from arguments.
|
||||
loggingLevels = {}
|
||||
for level in (
|
||||
logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
|
||||
logging.ERROR, logging.CRITICAL
|
||||
):
|
||||
loggingLevels[logging.getLevelName(level).lower()] = level
|
||||
|
||||
parser.add_argument(
|
||||
'-v', '--verbosity',
|
||||
help='Set the logging output verbosity.',
|
||||
choices=loggingLevels.keys(),
|
||||
default='info'
|
||||
)
|
||||
namespace = parser.parse_args(arguments)
|
||||
|
||||
# Set up basic logging
|
||||
logging.basicConfig(level=loggingLevels[namespace.verbosity])
|
||||
|
||||
session = ftrack_api.Session()
|
||||
register(session)
|
||||
|
||||
# Wait for events
|
||||
logging.info(
|
||||
'Registered actions and listening for events. Use Ctrl-C to abort.'
|
||||
)
|
||||
session.event_hub.wait()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
from avalon import api, io
|
||||
from pype.api import Logger
|
||||
from pype.modules.clockify import ClockifyAPI
|
||||
from pype.modules.clockify.clockify_api import ClockifyAPI
|
||||
|
||||
|
||||
log = Logger().get_logger(__name__, "clockify_start")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from avalon import api, io
|
||||
from pype.modules.clockify import ClockifyAPI
|
||||
from pype.modules.clockify.clockify_api import ClockifyAPI
|
||||
from pype.api import Logger
|
||||
log = Logger().get_logger(__name__, "clockify_sync")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
from Qt import QtCore, QtGui, QtWidgets
|
||||
from avalon import style
|
||||
|
||||
|
||||
class MessageWidget(QtWidgets.QWidget):
|
||||
|
||||
SIZE_W = 300
|
||||
SIZE_H = 130
|
||||
|
||||
closed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None, messages=[], title="Message"):
|
||||
|
||||
super(MessageWidget, self).__init__()
|
||||
|
||||
self._parent = parent
|
||||
|
||||
# Icon
|
||||
if parent and hasattr(parent, 'icon'):
|
||||
self.setWindowIcon(parent.icon)
|
||||
else:
|
||||
from pypeapp.resources import get_resource
|
||||
self.setWindowIcon(QtGui.QIcon(get_resource('icon.png')))
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowCloseButtonHint |
|
||||
QtCore.Qt.WindowMinimizeButtonHint
|
||||
)
|
||||
|
||||
# Font
|
||||
self.font = QtGui.QFont()
|
||||
self.font.setFamily("DejaVu Sans Condensed")
|
||||
self.font.setPointSize(9)
|
||||
self.font.setBold(True)
|
||||
self.font.setWeight(50)
|
||||
self.font.setKerning(True)
|
||||
|
||||
# Size setting
|
||||
self.resize(self.SIZE_W, self.SIZE_H)
|
||||
self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H))
|
||||
self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100))
|
||||
|
||||
# Style
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
self.setLayout(self._ui_layout(messages))
|
||||
self.setWindowTitle(title)
|
||||
|
||||
def _ui_layout(self, messages):
|
||||
if not messages:
|
||||
messages = ["*Misssing messages (This is a bug)*", ]
|
||||
|
||||
elif not isinstance(messages, (tuple, list)):
|
||||
messages = [messages, ]
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
|
||||
labels = []
|
||||
for message in messages:
|
||||
label = QtWidgets.QLabel(message)
|
||||
label.setFont(self.font)
|
||||
label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
|
||||
label.setTextFormat(QtCore.Qt.RichText)
|
||||
label.setWordWrap(True)
|
||||
|
||||
labels.append(label)
|
||||
main_layout.addWidget(label)
|
||||
|
||||
btn_close = QtWidgets.QPushButton("Close")
|
||||
btn_close.setToolTip('Close this window')
|
||||
btn_close.clicked.connect(self.on_close_clicked)
|
||||
|
||||
btn_group = QtWidgets.QHBoxLayout()
|
||||
btn_group.addStretch(1)
|
||||
btn_group.addWidget(btn_close)
|
||||
|
||||
main_layout.addLayout(btn_group)
|
||||
|
||||
self.labels = labels
|
||||
self.btn_group = btn_group
|
||||
self.btn_close = btn_close
|
||||
self.main_layout = main_layout
|
||||
|
||||
return main_layout
|
||||
|
||||
def on_close_clicked(self):
|
||||
self.close()
|
||||
|
||||
def close(self, *args, **kwargs):
|
||||
self.closed.emit()
|
||||
super(MessageWidget, self).close(*args, **kwargs)
|
||||
|
|
@ -1,6 +1,95 @@
|
|||
import os
|
||||
from Qt import QtCore, QtGui, QtWidgets
|
||||
from avalon import style
|
||||
from pype.api import resources
|
||||
|
||||
|
||||
class MessageWidget(QtWidgets.QWidget):
|
||||
|
||||
SIZE_W = 300
|
||||
SIZE_H = 130
|
||||
|
||||
closed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None, messages=[], title="Message"):
|
||||
|
||||
super(MessageWidget, self).__init__()
|
||||
|
||||
self._parent = parent
|
||||
|
||||
# Icon
|
||||
if parent and hasattr(parent, 'icon'):
|
||||
self.setWindowIcon(parent.icon)
|
||||
else:
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowCloseButtonHint |
|
||||
QtCore.Qt.WindowMinimizeButtonHint
|
||||
)
|
||||
|
||||
# Font
|
||||
self.font = QtGui.QFont()
|
||||
self.font.setFamily("DejaVu Sans Condensed")
|
||||
self.font.setPointSize(9)
|
||||
self.font.setBold(True)
|
||||
self.font.setWeight(50)
|
||||
self.font.setKerning(True)
|
||||
|
||||
# Size setting
|
||||
self.resize(self.SIZE_W, self.SIZE_H)
|
||||
self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H))
|
||||
self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100))
|
||||
|
||||
# Style
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
self.setLayout(self._ui_layout(messages))
|
||||
self.setWindowTitle(title)
|
||||
|
||||
def _ui_layout(self, messages):
|
||||
if not messages:
|
||||
messages = ["*Misssing messages (This is a bug)*", ]
|
||||
|
||||
elif not isinstance(messages, (tuple, list)):
|
||||
messages = [messages, ]
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
|
||||
labels = []
|
||||
for message in messages:
|
||||
label = QtWidgets.QLabel(message)
|
||||
label.setFont(self.font)
|
||||
label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
|
||||
label.setTextFormat(QtCore.Qt.RichText)
|
||||
label.setWordWrap(True)
|
||||
|
||||
labels.append(label)
|
||||
main_layout.addWidget(label)
|
||||
|
||||
btn_close = QtWidgets.QPushButton("Close")
|
||||
btn_close.setToolTip('Close this window')
|
||||
btn_close.clicked.connect(self.on_close_clicked)
|
||||
|
||||
btn_group = QtWidgets.QHBoxLayout()
|
||||
btn_group.addStretch(1)
|
||||
btn_group.addWidget(btn_close)
|
||||
|
||||
main_layout.addLayout(btn_group)
|
||||
|
||||
self.labels = labels
|
||||
self.btn_group = btn_group
|
||||
self.btn_close = btn_close
|
||||
self.main_layout = main_layout
|
||||
|
||||
return main_layout
|
||||
|
||||
def on_close_clicked(self):
|
||||
self.close()
|
||||
|
||||
def close(self, *args, **kwargs):
|
||||
self.closed.emit()
|
||||
super(MessageWidget, self).close(*args, **kwargs)
|
||||
|
||||
|
||||
class ClockifySettings(QtWidgets.QWidget):
|
||||
|
|
@ -26,10 +115,7 @@ class ClockifySettings(QtWidgets.QWidget):
|
|||
elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'):
|
||||
self.setWindowIcon(self.parent.parent.icon)
|
||||
else:
|
||||
pype_setup = os.getenv('PYPE_SETUP_PATH')
|
||||
items = [pype_setup, "app", "resources", "icon.png"]
|
||||
fname = os.path.sep.join(items)
|
||||
icon = QtGui.QIcon(fname)
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.setWindowFlags(
|
||||
|
|
@ -497,9 +497,8 @@ class DeleteAssetSubset(BaseAction):
|
|||
for entity in entities:
|
||||
ftrack_id = entity["id"]
|
||||
ftrack_id_name_map[ftrack_id] = entity["name"]
|
||||
if ftrack_id in ftrack_ids_to_delete:
|
||||
continue
|
||||
not_deleted_entities_id.append(ftrack_id)
|
||||
if ftrack_id not in ftrack_ids_to_delete:
|
||||
not_deleted_entities_id.append(ftrack_id)
|
||||
|
||||
mongo_proc_txt = "MongoProcessing: "
|
||||
ftrack_proc_txt = "Ftrack processing: "
|
||||
|
|
@ -534,25 +533,20 @@ class DeleteAssetSubset(BaseAction):
|
|||
ftrack_proc_txt, ", ".join(ftrack_ids_to_delete)
|
||||
))
|
||||
|
||||
joined_ids_to_delete = ", ".join(
|
||||
["\"{}\"".format(id) for id in ftrack_ids_to_delete]
|
||||
ftrack_ents_to_delete = (
|
||||
self._filter_entities_to_delete(ftrack_ids_to_delete, session)
|
||||
)
|
||||
ftrack_ents_to_delete = self.session.query(
|
||||
"select id, link from TypedContext where id in ({})".format(
|
||||
joined_ids_to_delete
|
||||
)
|
||||
).all()
|
||||
for entity in ftrack_ents_to_delete:
|
||||
self.session.delete(entity)
|
||||
session.delete(entity)
|
||||
try:
|
||||
self.session.commit()
|
||||
session.commit()
|
||||
except Exception:
|
||||
ent_path = "/".join(
|
||||
[ent["name"] for ent in entity["link"]]
|
||||
)
|
||||
msg = "Failed to delete entity"
|
||||
report_messages[msg].append(ent_path)
|
||||
self.session.rollback()
|
||||
session.rollback()
|
||||
self.log.warning(
|
||||
"{} <{}>".format(msg, ent_path),
|
||||
exc_info=True
|
||||
|
|
@ -568,7 +562,7 @@ class DeleteAssetSubset(BaseAction):
|
|||
for name in asset_names_to_delete
|
||||
])
|
||||
# Find assets of selected entities with names of checked subsets
|
||||
assets = self.session.query((
|
||||
assets = session.query((
|
||||
"select id from Asset where"
|
||||
" context_id in ({}) and name in ({})"
|
||||
).format(joined_not_deleted, joined_asset_names)).all()
|
||||
|
|
@ -578,20 +572,54 @@ class DeleteAssetSubset(BaseAction):
|
|||
", ".join([asset["id"] for asset in assets])
|
||||
))
|
||||
for asset in assets:
|
||||
self.session.delete(asset)
|
||||
session.delete(asset)
|
||||
try:
|
||||
self.session.commit()
|
||||
session.commit()
|
||||
except Exception:
|
||||
self.session.rollback()
|
||||
session.rollback()
|
||||
msg = "Failed to delete asset"
|
||||
report_messages[msg].append(asset["id"])
|
||||
self.log.warning(
|
||||
"{} <{}>".format(asset["id"]),
|
||||
"Asset: {} <{}>".format(asset["name"], asset["id"]),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
return self.report_handle(report_messages, project_name, event)
|
||||
|
||||
def _filter_entities_to_delete(self, ftrack_ids_to_delete, session):
|
||||
"""Filter children entities to avoid CircularDependencyError."""
|
||||
joined_ids_to_delete = ", ".join(
|
||||
["\"{}\"".format(id) for id in ftrack_ids_to_delete]
|
||||
)
|
||||
to_delete_entities = session.query(
|
||||
"select id, link from TypedContext where id in ({})".format(
|
||||
joined_ids_to_delete
|
||||
)
|
||||
).all()
|
||||
filtered = to_delete_entities[:]
|
||||
while True:
|
||||
changed = False
|
||||
_filtered = filtered[:]
|
||||
for entity in filtered:
|
||||
entity_id = entity["id"]
|
||||
|
||||
for _entity in tuple(_filtered):
|
||||
if entity_id == _entity["id"]:
|
||||
continue
|
||||
|
||||
for _link in _entity["link"]:
|
||||
if entity_id == _link["id"] and _entity in _filtered:
|
||||
_filtered.remove(_entity)
|
||||
changed = True
|
||||
break
|
||||
|
||||
filtered = _filtered
|
||||
|
||||
if not changed:
|
||||
break
|
||||
|
||||
return filtered
|
||||
|
||||
def report_handle(self, report_messages, project_name, event):
|
||||
if not report_messages:
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
import os
|
||||
import sys
|
||||
import logging
|
||||
import subprocess
|
||||
from operator import itemgetter
|
||||
import ftrack_api
|
||||
from pype.modules.ftrack.lib import BaseAction, statics_icon
|
||||
from pype.api import Logger, config
|
||||
|
||||
log = Logger().get_logger(__name__)
|
||||
|
||||
|
||||
class DJVViewAction(BaseAction):
|
||||
|
|
@ -19,20 +13,18 @@ class DJVViewAction(BaseAction):
|
|||
|
||||
type = 'Application'
|
||||
|
||||
allowed_types = [
|
||||
"cin", "dpx", "avi", "dv", "gif", "flv", "mkv", "mov", "mpg", "mpeg",
|
||||
"mp4", "m4v", "mxf", "iff", "z", "ifl", "jpeg", "jpg", "jfif", "lut",
|
||||
"1dl", "exr", "pic", "png", "ppm", "pnm", "pgm", "pbm", "rla", "rpf",
|
||||
"sgi", "rgba", "rgb", "bw", "tga", "tiff", "tif", "img"
|
||||
]
|
||||
|
||||
def __init__(self, session, plugins_presets):
|
||||
'''Expects a ftrack_api.Session instance'''
|
||||
super().__init__(session, plugins_presets)
|
||||
self.djv_path = None
|
||||
|
||||
self.config_data = config.get_presets()['djv_view']['config']
|
||||
self.set_djv_path()
|
||||
|
||||
if self.djv_path is None:
|
||||
return
|
||||
|
||||
self.allowed_types = self.config_data.get(
|
||||
'file_ext', ["img", "mov", "exr"]
|
||||
)
|
||||
self.djv_path = self.find_djv_path()
|
||||
|
||||
def preregister(self):
|
||||
if self.djv_path is None:
|
||||
|
|
@ -53,11 +45,10 @@ class DJVViewAction(BaseAction):
|
|||
return True
|
||||
return False
|
||||
|
||||
def set_djv_path(self):
|
||||
for path in self.config_data.get("djv_paths", []):
|
||||
def find_djv_path(self):
|
||||
for path in (os.environ.get("DJV_PATH") or "").split(os.pathsep):
|
||||
if os.path.exists(path):
|
||||
self.djv_path = path
|
||||
break
|
||||
return path
|
||||
|
||||
def interface(self, session, entities, event):
|
||||
if event['data'].get('values', {}):
|
||||
|
|
@ -221,43 +212,3 @@ def register(session, plugins_presets={}):
|
|||
"""Register hooks."""
|
||||
|
||||
DJVViewAction(session, plugins_presets).register()
|
||||
|
||||
|
||||
def main(arguments=None):
|
||||
'''Set up logging and register action.'''
|
||||
if arguments is None:
|
||||
arguments = []
|
||||
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
# Allow setting of logging level from arguments.
|
||||
loggingLevels = {}
|
||||
for level in (
|
||||
logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
|
||||
logging.ERROR, logging.CRITICAL
|
||||
):
|
||||
loggingLevels[logging.getLevelName(level).lower()] = level
|
||||
|
||||
parser.add_argument(
|
||||
'-v', '--verbosity',
|
||||
help='Set the logging output verbosity.',
|
||||
choices=loggingLevels.keys(),
|
||||
default='info'
|
||||
)
|
||||
namespace = parser.parse_args(arguments)
|
||||
|
||||
# Set up basic logging
|
||||
logging.basicConfig(level=loggingLevels[namespace.verbosity])
|
||||
|
||||
session = ftrack_api.Session()
|
||||
register(session)
|
||||
|
||||
# Wait for events
|
||||
logging.info(
|
||||
'Registered actions and listening for events. Use Ctrl-C to abort.'
|
||||
)
|
||||
session.event_hub.wait()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from pype.modules.ftrack import BaseAction
|
||||
from pype.modules.ftrack.lib.avalon_sync import SyncEntitiesFactory
|
||||
from pype.api import config
|
||||
|
||||
|
||||
class SyncToAvalonServer(BaseAction):
|
||||
|
|
@ -38,17 +36,6 @@ class SyncToAvalonServer(BaseAction):
|
|||
variant = "- Sync To Avalon (Server)"
|
||||
#: Action description.
|
||||
description = "Send data from Ftrack to Avalon"
|
||||
#: Action icon.
|
||||
icon = "{}/ftrack/action_icons/PypeAdmin.svg".format(
|
||||
os.environ.get(
|
||||
"PYPE_STATICS_SERVER",
|
||||
"http://localhost:{}".format(
|
||||
config.get_presets().get("services", {}).get(
|
||||
"rest_api", {}
|
||||
).get("default_port", 8021)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -84,6 +84,9 @@ class VersionToTaskStatus(BaseEvent):
|
|||
if not task:
|
||||
continue
|
||||
|
||||
if version["asset"]["type"]["short"].lower() == "scene":
|
||||
continue
|
||||
|
||||
project_schema = task["project"]["project_schema"]
|
||||
# Get all available statuses for Task
|
||||
statuses = project_schema.get_statuses("Task", task["type_id"])
|
||||
|
|
|
|||
|
|
@ -522,6 +522,21 @@ def main(argv):
|
|||
help="Load creadentials from apps dir",
|
||||
action="store_true"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-clockifyapikey", type=str,
|
||||
help=(
|
||||
"Enter API key for Clockify actions."
|
||||
" (default from environment: $CLOCKIFY_API_KEY)"
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"-clockifyworkspace", type=str,
|
||||
help=(
|
||||
"Enter workspace for Clockify."
|
||||
" (default from module presets or "
|
||||
"environment: $CLOCKIFY_WORKSPACE)"
|
||||
)
|
||||
)
|
||||
ftrack_url = os.environ.get('FTRACK_SERVER')
|
||||
username = os.environ.get('FTRACK_API_USER')
|
||||
api_key = os.environ.get('FTRACK_API_KEY')
|
||||
|
|
@ -546,6 +561,12 @@ def main(argv):
|
|||
if kwargs.ftrackapikey:
|
||||
api_key = kwargs.ftrackapikey
|
||||
|
||||
if kwargs.clockifyworkspace:
|
||||
os.environ["CLOCKIFY_WORKSPACE"] = kwargs.clockifyworkspace
|
||||
|
||||
if kwargs.clockifyapikey:
|
||||
os.environ["CLOCKIFY_API_KEY"] = kwargs.clockifyapikey
|
||||
|
||||
legacy = kwargs.legacy
|
||||
# Check url regex and accessibility
|
||||
ftrack_url = check_ftrack_url(ftrack_url)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from pype.api import (
|
|||
compose_url
|
||||
)
|
||||
|
||||
from pype.modules.ftrack.lib.custom_db_connector import DbConnector
|
||||
from pype.modules.ftrack.lib.custom_db_connector import CustomDbConnector
|
||||
|
||||
|
||||
TOPIC_STATUS_SERVER = "pype.event.server.status"
|
||||
|
|
@ -44,15 +44,8 @@ def get_ftrack_event_mongo_info():
|
|||
mongo_url = os.environ.get("FTRACK_EVENTS_MONGO_URL")
|
||||
if mongo_url is not None:
|
||||
components = decompose_url(mongo_url)
|
||||
_used_ftrack_url = True
|
||||
else:
|
||||
components = get_default_components()
|
||||
_used_ftrack_url = False
|
||||
|
||||
if not _used_ftrack_url or components["database"] is None:
|
||||
components["database"] = database_name
|
||||
|
||||
components.pop("collection", None)
|
||||
|
||||
uri = compose_url(**components)
|
||||
|
||||
|
|
@ -166,10 +159,10 @@ class ProcessEventHub(SocketBaseEventHub):
|
|||
pypelog = Logger().get_logger("Session Processor")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.dbcon = DbConnector(
|
||||
self.dbcon = CustomDbConnector(
|
||||
self.uri,
|
||||
self.port,
|
||||
self.database,
|
||||
self.port,
|
||||
self.table_name
|
||||
)
|
||||
super(ProcessEventHub, self).__init__(*args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from pype.api import Logger
|
|||
class SocketThread(threading.Thread):
|
||||
"""Thread that checks suprocess of storer of processor of events"""
|
||||
|
||||
MAX_TIMEOUT = 35
|
||||
MAX_TIMEOUT = int(os.environ.get("PYPE_FTRACK_SOCKET_TIMEOUT", 45))
|
||||
|
||||
def __init__(self, name, port, filepath, additional_args=[]):
|
||||
super(SocketThread, self).__init__()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from pype.modules.ftrack.ftrack_server.lib import (
|
|||
SocketSession, ProcessEventHub, TOPIC_STATUS_SERVER
|
||||
)
|
||||
import ftrack_api
|
||||
from pype.api import Logger
|
||||
from pype.api import Logger, config
|
||||
|
||||
log = Logger().get_logger("Event processor")
|
||||
|
||||
|
|
@ -55,6 +55,42 @@ def register(session):
|
|||
)
|
||||
|
||||
|
||||
def clockify_module_registration():
|
||||
module_name = "Clockify"
|
||||
|
||||
menu_items = config.get_presets()["tray"]["menu_items"]
|
||||
if not menu_items["item_usage"][module_name]:
|
||||
return
|
||||
|
||||
api_key = os.environ.get("CLOCKIFY_API_KEY")
|
||||
if not api_key:
|
||||
log.warning("Clockify API key is not set.")
|
||||
return
|
||||
|
||||
workspace_name = os.environ.get("CLOCKIFY_WORKSPACE")
|
||||
if not workspace_name:
|
||||
workspace_name = (
|
||||
menu_items
|
||||
.get("attributes", {})
|
||||
.get(module_name, {})
|
||||
.get("workspace_name", {})
|
||||
)
|
||||
|
||||
if not workspace_name:
|
||||
log.warning("Clockify Workspace is not set.")
|
||||
return
|
||||
|
||||
os.environ["CLOCKIFY_WORKSPACE"] = workspace_name
|
||||
|
||||
from pype.modules.clockify.constants import CLOCKIFY_FTRACK_SERVER_PATH
|
||||
|
||||
current = os.environ.get("FTRACK_EVENTS_PATH") or ""
|
||||
if current:
|
||||
current += os.pathsep
|
||||
os.environ["FTRACK_EVENTS_PATH"] = current + CLOCKIFY_FTRACK_SERVER_PATH
|
||||
return True
|
||||
|
||||
|
||||
def main(args):
|
||||
port = int(args[-1])
|
||||
# Create a TCP/IP socket
|
||||
|
|
@ -66,6 +102,11 @@ def main(args):
|
|||
sock.connect(server_address)
|
||||
|
||||
sock.sendall(b"CreatedProcess")
|
||||
try:
|
||||
clockify_module_registration()
|
||||
except Exception:
|
||||
log.info("Clockify registration failed.", exc_info=True)
|
||||
|
||||
try:
|
||||
session = SocketSession(
|
||||
auto_connect_event_hub=True, sock=sock, Eventhub=ProcessEventHub
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from pype.modules.ftrack.ftrack_server.lib import (
|
|||
get_ftrack_event_mongo_info,
|
||||
TOPIC_STATUS_SERVER, TOPIC_STATUS_SERVER_RESULT
|
||||
)
|
||||
from pype.modules.ftrack.lib.custom_db_connector import DbConnector
|
||||
from pype.modules.ftrack.lib.custom_db_connector import CustomDbConnector
|
||||
from pype.api import Logger
|
||||
|
||||
log = Logger().get_logger("Event storer")
|
||||
|
|
@ -24,7 +24,7 @@ class SessionFactory:
|
|||
|
||||
|
||||
uri, port, database, table_name = get_ftrack_event_mongo_info()
|
||||
dbcon = DbConnector(uri, port, database, table_name)
|
||||
dbcon = CustomDbConnector(uri, database, port, table_name)
|
||||
|
||||
# ignore_topics = ["ftrack.meta.connected"]
|
||||
ignore_topics = []
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import time
|
|||
import logging
|
||||
import functools
|
||||
import atexit
|
||||
import os
|
||||
|
||||
# Third-party dependencies
|
||||
import pymongo
|
||||
|
|
@ -40,7 +41,7 @@ def auto_reconnect(func):
|
|||
|
||||
|
||||
def check_active_table(func):
|
||||
"""Check if DbConnector has active table before db method is called"""
|
||||
"""Check if CustomDbConnector has active collection."""
|
||||
@functools.wraps(func)
|
||||
def decorated(obj, *args, **kwargs):
|
||||
if not obj.active_table:
|
||||
|
|
@ -49,23 +50,12 @@ def check_active_table(func):
|
|||
return decorated
|
||||
|
||||
|
||||
def check_active_table(func):
|
||||
"""Handling auto reconnect in 3 retry times"""
|
||||
@functools.wraps(func)
|
||||
def decorated(obj, *args, **kwargs):
|
||||
if not obj.active_table:
|
||||
raise NotActiveTable("Active table is not set. (This is bug)")
|
||||
return func(obj, *args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
|
||||
class DbConnector:
|
||||
class CustomDbConnector:
|
||||
log = logging.getLogger(__name__)
|
||||
timeout = 1000
|
||||
timeout = int(os.environ["AVALON_TIMEOUT"])
|
||||
|
||||
def __init__(
|
||||
self, uri, port=None, database_name=None, table_name=None
|
||||
self, uri, database_name, port=None, table_name=None
|
||||
):
|
||||
self._mongo_client = None
|
||||
self._sentry_client = None
|
||||
|
|
@ -78,9 +68,6 @@ class DbConnector:
|
|||
if port is None:
|
||||
port = components.get("port")
|
||||
|
||||
if database_name is None:
|
||||
database_name = components.get("database")
|
||||
|
||||
if database_name is None:
|
||||
raise ValueError(
|
||||
"Database is not defined for connection. {}".format(uri)
|
||||
|
|
@ -99,7 +86,7 @@ class DbConnector:
|
|||
# not all methods of PyMongo database are implemented with this it is
|
||||
# possible to use them too
|
||||
try:
|
||||
return super(DbConnector, self).__getattribute__(attr)
|
||||
return super(CustomDbConnector, self).__getattribute__(attr)
|
||||
except AttributeError:
|
||||
if self.active_table is None:
|
||||
raise NotActiveTable()
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@ import copy
|
|||
import platform
|
||||
import avalon.lib
|
||||
import acre
|
||||
import getpass
|
||||
from pype import lib as pypelib
|
||||
from pype.api import config, Anatomy
|
||||
from .ftrack_action_handler import BaseAction
|
||||
from avalon.api import (
|
||||
last_workfile, HOST_WORKFILE_EXTENSIONS, should_start_last_workfile
|
||||
)
|
||||
|
||||
|
||||
class AppAction(BaseAction):
|
||||
|
|
@ -82,7 +86,7 @@ class AppAction(BaseAction):
|
|||
|
||||
if (
|
||||
len(entities) != 1
|
||||
or entities[0].entity_type.lower() != 'task'
|
||||
or entities[0].entity_type.lower() != "task"
|
||||
):
|
||||
return False
|
||||
|
||||
|
|
@ -90,21 +94,31 @@ class AppAction(BaseAction):
|
|||
if entity["parent"].entity_type.lower() == "project":
|
||||
return False
|
||||
|
||||
ft_project = self.get_project_from_entity(entity)
|
||||
database = pypelib.get_avalon_database()
|
||||
project_name = ft_project["full_name"]
|
||||
avalon_project = database[project_name].find_one({
|
||||
"type": "project"
|
||||
})
|
||||
avalon_project_apps = event["data"].get("avalon_project_apps", None)
|
||||
avalon_project_doc = event["data"].get("avalon_project_doc", None)
|
||||
if avalon_project_apps is None:
|
||||
if avalon_project_doc is None:
|
||||
ft_project = self.get_project_from_entity(entity)
|
||||
database = pypelib.get_avalon_database()
|
||||
project_name = ft_project["full_name"]
|
||||
avalon_project_doc = database[project_name].find_one({
|
||||
"type": "project"
|
||||
}) or False
|
||||
event["data"]["avalon_project_doc"] = avalon_project_doc
|
||||
|
||||
if not avalon_project:
|
||||
if not avalon_project_doc:
|
||||
return False
|
||||
|
||||
project_apps_config = avalon_project_doc["config"].get("apps", [])
|
||||
avalon_project_apps = [
|
||||
app["name"] for app in project_apps_config
|
||||
] or False
|
||||
event["data"]["avalon_project_apps"] = avalon_project_apps
|
||||
|
||||
if not avalon_project_apps:
|
||||
return False
|
||||
|
||||
project_apps = avalon_project["config"].get("apps", [])
|
||||
apps = [app["name"] for app in project_apps]
|
||||
if self.identifier in apps:
|
||||
return True
|
||||
return False
|
||||
return self.identifier in avalon_project_apps
|
||||
|
||||
def _launch(self, event):
|
||||
entities = self._translate_event(event)
|
||||
|
|
@ -140,6 +154,9 @@ class AppAction(BaseAction):
|
|||
"""
|
||||
|
||||
entity = entities[0]
|
||||
|
||||
task_name = entity["name"]
|
||||
|
||||
project_name = entity["project"]["full_name"]
|
||||
|
||||
database = pypelib.get_avalon_database()
|
||||
|
|
@ -152,18 +169,19 @@ class AppAction(BaseAction):
|
|||
|
||||
hierarchy = ""
|
||||
asset_doc_parents = asset_document["data"].get("parents")
|
||||
if len(asset_doc_parents) > 0:
|
||||
if asset_doc_parents:
|
||||
hierarchy = os.path.join(*asset_doc_parents)
|
||||
|
||||
application = avalon.lib.get_application(self.identifier)
|
||||
host_name = application["application_dir"]
|
||||
data = {
|
||||
"project": {
|
||||
"name": entity["project"]["full_name"],
|
||||
"code": entity["project"]["name"]
|
||||
},
|
||||
"task": entity["name"],
|
||||
"task": task_name,
|
||||
"asset": asset_name,
|
||||
"app": application["application_dir"],
|
||||
"app": host_name,
|
||||
"hierarchy": hierarchy
|
||||
}
|
||||
|
||||
|
|
@ -187,17 +205,48 @@ class AppAction(BaseAction):
|
|||
except FileExistsError:
|
||||
pass
|
||||
|
||||
last_workfile_path = None
|
||||
extensions = HOST_WORKFILE_EXTENSIONS.get(host_name)
|
||||
if extensions:
|
||||
# Find last workfile
|
||||
file_template = anatomy.templates["work"]["file"]
|
||||
data.update({
|
||||
"version": 1,
|
||||
"user": getpass.getuser(),
|
||||
"ext": extensions[0]
|
||||
})
|
||||
|
||||
last_workfile_path = last_workfile(
|
||||
workdir, file_template, data, extensions, True
|
||||
)
|
||||
|
||||
# set environments for Avalon
|
||||
prep_env = copy.deepcopy(os.environ)
|
||||
prep_env.update({
|
||||
"AVALON_PROJECT": project_name,
|
||||
"AVALON_ASSET": asset_name,
|
||||
"AVALON_TASK": entity["name"],
|
||||
"AVALON_APP": self.identifier.split("_")[0],
|
||||
"AVALON_TASK": task_name,
|
||||
"AVALON_APP": host_name,
|
||||
"AVALON_APP_NAME": self.identifier,
|
||||
"AVALON_HIERARCHY": hierarchy,
|
||||
"AVALON_WORKDIR": workdir
|
||||
})
|
||||
|
||||
start_last_workfile = should_start_last_workfile(
|
||||
project_name, host_name, task_name
|
||||
)
|
||||
# Store boolean as "0"(False) or "1"(True)
|
||||
prep_env["AVALON_OPEN_LAST_WORKFILE"] = (
|
||||
str(int(bool(start_last_workfile)))
|
||||
)
|
||||
|
||||
if (
|
||||
start_last_workfile
|
||||
and last_workfile_path
|
||||
and os.path.exists(last_workfile_path)
|
||||
):
|
||||
prep_env["AVALON_LAST_WORKFILE"] = last_workfile_path
|
||||
|
||||
prep_env.update(anatomy.roots_obj.root_environments())
|
||||
|
||||
# collect all parents from the task
|
||||
|
|
@ -213,7 +262,6 @@ class AppAction(BaseAction):
|
|||
tools_env = acre.get_tools(tools_attr)
|
||||
env = acre.compute(tools_env)
|
||||
env = acre.merge(env, current_env=dict(prep_env))
|
||||
env = acre.append(dict(prep_env), env)
|
||||
|
||||
# Get path to execute
|
||||
st_temp_path = os.environ["PYPE_CONFIG"]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import requests
|
|||
from avalon import style
|
||||
from pype.modules.ftrack import credentials
|
||||
from . import login_tools
|
||||
from pype.api import resources
|
||||
from Qt import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
|
|
@ -29,10 +30,7 @@ class Login_Dialog_ui(QtWidgets.QWidget):
|
|||
elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'):
|
||||
self.setWindowIcon(self.parent.parent.icon)
|
||||
else:
|
||||
pype_setup = os.getenv('PYPE_SETUP_PATH')
|
||||
items = [pype_setup, "app", "resources", "icon.png"]
|
||||
fname = os.path.sep.join(items)
|
||||
icon = QtGui.QIcon(fname)
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.setWindowFlags(
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from urllib import parse
|
||||
import os
|
||||
import webbrowser
|
||||
import functools
|
||||
import pype
|
||||
import inspect
|
||||
from Qt import QtCore
|
||||
from pype.api import resources
|
||||
|
||||
|
||||
class LoginServerHandler(BaseHTTPRequestHandler):
|
||||
'''Login server handler.'''
|
||||
|
||||
message_filepath = resources.get_resource("ftrack", "sign_in_message.html")
|
||||
|
||||
def __init__(self, login_callback, *args, **kw):
|
||||
'''Initialise handler.'''
|
||||
self.login_callback = login_callback
|
||||
|
|
@ -28,23 +28,21 @@ class LoginServerHandler(BaseHTTPRequestHandler):
|
|||
login_credentials = parse.parse_qs(query)
|
||||
api_user = login_credentials['api_user'][0]
|
||||
api_key = login_credentials['api_key'][0]
|
||||
# get path to resources
|
||||
path_items = os.path.dirname(
|
||||
inspect.getfile(pype)
|
||||
).split(os.path.sep)
|
||||
del path_items[-1]
|
||||
path_items.extend(['res', 'ftrack', 'sign_in_message.html'])
|
||||
message_filepath = os.path.sep.join(path_items)
|
||||
message_file = open(message_filepath, 'r')
|
||||
sign_in_message = message_file.read()
|
||||
message_file.close()
|
||||
|
||||
with open(self.message_filepath, "r") as message_file:
|
||||
sign_in_message = message_file.read()
|
||||
|
||||
# formatting html code for python
|
||||
replacement = [('{', '{{'), ('}', '}}'), ('{{}}', '{}')]
|
||||
for r in (replacement):
|
||||
sign_in_message = sign_in_message.replace(*r)
|
||||
replacements = (
|
||||
("{", "{{"),
|
||||
("}", "}}"),
|
||||
("{{}}", "{}")
|
||||
)
|
||||
for replacement in (replacements):
|
||||
sign_in_message = sign_in_message.replace(*replacement)
|
||||
message = sign_in_message.format(api_user)
|
||||
else:
|
||||
message = '<h1>Failed to sign in</h1>'
|
||||
message = "<h1>Failed to sign in</h1>"
|
||||
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
|
|
@ -74,7 +72,6 @@ class LoginServerThread(QtCore.QThread):
|
|||
|
||||
def run(self):
|
||||
'''Listen for events.'''
|
||||
# self._server = BaseHTTPServer.HTTPServer(
|
||||
self._server = HTTPServer(
|
||||
('localhost', 0),
|
||||
functools.partial(
|
||||
|
|
|
|||
|
|
@ -1,26 +1,25 @@
|
|||
import time
|
||||
import collections
|
||||
from Qt import QtCore
|
||||
import threading
|
||||
from pynput import mouse, keyboard
|
||||
from pype.api import Logger
|
||||
|
||||
|
||||
class IdleManager(QtCore.QThread):
|
||||
class IdleManager(threading.Thread):
|
||||
""" Measure user's idle time in seconds.
|
||||
Idle time resets on keyboard/mouse input.
|
||||
Is able to emit signals at specific time idle.
|
||||
"""
|
||||
time_signals = collections.defaultdict(list)
|
||||
time_callbacks = collections.defaultdict(list)
|
||||
idle_time = 0
|
||||
signal_reset_timer = QtCore.Signal()
|
||||
|
||||
def __init__(self):
|
||||
super(IdleManager, self).__init__()
|
||||
self.log = Logger().get_logger(self.__class__.__name__)
|
||||
self.signal_reset_timer.connect(self._reset_time)
|
||||
self.qaction = None
|
||||
self.failed_icon = None
|
||||
self._is_running = False
|
||||
self.threads = []
|
||||
|
||||
def set_qaction(self, qaction, failed_icon):
|
||||
self.qaction = qaction
|
||||
|
|
@ -32,18 +31,18 @@ class IdleManager(QtCore.QThread):
|
|||
def tray_exit(self):
|
||||
self.stop()
|
||||
try:
|
||||
self.time_signals = {}
|
||||
self.time_callbacks = {}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def add_time_signal(self, emit_time, signal):
|
||||
""" If any module want to use IdleManager, need to use add_time_signal
|
||||
:param emit_time: time when signal will be emitted
|
||||
:type emit_time: int
|
||||
:param signal: signal that will be emitted (without objects)
|
||||
:type signal: QtCore.Signal
|
||||
def add_time_callback(self, emit_time, callback):
|
||||
"""If any module want to use IdleManager, need to use this method.
|
||||
|
||||
Args:
|
||||
emit_time(int): Time when callback will be triggered.
|
||||
callback(func): Callback that will be triggered.
|
||||
"""
|
||||
self.time_signals[emit_time].append(signal)
|
||||
self.time_callbacks[emit_time].append(callback)
|
||||
|
||||
@property
|
||||
def is_running(self):
|
||||
|
|
@ -58,17 +57,26 @@ class IdleManager(QtCore.QThread):
|
|||
def run(self):
|
||||
self.log.info('IdleManager has started')
|
||||
self._is_running = True
|
||||
thread_mouse = MouseThread(self.signal_reset_timer)
|
||||
thread_mouse = MouseThread(self._reset_time)
|
||||
thread_mouse.start()
|
||||
thread_keyboard = KeyboardThread(self.signal_reset_timer)
|
||||
thread_keyboard = KeyboardThread(self._reset_time)
|
||||
thread_keyboard.start()
|
||||
try:
|
||||
while self.is_running:
|
||||
if self.idle_time in self.time_callbacks:
|
||||
for callback in self.time_callbacks[self.idle_time]:
|
||||
thread = threading.Thread(target=callback)
|
||||
thread.start()
|
||||
self.threads.append(thread)
|
||||
|
||||
for thread in tuple(self.threads):
|
||||
if not thread.isAlive():
|
||||
thread.join()
|
||||
self.threads.remove(thread)
|
||||
|
||||
self.idle_time += 1
|
||||
if self.idle_time in self.time_signals:
|
||||
for signal in self.time_signals[self.idle_time]:
|
||||
signal.emit()
|
||||
time.sleep(1)
|
||||
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
'Idle Manager service has failed', exc_info=True
|
||||
|
|
@ -79,16 +87,14 @@ class IdleManager(QtCore.QThread):
|
|||
|
||||
# Threads don't have their attrs when Qt application already finished
|
||||
try:
|
||||
thread_mouse.signal_stop.emit()
|
||||
thread_mouse.terminate()
|
||||
thread_mouse.wait()
|
||||
thread_mouse.stop()
|
||||
thread_mouse.join()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
thread_keyboard.signal_stop.emit()
|
||||
thread_keyboard.terminate()
|
||||
thread_keyboard.wait()
|
||||
thread_keyboard.stop()
|
||||
thread_keyboard.join()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
|
@ -96,49 +102,24 @@ class IdleManager(QtCore.QThread):
|
|||
self.log.info('IdleManager has stopped')
|
||||
|
||||
|
||||
class MouseThread(QtCore.QThread):
|
||||
"""Listens user's mouse movement
|
||||
"""
|
||||
signal_stop = QtCore.Signal()
|
||||
class MouseThread(mouse.Listener):
|
||||
"""Listens user's mouse movement."""
|
||||
|
||||
def __init__(self, signal):
|
||||
super(MouseThread, self).__init__()
|
||||
self.signal_stop.connect(self.stop)
|
||||
self.m_listener = None
|
||||
|
||||
self.signal_reset_timer = signal
|
||||
|
||||
def stop(self):
|
||||
if self.m_listener is not None:
|
||||
self.m_listener.stop()
|
||||
def __init__(self, callback):
|
||||
super(MouseThread, self).__init__(on_move=self.on_move)
|
||||
self.callback = callback
|
||||
|
||||
def on_move(self, posx, posy):
|
||||
self.signal_reset_timer.emit()
|
||||
|
||||
def run(self):
|
||||
self.m_listener = mouse.Listener(on_move=self.on_move)
|
||||
self.m_listener.start()
|
||||
self.callback()
|
||||
|
||||
|
||||
class KeyboardThread(QtCore.QThread):
|
||||
"""Listens user's keyboard input
|
||||
"""
|
||||
signal_stop = QtCore.Signal()
|
||||
class KeyboardThread(keyboard.Listener):
|
||||
"""Listens user's keyboard input."""
|
||||
|
||||
def __init__(self, signal):
|
||||
super(KeyboardThread, self).__init__()
|
||||
self.signal_stop.connect(self.stop)
|
||||
self.k_listener = None
|
||||
def __init__(self, callback):
|
||||
super(KeyboardThread, self).__init__(on_press=self.on_press)
|
||||
|
||||
self.signal_reset_timer = signal
|
||||
|
||||
def stop(self):
|
||||
if self.k_listener is not None:
|
||||
self.k_listener.stop()
|
||||
self.callback = callback
|
||||
|
||||
def on_press(self, key):
|
||||
self.signal_reset_timer.emit()
|
||||
|
||||
def run(self):
|
||||
self.k_listener = keyboard.Listener(on_press=self.on_press)
|
||||
self.k_listener.start()
|
||||
self.callback()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
from Qt import QtCore, QtGui, QtWidgets
|
||||
from avalon import style
|
||||
from pype.api import resources
|
||||
|
||||
|
||||
class MusterLogin(QtWidgets.QWidget):
|
||||
|
|
@ -23,10 +24,7 @@ class MusterLogin(QtWidgets.QWidget):
|
|||
elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'):
|
||||
self.setWindowIcon(parent.parent.icon)
|
||||
else:
|
||||
pype_setup = os.getenv('PYPE_SETUP_PATH')
|
||||
items = [pype_setup, "app", "resources", "icon.png"]
|
||||
fname = os.path.sep.join(items)
|
||||
icon = QtGui.QIcon(fname)
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.setWindowFlags(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ from .rest_api import RestApiServer
|
|||
from .base_class import RestApi, abort, route, register_statics
|
||||
from .lib import RestMethods, CallbackResult
|
||||
|
||||
CLASS_DEFINIION = RestApiServer
|
||||
|
||||
|
||||
def tray_init(tray_widget, main_widget):
|
||||
return RestApiServer()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from socketserver import ThreadingMixIn
|
|||
from http.server import HTTPServer
|
||||
from .lib import RestApiFactory, Handler
|
||||
from .base_class import route, register_statics
|
||||
from pype.api import config, Logger
|
||||
from pype.api import Logger
|
||||
|
||||
log = Logger().get_logger("RestApiServer")
|
||||
|
||||
|
|
@ -85,24 +85,22 @@ class RestApiServer:
|
|||
Callback may return many types. For more information read docstring of
|
||||
`_handle_callback_result` defined in handler.
|
||||
"""
|
||||
default_port = 8011
|
||||
exclude_ports = []
|
||||
|
||||
def __init__(self):
|
||||
self.qaction = None
|
||||
self.failed_icon = None
|
||||
self._is_running = False
|
||||
|
||||
try:
|
||||
self.presets = config.get_presets()["services"]["rest_api"]
|
||||
except Exception:
|
||||
self.presets = {"default_port": 8011, "exclude_ports": []}
|
||||
log.debug((
|
||||
"There are not set presets for RestApiModule."
|
||||
" Using defaults \"{}\""
|
||||
).format(str(self.presets)))
|
||||
|
||||
port = self.find_port()
|
||||
self.rest_api_thread = RestApiThread(self, port)
|
||||
|
||||
statics_dir = os.path.sep.join([os.environ["PYPE_MODULE_ROOT"], "res"])
|
||||
statics_dir = os.path.join(
|
||||
os.environ["PYPE_MODULE_ROOT"],
|
||||
"pype",
|
||||
"resources"
|
||||
)
|
||||
self.register_statics("/res", statics_dir)
|
||||
os.environ["PYPE_STATICS_SERVER"] = "{}/res".format(
|
||||
os.environ["PYPE_REST_API_URL"]
|
||||
|
|
@ -126,8 +124,8 @@ class RestApiServer:
|
|||
RestApiFactory.register_obj(obj)
|
||||
|
||||
def find_port(self):
|
||||
start_port = self.presets["default_port"]
|
||||
exclude_ports = self.presets["exclude_ports"]
|
||||
start_port = self.default_port
|
||||
exclude_ports = self.exclude_ports
|
||||
found_port = None
|
||||
# port check takes time so it's lowered to 100 ports
|
||||
for port in range(start_port, start_port+100):
|
||||
|
|
|
|||
|
|
@ -10,10 +10,37 @@ from . import DropEmpty, ComponentsList, ComponentItem
|
|||
|
||||
|
||||
class DropDataFrame(QtWidgets.QFrame):
|
||||
image_extensions = [
|
||||
".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave", ".cal",
|
||||
".cin", ".cpc", ".cpt", ".dds", ".dpx", ".ecw", ".exr", ".fits",
|
||||
".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc", ".icer",
|
||||
".icns", ".ico", ".cur", ".ics", ".ilbm", ".jbig", ".jbig2",
|
||||
".jng", ".jpeg", ".jpeg-ls", ".jpeg", ".2000", ".jpg", ".xr",
|
||||
".jpeg", ".xt", ".jpeg-hdr", ".kra", ".mng", ".miff", ".nrrd",
|
||||
".ora", ".pam", ".pbm", ".pgm", ".ppm", ".pnm", ".pcx", ".pgf",
|
||||
".pictor", ".png", ".psd", ".psb", ".psp", ".qtvr", ".ras",
|
||||
".rgbe", ".logluv", ".tiff", ".sgi", ".tga", ".tiff", ".tiff/ep",
|
||||
".tiff/it", ".ufo", ".ufp", ".wbmp", ".webp", ".xbm", ".xcf",
|
||||
".xpm", ".xwd"
|
||||
]
|
||||
video_extensions = [
|
||||
".3g2", ".3gp", ".amv", ".asf", ".avi", ".drc", ".f4a", ".f4b",
|
||||
".f4p", ".f4v", ".flv", ".gif", ".gifv", ".m2v", ".m4p", ".m4v",
|
||||
".mkv", ".mng", ".mov", ".mp2", ".mp4", ".mpe", ".mpeg", ".mpg",
|
||||
".mpv", ".mxf", ".nsv", ".ogg", ".ogv", ".qt", ".rm", ".rmvb",
|
||||
".roq", ".svi", ".vob", ".webm", ".wmv", ".yuv"
|
||||
]
|
||||
extensions = {
|
||||
"nuke": [".nk"],
|
||||
"maya": [".ma", ".mb"],
|
||||
"houdini": [".hip"],
|
||||
"image_file": image_extensions,
|
||||
"video_file": video_extensions
|
||||
}
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__()
|
||||
self.parent_widget = parent
|
||||
self.presets = config.get_presets()['standalone_publish']
|
||||
|
||||
self.setAcceptDrops(True)
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
|
|
@ -26,7 +53,9 @@ class DropDataFrame(QtWidgets.QFrame):
|
|||
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.drop_widget.sizePolicy().hasHeightForWidth())
|
||||
sizePolicy.setHeightForWidth(
|
||||
self.drop_widget.sizePolicy().hasHeightForWidth()
|
||||
)
|
||||
self.drop_widget.setSizePolicy(sizePolicy)
|
||||
|
||||
layout.addWidget(self.drop_widget)
|
||||
|
|
@ -255,8 +284,8 @@ class DropDataFrame(QtWidgets.QFrame):
|
|||
file_info = data['file_info']
|
||||
|
||||
if (
|
||||
ext in self.presets['extensions']['image_file'] or
|
||||
ext in self.presets['extensions']['video_file']
|
||||
ext in self.image_extensions
|
||||
or ext in self.video_extensions
|
||||
):
|
||||
probe_data = self.load_data_with_probe(filepath)
|
||||
if 'fps' not in data:
|
||||
|
|
@ -293,7 +322,7 @@ class DropDataFrame(QtWidgets.QFrame):
|
|||
data[key] = value
|
||||
|
||||
icon = 'default'
|
||||
for ico, exts in self.presets['extensions'].items():
|
||||
for ico, exts in self.extensions.items():
|
||||
if ext in exts:
|
||||
icon = ico
|
||||
break
|
||||
|
|
@ -304,17 +333,16 @@ class DropDataFrame(QtWidgets.QFrame):
|
|||
icon += 's'
|
||||
data['icon'] = icon
|
||||
data['thumb'] = (
|
||||
ext in self.presets['extensions']['image_file'] or
|
||||
ext in self.presets['extensions']['video_file']
|
||||
ext in self.image_extensions
|
||||
or ext in self.video_extensions
|
||||
)
|
||||
data['prev'] = (
|
||||
ext in self.presets['extensions']['video_file'] or
|
||||
(new_is_seq and ext in self.presets['extensions']['image_file'])
|
||||
ext in self.video_extensions
|
||||
or (new_is_seq and ext in self.image_extensions)
|
||||
)
|
||||
|
||||
actions = []
|
||||
|
||||
|
||||
found = False
|
||||
for item in self.components_list.widgets():
|
||||
if data['ext'] != item.in_data['ext']:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from .timers_manager import TimersManager
|
||||
from .widget_user_idle import WidgetUserIdle
|
||||
|
||||
CLASS_DEFINIION = TimersManager
|
||||
|
||||
|
||||
def tray_init(tray_widget, main_widget):
|
||||
return TimersManager(tray_widget, main_widget)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from Qt import QtCore
|
||||
from .widget_user_idle import WidgetUserIdle
|
||||
from .widget_user_idle import WidgetUserIdle, SignalHandler
|
||||
from pype.api import Logger, config
|
||||
|
||||
|
||||
|
|
@ -23,32 +22,36 @@ class TimersManager(metaclass=Singleton):
|
|||
If IdleManager is imported then is able to handle about stop timers
|
||||
when user idles for a long time (set in presets).
|
||||
"""
|
||||
modules = []
|
||||
is_running = False
|
||||
last_task = None
|
||||
|
||||
# Presetable attributes
|
||||
# - when timer will stop if idle manager is running (minutes)
|
||||
full_time = 15
|
||||
# - how many minutes before the timer is stopped will popup the message
|
||||
message_time = 0.5
|
||||
|
||||
def __init__(self, tray_widget, main_widget):
|
||||
self.log = Logger().get_logger(self.__class__.__name__)
|
||||
|
||||
self.modules = []
|
||||
self.is_running = False
|
||||
self.last_task = None
|
||||
|
||||
self.tray_widget = tray_widget
|
||||
self.main_widget = main_widget
|
||||
self.widget_user_idle = WidgetUserIdle(self)
|
||||
|
||||
self.idle_man = None
|
||||
self.signal_handler = None
|
||||
self.widget_user_idle = WidgetUserIdle(self, tray_widget)
|
||||
|
||||
def set_signal_times(self):
|
||||
try:
|
||||
timer_info = (
|
||||
config.get_presets()
|
||||
.get('services')
|
||||
.get('timers_manager')
|
||||
.get('timer')
|
||||
)
|
||||
full_time = int(float(timer_info['full_time'])*60)
|
||||
message_time = int(float(timer_info['message_time'])*60)
|
||||
full_time = int(self.full_time * 60)
|
||||
message_time = int(self.message_time * 60)
|
||||
self.time_show_message = full_time - message_time
|
||||
self.time_stop_timer = full_time
|
||||
return True
|
||||
except Exception:
|
||||
self.log.warning('Was not able to load presets for TimersManager')
|
||||
return False
|
||||
self.log.error("Couldn't set timer signals.", exc_info=True)
|
||||
|
||||
def add_module(self, module):
|
||||
""" Adds module to context
|
||||
|
|
@ -114,49 +117,59 @@ class TimersManager(metaclass=Singleton):
|
|||
:param modules: All imported modules from TrayManager
|
||||
:type modules: dict
|
||||
"""
|
||||
self.s_handler = SignalHandler(self)
|
||||
|
||||
if 'IdleManager' in modules:
|
||||
self.signal_handler = SignalHandler(self)
|
||||
if self.set_signal_times() is True:
|
||||
self.register_to_idle_manager(modules['IdleManager'])
|
||||
|
||||
def time_callback(self, int_def):
|
||||
if not self.signal_handler:
|
||||
return
|
||||
|
||||
if int_def == 0:
|
||||
self.signal_handler.signal_show_message.emit()
|
||||
elif int_def == 1:
|
||||
self.signal_handler.signal_change_label.emit()
|
||||
elif int_def == 2:
|
||||
self.signal_handler.signal_stop_timers.emit()
|
||||
|
||||
def register_to_idle_manager(self, man_obj):
|
||||
self.idle_man = man_obj
|
||||
|
||||
# Time when message is shown
|
||||
self.idle_man.add_time_callback(
|
||||
self.time_show_message,
|
||||
lambda: self.time_callback(0)
|
||||
)
|
||||
|
||||
# Times when idle is between show widget and stop timers
|
||||
show_to_stop_range = range(
|
||||
self.time_show_message-1, self.time_stop_timer
|
||||
self.time_show_message - 1, self.time_stop_timer
|
||||
)
|
||||
for num in show_to_stop_range:
|
||||
self.idle_man.add_time_signal(
|
||||
num,
|
||||
self.s_handler.signal_change_label
|
||||
self.idle_man.add_time_callback(
|
||||
num, lambda: self.time_callback(1)
|
||||
)
|
||||
# Times when widget is already shown and user restart idle
|
||||
shown_and_moved_range = range(
|
||||
self.time_stop_timer - self.time_show_message
|
||||
)
|
||||
for num in shown_and_moved_range:
|
||||
self.idle_man.add_time_signal(
|
||||
num,
|
||||
self.s_handler.signal_change_label
|
||||
self.idle_man.add_time_callback(
|
||||
num, lambda: self.time_callback(1)
|
||||
)
|
||||
# Time when message is shown
|
||||
self.idle_man.add_time_signal(
|
||||
self.time_show_message,
|
||||
self.s_handler.signal_show_message
|
||||
)
|
||||
|
||||
# Time when timers are stopped
|
||||
self.idle_man.add_time_signal(
|
||||
self.idle_man.add_time_callback(
|
||||
self.time_stop_timer,
|
||||
self.s_handler.signal_stop_timers
|
||||
lambda: self.time_callback(2)
|
||||
)
|
||||
|
||||
def change_label(self):
|
||||
if self.is_running is False:
|
||||
return
|
||||
if self.widget_user_idle.bool_is_showed is False:
|
||||
return
|
||||
if not hasattr(self, 'idle_man'):
|
||||
if not self.idle_man or self.widget_user_idle.bool_is_showed is False:
|
||||
return
|
||||
|
||||
if self.idle_man.idle_time > self.time_show_message:
|
||||
|
|
@ -174,14 +187,3 @@ class TimersManager(metaclass=Singleton):
|
|||
return
|
||||
if self.widget_user_idle.bool_is_showed is False:
|
||||
self.widget_user_idle.show()
|
||||
|
||||
|
||||
class SignalHandler(QtCore.QObject):
|
||||
signal_show_message = QtCore.Signal()
|
||||
signal_change_label = QtCore.Signal()
|
||||
signal_stop_timers = QtCore.Signal()
|
||||
def __init__(self, cls):
|
||||
super().__init__()
|
||||
self.signal_show_message.connect(cls.show_message)
|
||||
self.signal_change_label.connect(cls.change_label)
|
||||
self.signal_stop_timers.connect(cls.stop_timers)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
from pype.api import Logger
|
||||
from avalon import style
|
||||
from Qt import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
|
@ -8,18 +7,18 @@ class WidgetUserIdle(QtWidgets.QWidget):
|
|||
SIZE_W = 300
|
||||
SIZE_H = 160
|
||||
|
||||
def __init__(self, parent):
|
||||
def __init__(self, module, tray_widget):
|
||||
|
||||
super(WidgetUserIdle, self).__init__()
|
||||
|
||||
self.bool_is_showed = False
|
||||
self.bool_not_stopped = True
|
||||
|
||||
self.parent_widget = parent
|
||||
self.setWindowIcon(parent.tray_widget.icon)
|
||||
self.module = module
|
||||
self.setWindowIcon(tray_widget.icon)
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowCloseButtonHint |
|
||||
QtCore.Qt.WindowMinimizeButtonHint
|
||||
QtCore.Qt.WindowCloseButtonHint
|
||||
| QtCore.Qt.WindowMinimizeButtonHint
|
||||
)
|
||||
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
|
@ -129,11 +128,11 @@ class WidgetUserIdle(QtWidgets.QWidget):
|
|||
self.lbl_rest_time.setText(str_time)
|
||||
|
||||
def stop_timer(self):
|
||||
self.parent_widget.stop_timers()
|
||||
self.module.stop_timers()
|
||||
self.close_widget()
|
||||
|
||||
def restart_timer(self):
|
||||
self.parent_widget.restart_timers()
|
||||
self.module.restart_timers()
|
||||
self.close_widget()
|
||||
|
||||
def continue_timer(self):
|
||||
|
|
@ -154,3 +153,15 @@ class WidgetUserIdle(QtWidgets.QWidget):
|
|||
|
||||
def showEvent(self, event):
|
||||
self.bool_is_showed = True
|
||||
|
||||
|
||||
class SignalHandler(QtCore.QObject):
|
||||
signal_show_message = QtCore.Signal()
|
||||
signal_change_label = QtCore.Signal()
|
||||
signal_stop_timers = QtCore.Signal()
|
||||
|
||||
def __init__(self, cls):
|
||||
super().__init__()
|
||||
self.signal_show_message.connect(cls.show_message)
|
||||
self.signal_change_label.connect(cls.change_label)
|
||||
self.signal_stop_timers.connect(cls.stop_timers)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from Qt import QtCore, QtGui, QtWidgets
|
||||
from pype.resources import get_resource
|
||||
from avalon import style
|
||||
from pype.api import resources
|
||||
|
||||
|
||||
class UserWidget(QtWidgets.QWidget):
|
||||
|
|
@ -14,7 +14,7 @@ class UserWidget(QtWidgets.QWidget):
|
|||
self.module = module
|
||||
|
||||
# Style
|
||||
icon = QtGui.QIcon(get_resource("icon.png"))
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
self.setWindowTitle("Username Settings")
|
||||
self.setMinimumWidth(self.MIN_WIDTH)
|
||||
|
|
|
|||
32
pype/plugins/blender/create/create_camera.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
"""Create a camera asset."""
|
||||
|
||||
import bpy
|
||||
|
||||
from avalon import api
|
||||
from avalon.blender import Creator, lib
|
||||
import pype.hosts.blender.plugin
|
||||
|
||||
|
||||
class CreateCamera(Creator):
|
||||
"""Polygonal static geometry"""
|
||||
|
||||
name = "cameraMain"
|
||||
label = "Camera"
|
||||
family = "camera"
|
||||
icon = "video-camera"
|
||||
|
||||
def process(self):
|
||||
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = pype.hosts.blender.plugin.asset_name(asset, subset)
|
||||
collection = bpy.data.collections.new(name=name)
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
self.data['task'] = api.Session.get('AVALON_TASK')
|
||||
lib.imprint(collection, self.data)
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
for obj in lib.get_selection():
|
||||
collection.objects.link(obj)
|
||||
|
||||
return collection
|
||||
|
|
@ -174,22 +174,16 @@ class BlendActionLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
|
||||
strips = []
|
||||
|
||||
for obj in collection_metadata["objects"]:
|
||||
|
||||
for obj in list(collection_metadata["objects"]):
|
||||
# Get all the strips that use the action
|
||||
arm_objs = [
|
||||
arm for arm in bpy.data.objects if arm.type == 'ARMATURE']
|
||||
|
||||
for armature_obj in arm_objs:
|
||||
|
||||
if armature_obj.animation_data is not None:
|
||||
|
||||
for track in armature_obj.animation_data.nla_tracks:
|
||||
|
||||
for strip in track.strips:
|
||||
|
||||
if strip.action == obj.animation_data.action:
|
||||
|
||||
strips.append(strip)
|
||||
|
||||
bpy.data.actions.remove(obj.animation_data.action)
|
||||
|
|
@ -277,22 +271,16 @@ class BlendActionLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
objects = collection_metadata["objects"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
|
||||
for obj in objects:
|
||||
|
||||
for obj in list(objects):
|
||||
# Get all the strips that use the action
|
||||
arm_objs = [
|
||||
arm for arm in bpy.data.objects if arm.type == 'ARMATURE']
|
||||
|
||||
for armature_obj in arm_objs:
|
||||
|
||||
if armature_obj.animation_data is not None:
|
||||
|
||||
for track in armature_obj.animation_data.nla_tracks:
|
||||
|
||||
for strip in track.strips:
|
||||
|
||||
if strip.action == obj.animation_data.action:
|
||||
|
||||
track.strips.remove(strip)
|
||||
|
||||
bpy.data.actions.remove(obj.animation_data.action)
|
||||
|
|
|
|||
|
|
@ -30,9 +30,7 @@ class BlendAnimationLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
color = "orange"
|
||||
|
||||
def _remove(self, objects, lib_container):
|
||||
|
||||
for obj in objects:
|
||||
|
||||
for obj in list(objects):
|
||||
if obj.type == 'ARMATURE':
|
||||
bpy.data.armatures.remove(obj.data)
|
||||
elif obj.type == 'MESH':
|
||||
|
|
|
|||
247
pype/plugins/blender/load/load_camera.py
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
"""Load a camera asset in Blender."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from avalon import api, blender
|
||||
import bpy
|
||||
import pype.hosts.blender.plugin
|
||||
|
||||
logger = logging.getLogger("pype").getChild("blender").getChild("load_camera")
|
||||
|
||||
|
||||
class BlendCameraLoader(pype.hosts.blender.plugin.AssetLoader):
|
||||
"""Load a camera from a .blend file.
|
||||
|
||||
Warning:
|
||||
Loading the same asset more then once is not properly supported at the
|
||||
moment.
|
||||
"""
|
||||
|
||||
families = ["camera"]
|
||||
representations = ["blend"]
|
||||
|
||||
label = "Link Camera"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def _remove(self, objects, lib_container):
|
||||
for obj in list(objects):
|
||||
bpy.data.cameras.remove(obj.data)
|
||||
|
||||
bpy.data.collections.remove(bpy.data.collections[lib_container])
|
||||
|
||||
def _process(self, libpath, lib_container, container_name, actions):
|
||||
|
||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||
with bpy.data.libraries.load(
|
||||
libpath, link=True, relative=relative
|
||||
) as (_, data_to):
|
||||
data_to.collections = [lib_container]
|
||||
|
||||
scene = bpy.context.scene
|
||||
|
||||
scene.collection.children.link(bpy.data.collections[lib_container])
|
||||
|
||||
camera_container = scene.collection.children[lib_container].make_local()
|
||||
|
||||
objects_list = []
|
||||
|
||||
for obj in camera_container.objects:
|
||||
local_obj = obj.make_local()
|
||||
local_obj.data.make_local()
|
||||
|
||||
if not local_obj.get(blender.pipeline.AVALON_PROPERTY):
|
||||
local_obj[blender.pipeline.AVALON_PROPERTY] = dict()
|
||||
|
||||
avalon_info = local_obj[blender.pipeline.AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": container_name})
|
||||
|
||||
if actions[0] is not None:
|
||||
if local_obj.animation_data is None:
|
||||
local_obj.animation_data_create()
|
||||
local_obj.animation_data.action = actions[0]
|
||||
|
||||
if actions[1] is not None:
|
||||
if local_obj.data.animation_data is None:
|
||||
local_obj.data.animation_data_create()
|
||||
local_obj.data.animation_data.action = actions[1]
|
||||
|
||||
objects_list.append(local_obj)
|
||||
|
||||
camera_container.pop(blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
return objects_list
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
options: Optional[Dict] = None
|
||||
) -> Optional[List]:
|
||||
"""
|
||||
Arguments:
|
||||
name: Use pre-defined name
|
||||
namespace: Use pre-defined namespace
|
||||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
|
||||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
lib_container = pype.hosts.blender.plugin.asset_name(asset, subset)
|
||||
container_name = pype.hosts.blender.plugin.asset_name(
|
||||
asset, subset, namespace
|
||||
)
|
||||
|
||||
container = bpy.data.collections.new(lib_container)
|
||||
container.name = container_name
|
||||
blender.pipeline.containerise_existing(
|
||||
container,
|
||||
name,
|
||||
namespace,
|
||||
context,
|
||||
self.__class__.__name__,
|
||||
)
|
||||
|
||||
container_metadata = container.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
container_metadata["libpath"] = libpath
|
||||
container_metadata["lib_container"] = lib_container
|
||||
|
||||
objects_list = self._process(
|
||||
libpath, lib_container, container_name, (None, None))
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
container_metadata["objects"] = objects_list
|
||||
|
||||
nodes = list(container.objects)
|
||||
nodes.append(container)
|
||||
self[:] = nodes
|
||||
return nodes
|
||||
|
||||
def update(self, container: Dict, representation: Dict):
|
||||
"""Update the loaded asset.
|
||||
|
||||
This will remove all objects of the current collection, load the new
|
||||
ones and add them to the collection.
|
||||
If the objects of the collection are used in another collection they
|
||||
will not be removed, only unlinked. Normally this should not be the
|
||||
case though.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
|
||||
collection = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
|
||||
libpath = Path(api.get_representation_path(representation))
|
||||
extension = libpath.suffix.lower()
|
||||
|
||||
logger.info(
|
||||
"Container: %s\nRepresentation: %s",
|
||||
pformat(container, indent=2),
|
||||
pformat(representation, indent=2),
|
||||
)
|
||||
|
||||
assert collection, (
|
||||
f"The asset is not loaded: {container['objectName']}"
|
||||
)
|
||||
assert not (collection.children), (
|
||||
"Nested collections are not supported."
|
||||
)
|
||||
assert libpath, (
|
||||
"No existing library file found for {container['objectName']}"
|
||||
)
|
||||
assert libpath.is_file(), (
|
||||
f"The file doesn't exist: {libpath}"
|
||||
)
|
||||
assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, (
|
||||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
collection_libpath = collection_metadata["libpath"]
|
||||
objects = collection_metadata["objects"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
|
||||
normalized_collection_libpath = (
|
||||
str(Path(bpy.path.abspath(collection_libpath)).resolve())
|
||||
)
|
||||
normalized_libpath = (
|
||||
str(Path(bpy.path.abspath(str(libpath))).resolve())
|
||||
)
|
||||
logger.debug(
|
||||
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_collection_libpath,
|
||||
normalized_libpath,
|
||||
)
|
||||
if normalized_collection_libpath == normalized_libpath:
|
||||
logger.info("Library already loaded, not updating...")
|
||||
return
|
||||
|
||||
camera = objects[0]
|
||||
|
||||
camera_action = None
|
||||
camera_data_action = None
|
||||
|
||||
if camera.animation_data and camera.animation_data.action:
|
||||
camera_action = camera.animation_data.action
|
||||
|
||||
if camera.data.animation_data and camera.data.animation_data.action:
|
||||
camera_data_action = camera.data.animation_data.action
|
||||
|
||||
actions = (camera_action, camera_data_action)
|
||||
|
||||
self._remove(objects, lib_container)
|
||||
|
||||
objects_list = self._process(
|
||||
str(libpath), lib_container, collection.name, actions)
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
collection_metadata["objects"] = objects_list
|
||||
collection_metadata["libpath"] = str(libpath)
|
||||
collection_metadata["representation"] = str(representation["_id"])
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
def remove(self, container: Dict) -> bool:
|
||||
"""Remove an existing container from a Blender scene.
|
||||
|
||||
Arguments:
|
||||
container (avalon-core:container-1.0): Container to remove,
|
||||
from `host.ls()`.
|
||||
|
||||
Returns:
|
||||
bool: Whether the container was deleted.
|
||||
|
||||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
|
||||
collection = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
if not collection:
|
||||
return False
|
||||
assert not (collection.children), (
|
||||
"Nested collections are not supported."
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
objects = collection_metadata["objects"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
|
||||
self._remove(objects, lib_container)
|
||||
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
return True
|
||||
|
|
@ -7,20 +7,11 @@ from typing import Dict, List, Optional
|
|||
|
||||
from avalon import api, blender
|
||||
import bpy
|
||||
import pype.hosts.blender.plugin
|
||||
import pype.hosts.blender.plugin as plugin
|
||||
|
||||
|
||||
logger = logging.getLogger("pype").getChild(
|
||||
"blender").getChild("load_layout")
|
||||
|
||||
|
||||
class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
|
||||
"""Load animations from a .blend file.
|
||||
|
||||
Warning:
|
||||
Loading the same asset more then once is not properly supported at the
|
||||
moment.
|
||||
"""
|
||||
class BlendLayoutLoader(plugin.AssetLoader):
|
||||
"""Load layout from a .blend file."""
|
||||
|
||||
families = ["layout"]
|
||||
representations = ["blend"]
|
||||
|
|
@ -29,24 +20,25 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def _remove(self, objects, lib_container):
|
||||
|
||||
for obj in objects:
|
||||
|
||||
def _remove(self, objects, obj_container):
|
||||
for obj in list(objects):
|
||||
if obj.type == 'ARMATURE':
|
||||
bpy.data.armatures.remove(obj.data)
|
||||
elif obj.type == 'MESH':
|
||||
bpy.data.meshes.remove(obj.data)
|
||||
elif obj.type == 'CAMERA':
|
||||
bpy.data.cameras.remove(obj.data)
|
||||
elif obj.type == 'CURVE':
|
||||
bpy.data.curves.remove(obj.data)
|
||||
|
||||
for element_container in bpy.data.collections[lib_container].children:
|
||||
for element_container in obj_container.children:
|
||||
for child in element_container.children:
|
||||
bpy.data.collections.remove(child)
|
||||
bpy.data.collections.remove(element_container)
|
||||
|
||||
bpy.data.collections.remove(bpy.data.collections[lib_container])
|
||||
bpy.data.collections.remove(obj_container)
|
||||
|
||||
def _process(self, libpath, lib_container, container_name, actions):
|
||||
|
||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||
with bpy.data.libraries.load(
|
||||
libpath, link=True, relative=relative
|
||||
|
|
@ -58,45 +50,55 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
scene.collection.children.link(bpy.data.collections[lib_container])
|
||||
|
||||
layout_container = scene.collection.children[lib_container].make_local()
|
||||
layout_container.name = container_name
|
||||
|
||||
meshes = []
|
||||
objects_local_types = ['MESH', 'CAMERA', 'CURVE']
|
||||
|
||||
objects = []
|
||||
armatures = []
|
||||
|
||||
objects_list = []
|
||||
containers = list(layout_container.children)
|
||||
|
||||
for element_container in layout_container.children:
|
||||
element_container.make_local()
|
||||
meshes.extend([obj for obj in element_container.objects if obj.type == 'MESH'])
|
||||
armatures.extend([obj for obj in element_container.objects if obj.type == 'ARMATURE'])
|
||||
for child in element_container.children:
|
||||
child.make_local()
|
||||
meshes.extend(child.objects)
|
||||
for container in layout_container.children:
|
||||
if container.name == blender.pipeline.AVALON_CONTAINERS:
|
||||
containers.remove(container)
|
||||
|
||||
for container in containers:
|
||||
container.make_local()
|
||||
objects.extend([
|
||||
obj for obj in container.objects
|
||||
if obj.type in objects_local_types
|
||||
])
|
||||
armatures.extend([
|
||||
obj for obj in container.objects
|
||||
if obj.type == 'ARMATURE'
|
||||
])
|
||||
containers.extend(list(container.children))
|
||||
|
||||
# Link meshes first, then armatures.
|
||||
# The armature is unparented for all the non-local meshes,
|
||||
# when it is made local.
|
||||
for obj in meshes + armatures:
|
||||
obj = obj.make_local()
|
||||
obj.data.make_local()
|
||||
for obj in objects + armatures:
|
||||
local_obj = obj.make_local()
|
||||
if obj.data:
|
||||
obj.data.make_local()
|
||||
|
||||
if not obj.get(blender.pipeline.AVALON_PROPERTY):
|
||||
obj[blender.pipeline.AVALON_PROPERTY] = dict()
|
||||
if not local_obj.get(blender.pipeline.AVALON_PROPERTY):
|
||||
local_obj[blender.pipeline.AVALON_PROPERTY] = dict()
|
||||
|
||||
avalon_info = obj[blender.pipeline.AVALON_PROPERTY]
|
||||
avalon_info = local_obj[blender.pipeline.AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": container_name})
|
||||
|
||||
action = actions.get( obj.name, None )
|
||||
action = actions.get(local_obj.name, None)
|
||||
|
||||
if obj.type == 'ARMATURE' and action is not None:
|
||||
obj.animation_data.action = action
|
||||
|
||||
objects_list.append(obj)
|
||||
if local_obj.type == 'ARMATURE' and action is not None:
|
||||
local_obj.animation_data.action = action
|
||||
|
||||
layout_container.pop(blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
return objects_list
|
||||
return layout_container
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
|
|
@ -113,9 +115,15 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
lib_container = pype.hosts.blender.plugin.asset_name(asset, subset)
|
||||
container_name = pype.hosts.blender.plugin.asset_name(
|
||||
asset, subset, namespace
|
||||
lib_container = plugin.asset_name(
|
||||
asset, subset
|
||||
)
|
||||
unique_number = plugin.get_unique_number(
|
||||
asset, subset
|
||||
)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
container_name = plugin.asset_name(
|
||||
asset, subset, unique_number
|
||||
)
|
||||
|
||||
container = bpy.data.collections.new(lib_container)
|
||||
|
|
@ -134,11 +142,13 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
container_metadata["libpath"] = libpath
|
||||
container_metadata["lib_container"] = lib_container
|
||||
|
||||
objects_list = self._process(
|
||||
obj_container = self._process(
|
||||
libpath, lib_container, container_name, {})
|
||||
|
||||
container_metadata["obj_container"] = obj_container
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
container_metadata["objects"] = objects_list
|
||||
container_metadata["objects"] = obj_container.all_objects
|
||||
|
||||
nodes = list(container.objects)
|
||||
nodes.append(container)
|
||||
|
|
@ -157,7 +167,6 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
|
||||
collection = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
|
|
@ -165,7 +174,7 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
libpath = Path(api.get_representation_path(representation))
|
||||
extension = libpath.suffix.lower()
|
||||
|
||||
logger.info(
|
||||
self.log.info(
|
||||
"Container: %s\nRepresentation: %s",
|
||||
pformat(container, indent=2),
|
||||
pformat(representation, indent=2),
|
||||
|
|
@ -189,41 +198,41 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
collection_libpath = collection_metadata["libpath"]
|
||||
objects = collection_metadata["objects"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
obj_container = collection_metadata["obj_container"]
|
||||
|
||||
normalized_collection_libpath = (
|
||||
str(Path(bpy.path.abspath(collection_libpath)).resolve())
|
||||
)
|
||||
normalized_libpath = (
|
||||
str(Path(bpy.path.abspath(str(libpath))).resolve())
|
||||
)
|
||||
logger.debug(
|
||||
self.log.debug(
|
||||
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_collection_libpath,
|
||||
normalized_libpath,
|
||||
)
|
||||
if normalized_collection_libpath == normalized_libpath:
|
||||
logger.info("Library already loaded, not updating...")
|
||||
self.log.info("Library already loaded, not updating...")
|
||||
return
|
||||
|
||||
objects = collection_metadata["objects"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
|
||||
actions = {}
|
||||
|
||||
for obj in objects:
|
||||
|
||||
if obj.type == 'ARMATURE':
|
||||
if obj.animation_data and obj.animation_data.action:
|
||||
actions[obj.name] = obj.animation_data.action
|
||||
|
||||
actions[obj.name] = obj.animation_data.action
|
||||
self._remove(objects, obj_container)
|
||||
|
||||
self._remove(objects, lib_container)
|
||||
|
||||
objects_list = self._process(
|
||||
obj_container = self._process(
|
||||
str(libpath), lib_container, collection.name, actions)
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
collection_metadata["objects"] = objects_list
|
||||
collection_metadata["obj_container"] = obj_container
|
||||
collection_metadata["objects"] = obj_container.all_objects
|
||||
collection_metadata["libpath"] = str(libpath)
|
||||
collection_metadata["representation"] = str(representation["_id"])
|
||||
|
||||
|
|
@ -255,9 +264,9 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
objects = collection_metadata["objects"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
obj_container = collection_metadata["obj_container"]
|
||||
|
||||
self._remove(objects, lib_container)
|
||||
self._remove(objects, obj_container)
|
||||
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,20 +7,14 @@ from typing import Dict, List, Optional
|
|||
|
||||
from avalon import api, blender
|
||||
import bpy
|
||||
import pype.hosts.blender.plugin
|
||||
|
||||
logger = logging.getLogger("pype").getChild("blender").getChild("load_model")
|
||||
import pype.hosts.blender.plugin as plugin
|
||||
|
||||
|
||||
class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader):
|
||||
class BlendModelLoader(plugin.AssetLoader):
|
||||
"""Load models from a .blend file.
|
||||
|
||||
Because they come from a .blend file we can simply link the collection that
|
||||
contains the model. There is no further need to 'containerise' it.
|
||||
|
||||
Warning:
|
||||
Loading the same asset more then once is not properly supported at the
|
||||
moment.
|
||||
"""
|
||||
|
||||
families = ["model"]
|
||||
|
|
@ -30,54 +24,52 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def _remove(self, objects, lib_container):
|
||||
|
||||
for obj in objects:
|
||||
|
||||
def _remove(self, objects, container):
|
||||
for obj in list(objects):
|
||||
for material_slot in list(obj.material_slots):
|
||||
bpy.data.materials.remove(material_slot.material)
|
||||
bpy.data.meshes.remove(obj.data)
|
||||
|
||||
bpy.data.collections.remove(bpy.data.collections[lib_container])
|
||||
|
||||
def _process(self, libpath, lib_container, container_name):
|
||||
bpy.data.collections.remove(container)
|
||||
|
||||
def _process(
|
||||
self, libpath, lib_container, container_name,
|
||||
parent_collection
|
||||
):
|
||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||
with bpy.data.libraries.load(
|
||||
libpath, link=True, relative=relative
|
||||
) as (_, data_to):
|
||||
data_to.collections = [lib_container]
|
||||
|
||||
scene = bpy.context.scene
|
||||
parent = parent_collection
|
||||
|
||||
scene.collection.children.link(bpy.data.collections[lib_container])
|
||||
if parent is None:
|
||||
parent = bpy.context.scene.collection
|
||||
|
||||
model_container = scene.collection.children[lib_container].make_local()
|
||||
parent.children.link(bpy.data.collections[lib_container])
|
||||
|
||||
objects_list = []
|
||||
model_container = parent.children[lib_container].make_local()
|
||||
model_container.name = container_name
|
||||
|
||||
for obj in model_container.objects:
|
||||
local_obj = plugin.prepare_data(obj, container_name)
|
||||
plugin.prepare_data(local_obj.data, container_name)
|
||||
|
||||
obj = obj.make_local()
|
||||
|
||||
obj.data.make_local()
|
||||
|
||||
for material_slot in obj.material_slots:
|
||||
|
||||
material_slot.material.make_local()
|
||||
for material_slot in local_obj.material_slots:
|
||||
plugin.prepare_data(material_slot.material, container_name)
|
||||
|
||||
if not obj.get(blender.pipeline.AVALON_PROPERTY):
|
||||
local_obj[blender.pipeline.AVALON_PROPERTY] = dict()
|
||||
|
||||
obj[blender.pipeline.AVALON_PROPERTY] = dict()
|
||||
|
||||
avalon_info = obj[blender.pipeline.AVALON_PROPERTY]
|
||||
avalon_info = local_obj[blender.pipeline.AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": container_name})
|
||||
|
||||
objects_list.append(obj)
|
||||
|
||||
model_container.pop(blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
return objects_list
|
||||
return model_container
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
|
|
@ -94,35 +86,44 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
lib_container = pype.hosts.blender.plugin.asset_name(asset, subset)
|
||||
container_name = pype.hosts.blender.plugin.asset_name(
|
||||
asset, subset, namespace
|
||||
|
||||
lib_container = plugin.asset_name(
|
||||
asset, subset
|
||||
)
|
||||
unique_number = plugin.get_unique_number(
|
||||
asset, subset
|
||||
)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
container_name = plugin.asset_name(
|
||||
asset, subset, unique_number
|
||||
)
|
||||
|
||||
collection = bpy.data.collections.new(lib_container)
|
||||
collection.name = container_name
|
||||
container = bpy.data.collections.new(lib_container)
|
||||
container.name = container_name
|
||||
blender.pipeline.containerise_existing(
|
||||
collection,
|
||||
container,
|
||||
name,
|
||||
namespace,
|
||||
context,
|
||||
self.__class__.__name__,
|
||||
)
|
||||
|
||||
container_metadata = collection.get(
|
||||
container_metadata = container.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
container_metadata["libpath"] = libpath
|
||||
container_metadata["lib_container"] = lib_container
|
||||
|
||||
objects_list = self._process(
|
||||
libpath, lib_container, container_name)
|
||||
obj_container = self._process(
|
||||
libpath, lib_container, container_name, None)
|
||||
|
||||
container_metadata["obj_container"] = obj_container
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
container_metadata["objects"] = objects_list
|
||||
container_metadata["objects"] = obj_container.all_objects
|
||||
|
||||
nodes = list(collection.objects)
|
||||
nodes.append(collection)
|
||||
nodes = list(container.objects)
|
||||
nodes.append(container)
|
||||
self[:] = nodes
|
||||
return nodes
|
||||
|
||||
|
|
@ -144,7 +145,7 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
libpath = Path(api.get_representation_path(representation))
|
||||
extension = libpath.suffix.lower()
|
||||
|
||||
logger.debug(
|
||||
self.log.info(
|
||||
"Container: %s\nRepresentation: %s",
|
||||
pformat(container, indent=2),
|
||||
pformat(representation, indent=2),
|
||||
|
|
@ -162,38 +163,47 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
assert libpath.is_file(), (
|
||||
f"The file doesn't exist: {libpath}"
|
||||
)
|
||||
assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, (
|
||||
assert extension in plugin.VALID_EXTENSIONS, (
|
||||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
collection_libpath = collection_metadata["libpath"]
|
||||
objects = collection_metadata["objects"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
|
||||
obj_container = plugin.get_local_collection_with_name(
|
||||
collection_metadata["obj_container"].name
|
||||
)
|
||||
objects = obj_container.all_objects
|
||||
|
||||
container_name = obj_container.name
|
||||
|
||||
normalized_collection_libpath = (
|
||||
str(Path(bpy.path.abspath(collection_libpath)).resolve())
|
||||
)
|
||||
normalized_libpath = (
|
||||
str(Path(bpy.path.abspath(str(libpath))).resolve())
|
||||
)
|
||||
logger.debug(
|
||||
self.log.debug(
|
||||
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_collection_libpath,
|
||||
normalized_libpath,
|
||||
)
|
||||
if normalized_collection_libpath == normalized_libpath:
|
||||
logger.info("Library already loaded, not updating...")
|
||||
self.log.info("Library already loaded, not updating...")
|
||||
return
|
||||
|
||||
self._remove(objects, lib_container)
|
||||
parent = plugin.get_parent_collection(obj_container)
|
||||
|
||||
objects_list = self._process(
|
||||
str(libpath), lib_container, collection.name)
|
||||
self._remove(objects, obj_container)
|
||||
|
||||
obj_container = self._process(
|
||||
str(libpath), lib_container, container_name, parent)
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
collection_metadata["objects"] = objects_list
|
||||
collection_metadata["obj_container"] = obj_container
|
||||
collection_metadata["objects"] = obj_container.all_objects
|
||||
collection_metadata["libpath"] = str(libpath)
|
||||
collection_metadata["representation"] = str(representation["_id"])
|
||||
|
||||
|
|
@ -221,17 +231,20 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
objects = collection_metadata["objects"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
|
||||
self._remove(objects, lib_container)
|
||||
obj_container = plugin.get_local_collection_with_name(
|
||||
collection_metadata["obj_container"].name
|
||||
)
|
||||
objects = obj_container.all_objects
|
||||
|
||||
self._remove(objects, obj_container)
|
||||
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class CacheModelLoader(pype.hosts.blender.plugin.AssetLoader):
|
||||
class CacheModelLoader(plugin.AssetLoader):
|
||||
"""Load cache models.
|
||||
|
||||
Stores the imported asset in a collection named after the asset.
|
||||
|
|
@ -267,7 +280,7 @@ class CacheModelLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
subset = context["subset"]["name"]
|
||||
# TODO (jasper): evaluate use of namespace which is 'alien' to Blender.
|
||||
lib_container = container_name = (
|
||||
pype.hosts.blender.plugin.asset_name(asset, subset, namespace)
|
||||
plugin.asset_name(asset, subset, namespace)
|
||||
)
|
||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||
|
||||
|
|
|
|||
|
|
@ -7,20 +7,14 @@ from typing import Dict, List, Optional
|
|||
|
||||
from avalon import api, blender
|
||||
import bpy
|
||||
import pype.hosts.blender.plugin
|
||||
|
||||
logger = logging.getLogger("pype").getChild("blender").getChild("load_model")
|
||||
import pype.hosts.blender.plugin as plugin
|
||||
|
||||
|
||||
class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader):
|
||||
class BlendRigLoader(plugin.AssetLoader):
|
||||
"""Load rigs from a .blend file.
|
||||
|
||||
Because they come from a .blend file we can simply link the collection that
|
||||
contains the model. There is no further need to 'containerise' it.
|
||||
|
||||
Warning:
|
||||
Loading the same asset more then once is not properly supported at the
|
||||
moment.
|
||||
"""
|
||||
|
||||
families = ["rig"]
|
||||
|
|
@ -30,67 +24,69 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def _remove(self, objects, lib_container):
|
||||
|
||||
for obj in objects:
|
||||
|
||||
def _remove(self, objects, obj_container):
|
||||
for obj in list(objects):
|
||||
if obj.type == 'ARMATURE':
|
||||
bpy.data.armatures.remove(obj.data)
|
||||
elif obj.type == 'MESH':
|
||||
bpy.data.meshes.remove(obj.data)
|
||||
|
||||
for child in bpy.data.collections[lib_container].children:
|
||||
for child in obj_container.children:
|
||||
bpy.data.collections.remove(child)
|
||||
|
||||
bpy.data.collections.remove(bpy.data.collections[lib_container])
|
||||
|
||||
def _process(self, libpath, lib_container, container_name, action):
|
||||
bpy.data.collections.remove(obj_container)
|
||||
|
||||
def _process(
|
||||
self, libpath, lib_container, container_name,
|
||||
action, parent_collection
|
||||
):
|
||||
relative = bpy.context.preferences.filepaths.use_relative_paths
|
||||
with bpy.data.libraries.load(
|
||||
libpath, link=True, relative=relative
|
||||
) as (_, data_to):
|
||||
data_to.collections = [lib_container]
|
||||
|
||||
scene = bpy.context.scene
|
||||
parent = parent_collection
|
||||
|
||||
scene.collection.children.link(bpy.data.collections[lib_container])
|
||||
if parent is None:
|
||||
parent = bpy.context.scene.collection
|
||||
|
||||
rig_container = scene.collection.children[lib_container].make_local()
|
||||
parent.children.link(bpy.data.collections[lib_container])
|
||||
|
||||
rig_container = parent.children[lib_container].make_local()
|
||||
rig_container.name = container_name
|
||||
|
||||
meshes = []
|
||||
armatures = [
|
||||
obj for obj in rig_container.objects if obj.type == 'ARMATURE']
|
||||
|
||||
objects_list = []
|
||||
obj for obj in rig_container.objects
|
||||
if obj.type == 'ARMATURE'
|
||||
]
|
||||
|
||||
for child in rig_container.children:
|
||||
child.make_local()
|
||||
meshes.extend( child.objects )
|
||||
local_child = plugin.prepare_data(child, container_name)
|
||||
meshes.extend(local_child.objects)
|
||||
|
||||
# Link meshes first, then armatures.
|
||||
# The armature is unparented for all the non-local meshes,
|
||||
# when it is made local.
|
||||
for obj in meshes + armatures:
|
||||
obj = obj.make_local()
|
||||
obj.data.make_local()
|
||||
|
||||
if not obj.get(blender.pipeline.AVALON_PROPERTY):
|
||||
obj[blender.pipeline.AVALON_PROPERTY] = dict()
|
||||
|
||||
avalon_info = obj[blender.pipeline.AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": container_name})
|
||||
|
||||
if obj.type == 'ARMATURE' and action is not None:
|
||||
obj.animation_data.action = action
|
||||
|
||||
objects_list.append(obj)
|
||||
|
||||
local_obj = plugin.prepare_data(obj, container_name)
|
||||
plugin.prepare_data(local_obj.data, container_name)
|
||||
|
||||
if not local_obj.get(blender.pipeline.AVALON_PROPERTY):
|
||||
local_obj[blender.pipeline.AVALON_PROPERTY] = dict()
|
||||
|
||||
avalon_info = local_obj[blender.pipeline.AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": container_name})
|
||||
|
||||
if local_obj.type == 'ARMATURE' and action is not None:
|
||||
local_obj.animation_data.action = action
|
||||
|
||||
rig_container.pop(blender.pipeline.AVALON_PROPERTY)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
return objects_list
|
||||
return rig_container
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
|
|
@ -107,9 +103,15 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
lib_container = pype.hosts.blender.plugin.asset_name(asset, subset)
|
||||
container_name = pype.hosts.blender.plugin.asset_name(
|
||||
asset, subset, namespace
|
||||
lib_container = plugin.asset_name(
|
||||
asset, subset
|
||||
)
|
||||
unique_number = plugin.get_unique_number(
|
||||
asset, subset
|
||||
)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
container_name = plugin.asset_name(
|
||||
asset, subset, unique_number
|
||||
)
|
||||
|
||||
container = bpy.data.collections.new(lib_container)
|
||||
|
|
@ -128,11 +130,13 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
container_metadata["libpath"] = libpath
|
||||
container_metadata["lib_container"] = lib_container
|
||||
|
||||
objects_list = self._process(
|
||||
libpath, lib_container, container_name, None)
|
||||
obj_container = self._process(
|
||||
libpath, lib_container, container_name, None, None)
|
||||
|
||||
container_metadata["obj_container"] = obj_container
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
container_metadata["objects"] = objects_list
|
||||
container_metadata["objects"] = obj_container.all_objects
|
||||
|
||||
nodes = list(container.objects)
|
||||
nodes.append(container)
|
||||
|
|
@ -151,15 +155,13 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
Warning:
|
||||
No nested collections are supported at the moment!
|
||||
"""
|
||||
|
||||
collection = bpy.data.collections.get(
|
||||
container["objectName"]
|
||||
)
|
||||
|
||||
libpath = Path(api.get_representation_path(representation))
|
||||
extension = libpath.suffix.lower()
|
||||
|
||||
logger.info(
|
||||
self.log.info(
|
||||
"Container: %s\nRepresentation: %s",
|
||||
pformat(container, indent=2),
|
||||
pformat(representation, indent=2),
|
||||
|
|
@ -177,44 +179,55 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
assert libpath.is_file(), (
|
||||
f"The file doesn't exist: {libpath}"
|
||||
)
|
||||
assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, (
|
||||
assert extension in plugin.VALID_EXTENSIONS, (
|
||||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
collection_libpath = collection_metadata["libpath"]
|
||||
objects = collection_metadata["objects"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
|
||||
obj_container = plugin.get_local_collection_with_name(
|
||||
collection_metadata["obj_container"].name
|
||||
)
|
||||
objects = obj_container.all_objects
|
||||
|
||||
container_name = obj_container.name
|
||||
|
||||
normalized_collection_libpath = (
|
||||
str(Path(bpy.path.abspath(collection_libpath)).resolve())
|
||||
)
|
||||
normalized_libpath = (
|
||||
str(Path(bpy.path.abspath(str(libpath))).resolve())
|
||||
)
|
||||
logger.debug(
|
||||
self.log.debug(
|
||||
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
|
||||
normalized_collection_libpath,
|
||||
normalized_libpath,
|
||||
)
|
||||
if normalized_collection_libpath == normalized_libpath:
|
||||
logger.info("Library already loaded, not updating...")
|
||||
self.log.info("Library already loaded, not updating...")
|
||||
return
|
||||
|
||||
# Get the armature of the rig
|
||||
armatures = [obj for obj in objects if obj.type == 'ARMATURE']
|
||||
assert(len(armatures) == 1)
|
||||
|
||||
action = armatures[0].animation_data.action
|
||||
action = None
|
||||
if armatures[0].animation_data and armatures[0].animation_data.action:
|
||||
action = armatures[0].animation_data.action
|
||||
|
||||
self._remove(objects, lib_container)
|
||||
parent = plugin.get_parent_collection(obj_container)
|
||||
|
||||
objects_list = self._process(
|
||||
str(libpath), lib_container, collection.name, action)
|
||||
self._remove(objects, obj_container)
|
||||
|
||||
obj_container = self._process(
|
||||
str(libpath), lib_container, container_name, action, parent)
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
collection_metadata["objects"] = objects_list
|
||||
collection_metadata["obj_container"] = obj_container
|
||||
collection_metadata["objects"] = obj_container.all_objects
|
||||
collection_metadata["libpath"] = str(libpath)
|
||||
collection_metadata["representation"] = str(representation["_id"])
|
||||
|
||||
|
|
@ -245,10 +258,13 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader):
|
|||
|
||||
collection_metadata = collection.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
objects = collection_metadata["objects"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
|
||||
self._remove(objects, lib_container)
|
||||
obj_container = plugin.get_local_collection_with_name(
|
||||
collection_metadata["obj_container"].name
|
||||
)
|
||||
objects = obj_container.all_objects
|
||||
|
||||
self._remove(objects, obj_container)
|
||||
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class ExtractBlend(pype.api.Extractor):
|
|||
|
||||
label = "Extract Blend"
|
||||
hosts = ["blender"]
|
||||
families = ["animation", "model", "rig", "action", "layout"]
|
||||
families = ["model", "camera", "rig", "action", "layout", "animation"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
|
|
|
|||
|
|
@ -10,9 +10,14 @@ class CollectRenderPath(pyblish.api.InstancePlugin):
|
|||
order = pyblish.api.CollectorOrder + 0.495
|
||||
families = ["render.farm"]
|
||||
|
||||
# Presets
|
||||
anatomy_render_key = None
|
||||
anatomy_publish_render_key = None
|
||||
|
||||
def process(self, instance):
|
||||
anatomy = instance.context.data["anatomy"]
|
||||
anatomy_data = copy.deepcopy(instance.data["anatomyData"])
|
||||
anatomy_data["family"] = "render"
|
||||
padding = anatomy.templates.get("frame_padding", 4)
|
||||
anatomy_data.update({
|
||||
"frame": f"%0{padding}d",
|
||||
|
|
@ -21,12 +26,28 @@ class CollectRenderPath(pyblish.api.InstancePlugin):
|
|||
|
||||
anatomy_filled = anatomy.format(anatomy_data)
|
||||
|
||||
render_dir = anatomy_filled["render_tmp"]["folder"]
|
||||
render_path = anatomy_filled["render_tmp"]["path"]
|
||||
# get anatomy rendering keys
|
||||
anatomy_render_key = self.anatomy_render_key or "render"
|
||||
anatomy_publish_render_key = self.anatomy_publish_render_key or "render"
|
||||
|
||||
# get folder and path for rendering images from celaction
|
||||
render_dir = anatomy_filled[anatomy_render_key]["folder"]
|
||||
render_path = anatomy_filled[anatomy_render_key]["path"]
|
||||
|
||||
# create dir if it doesnt exists
|
||||
os.makedirs(render_dir, exist_ok=True)
|
||||
try:
|
||||
if not os.path.isdir(render_dir):
|
||||
os.makedirs(render_dir, exist_ok=True)
|
||||
except OSError:
|
||||
# directory is not available
|
||||
self.log.warning("Path is unreachable: `{}`".format(render_dir))
|
||||
|
||||
# add rendering path to instance data
|
||||
instance.data["path"] = render_path
|
||||
|
||||
# get anatomy for published renders folder path
|
||||
if anatomy_filled.get(anatomy_publish_render_key):
|
||||
instance.data["publishRenderFolder"] = anatomy_filled[
|
||||
anatomy_publish_render_key]["folder"]
|
||||
|
||||
self.log.info(f"Render output path set to: `{render_path}`")
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import pyblish.api
|
|||
|
||||
|
||||
class VersionUpScene(pyblish.api.ContextPlugin):
|
||||
order = pyblish.api.IntegratorOrder
|
||||
order = pyblish.api.IntegratorOrder + 0.5
|
||||
label = 'Version Up Scene'
|
||||
families = ['scene']
|
||||
families = ['workfile']
|
||||
optional = True
|
||||
active = True
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin):
|
|||
resolution_width = instance.data["resolutionWidth"]
|
||||
resolution_height = instance.data["resolutionHeight"]
|
||||
render_dir = os.path.normpath(os.path.dirname(render_path))
|
||||
render_path = os.path.normpath(render_path)
|
||||
script_name = os.path.basename(script_path)
|
||||
jobname = "%s - %s" % (script_name, instance.name)
|
||||
|
||||
|
|
@ -98,6 +99,7 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin):
|
|||
args = [
|
||||
f"<QUOTE>{script_path}<QUOTE>",
|
||||
"-a",
|
||||
"-16",
|
||||
"-s <STARTFRAME>",
|
||||
"-e <ENDFRAME>",
|
||||
f"-d <QUOTE>{render_dir}<QUOTE>",
|
||||
|
|
@ -135,8 +137,10 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin):
|
|||
|
||||
# Optional, enable double-click to preview rendered
|
||||
# frames from Deadline Monitor
|
||||
"OutputFilename0": output_filename_0.replace("\\", "/")
|
||||
"OutputFilename0": output_filename_0.replace("\\", "/"),
|
||||
|
||||
# # Asset dependency to wait for at least the scene file to sync.
|
||||
# "AssetDependency0": script_path
|
||||
},
|
||||
"PluginInfo": {
|
||||
# Input
|
||||
|
|
|
|||
|
|
@ -96,6 +96,6 @@ class CollectFtrackApi(pyblish.api.ContextPlugin):
|
|||
task_entity = None
|
||||
self.log.warning("Task name is not set.")
|
||||
|
||||
context.data["ftrackProject"] = asset_entity
|
||||
context.data["ftrackProject"] = project_entity
|
||||
context.data["ftrackEntity"] = asset_entity
|
||||
context.data["ftrackTask"] = task_entity
|
||||
|
|
|
|||
|
|
@ -54,8 +54,52 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
|
|||
self.log.debug(query)
|
||||
return query
|
||||
|
||||
def process(self, instance):
|
||||
def _set_task_status(self, instance, task_entity, session):
|
||||
project_entity = instance.context.data.get("ftrackProject")
|
||||
if not project_entity:
|
||||
self.log.info("Task status won't be set, project is not known.")
|
||||
return
|
||||
|
||||
if not task_entity:
|
||||
self.log.info("Task status won't be set, task is not known.")
|
||||
return
|
||||
|
||||
status_name = instance.context.data.get("ftrackStatus")
|
||||
if not status_name:
|
||||
self.log.info("Ftrack status name is not set.")
|
||||
return
|
||||
|
||||
self.log.debug(
|
||||
"Ftrack status name will be (maybe) set to \"{}\"".format(
|
||||
status_name
|
||||
)
|
||||
)
|
||||
|
||||
project_schema = project_entity["project_schema"]
|
||||
task_statuses = project_schema.get_statuses(
|
||||
"Task", task_entity["type_id"]
|
||||
)
|
||||
task_statuses_by_low_name = {
|
||||
status["name"].lower(): status for status in task_statuses
|
||||
}
|
||||
status = task_statuses_by_low_name.get(status_name.lower())
|
||||
if not status:
|
||||
self.log.warning((
|
||||
"Task status \"{}\" won't be set,"
|
||||
" status is now allowed on task type \"{}\"."
|
||||
).format(status_name, task_entity["type"]["name"]))
|
||||
return
|
||||
|
||||
self.log.info("Setting task status to \"{}\"".format(status_name))
|
||||
task_entity["status"] = status
|
||||
try:
|
||||
session.commit()
|
||||
except Exception:
|
||||
tp, value, tb = sys.exc_info()
|
||||
session.rollback()
|
||||
six.reraise(tp, value, tb)
|
||||
|
||||
def process(self, instance):
|
||||
session = instance.context.data["ftrackSession"]
|
||||
if instance.data.get("ftrackTask"):
|
||||
task = instance.data["ftrackTask"]
|
||||
|
|
@ -78,9 +122,11 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
|
|||
info_msg += ", metadata: {metadata}."
|
||||
|
||||
used_asset_versions = []
|
||||
|
||||
self._set_task_status(instance, task, session)
|
||||
|
||||
# Iterate over components and publish
|
||||
for data in instance.data.get("ftrackComponentsList", []):
|
||||
|
||||
# AssetType
|
||||
# Get existing entity.
|
||||
assettype_data = {"short": "upload"}
|
||||
|
|
@ -94,9 +140,9 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
|
|||
# Create a new entity if none exits.
|
||||
if not assettype_entity:
|
||||
assettype_entity = session.create("AssetType", assettype_data)
|
||||
self.log.debug(
|
||||
"Created new AssetType with data: ".format(assettype_data)
|
||||
)
|
||||
self.log.debug("Created new AssetType with data: {}".format(
|
||||
assettype_data
|
||||
))
|
||||
|
||||
# Asset
|
||||
# Get existing entity.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import sys
|
||||
|
||||
import six
|
||||
import pyblish.api
|
||||
from avalon import io
|
||||
|
||||
try:
|
||||
from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_AUTO_SYNC
|
||||
except Exception:
|
||||
CUST_ATTR_AUTO_SYNC = "avalon_auto_sync"
|
||||
|
||||
|
||||
class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
|
||||
"""
|
||||
|
|
@ -39,15 +43,32 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
|
|||
if "hierarchyContext" not in context.data:
|
||||
return
|
||||
|
||||
self.session = self.context.data["ftrackSession"]
|
||||
project_name = self.context.data["projectEntity"]["name"]
|
||||
query = 'Project where full_name is "{}"'.format(project_name)
|
||||
project = self.session.query(query).one()
|
||||
auto_sync_state = project[
|
||||
"custom_attributes"][CUST_ATTR_AUTO_SYNC]
|
||||
|
||||
if not io.Session:
|
||||
io.install()
|
||||
|
||||
self.ft_project = None
|
||||
self.session = context.data["ftrackSession"]
|
||||
|
||||
input_data = context.data["hierarchyContext"]
|
||||
|
||||
self.import_to_ftrack(input_data)
|
||||
# disable termporarily ftrack project's autosyncing
|
||||
if auto_sync_state:
|
||||
self.auto_sync_off(project)
|
||||
|
||||
try:
|
||||
# import ftrack hierarchy
|
||||
self.import_to_ftrack(input_data)
|
||||
except Exception:
|
||||
raise
|
||||
finally:
|
||||
if auto_sync_state:
|
||||
self.auto_sync_on(project)
|
||||
|
||||
def import_to_ftrack(self, input_data, parent=None):
|
||||
for entity_name in input_data:
|
||||
|
|
@ -217,3 +238,28 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
|
|||
six.reraise(tp, value, tb)
|
||||
|
||||
return entity
|
||||
|
||||
def auto_sync_off(self, project):
|
||||
project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = False
|
||||
|
||||
self.log.info("Ftrack autosync swithed off")
|
||||
|
||||
try:
|
||||
self.session.commit()
|
||||
except Exception:
|
||||
tp, value, tb = sys.exc_info()
|
||||
self.session.rollback()
|
||||
raise
|
||||
|
||||
def auto_sync_on(self, project):
|
||||
|
||||
project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = True
|
||||
|
||||
self.log.info("Ftrack autosync swithed on")
|
||||
|
||||
try:
|
||||
self.session.commit()
|
||||
except Exception:
|
||||
tp, value, tb = sys.exc_info()
|
||||
self.session.rollback()
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -1,34 +1,27 @@
|
|||
import os
|
||||
import subprocess
|
||||
import json
|
||||
from pype.api import config
|
||||
from avalon import api
|
||||
|
||||
|
||||
def get_families():
|
||||
families = []
|
||||
paths = config.get_presets().get("djv_view", {}).get("config", {}).get(
|
||||
"djv_paths", []
|
||||
)
|
||||
for path in paths:
|
||||
def existing_djv_path():
|
||||
djv_paths = os.environ.get("DJV_PATH") or ""
|
||||
for path in djv_paths.split(os.pathsep):
|
||||
if os.path.exists(path):
|
||||
families.append("*")
|
||||
break
|
||||
return families
|
||||
|
||||
|
||||
def get_representation():
|
||||
return config.get_presets().get("djv_view", {}).get("config", {}).get(
|
||||
'file_ext', []
|
||||
)
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
class OpenInDJV(api.Loader):
|
||||
"""Open Image Sequence with system default"""
|
||||
|
||||
config_data = config.get_presets().get("djv_view", {}).get("config", {})
|
||||
families = get_families()
|
||||
representations = get_representation()
|
||||
djv_path = existing_djv_path()
|
||||
families = ["*"] if djv_path else []
|
||||
representations = [
|
||||
"cin", "dpx", "avi", "dv", "gif", "flv", "mkv", "mov", "mpg", "mpeg",
|
||||
"mp4", "m4v", "mxf", "iff", "z", "ifl", "jpeg", "jpg", "jfif", "lut",
|
||||
"1dl", "exr", "pic", "png", "ppm", "pnm", "pgm", "pbm", "rla", "rpf",
|
||||
"sgi", "rgba", "rgb", "bw", "tga", "tiff", "tif", "img"
|
||||
]
|
||||
|
||||
label = "Open in DJV"
|
||||
order = -10
|
||||
|
|
@ -36,14 +29,6 @@ class OpenInDJV(api.Loader):
|
|||
color = "orange"
|
||||
|
||||
def load(self, context, name, namespace, data):
|
||||
self.djv_path = None
|
||||
paths = config.get_presets().get("djv_view", {}).get("config", {}).get(
|
||||
"djv_paths", []
|
||||
)
|
||||
for path in paths:
|
||||
if os.path.exists(path):
|
||||
self.djv_path = path
|
||||
break
|
||||
directory = os.path.dirname(self.fname)
|
||||
from avalon.vendor import clique
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
"textures",
|
||||
"action",
|
||||
"harmony.template",
|
||||
"harmony.palette",
|
||||
"editorial"
|
||||
]
|
||||
exclude_families = ["clip"]
|
||||
|
|
@ -515,12 +516,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
instance: the instance to integrate
|
||||
"""
|
||||
transfers = instance.data.get("transfers", list())
|
||||
|
||||
for src, dest in transfers:
|
||||
if os.path.normpath(src) != os.path.normpath(dest):
|
||||
self.copy_file(src, dest)
|
||||
|
||||
transfers = instance.data.get("transfers", list())
|
||||
for src, dest in transfers:
|
||||
self.copy_file(src, dest)
|
||||
|
||||
|
|
@ -558,12 +553,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
|
||||
# copy file with speedcopy and check if size of files are simetrical
|
||||
while True:
|
||||
import shutil
|
||||
try:
|
||||
copyfile(src, dst)
|
||||
except (OSError, AttributeError) as e:
|
||||
self.log.warning(e)
|
||||
# try it again with shutil
|
||||
import shutil
|
||||
except shutil.SameFileError as sfe:
|
||||
self.log.critical("files are the same {} to {}".format(src, dst))
|
||||
os.remove(dst)
|
||||
try:
|
||||
shutil.copyfile(src, dst)
|
||||
self.log.debug("Copying files with shutil...")
|
||||
|
|
@ -607,7 +602,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
"type": "subset",
|
||||
"name": subset_name,
|
||||
"data": {
|
||||
"families": instance.data.get('families')
|
||||
"families": instance.data.get("families", [])
|
||||
},
|
||||
"parent": asset["_id"]
|
||||
}).inserted_id
|
||||
|
|
@ -747,6 +742,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
value += 1
|
||||
|
||||
if value > highest_value:
|
||||
matching_profiles = {}
|
||||
highest_value = value
|
||||
|
||||
if value == highest_value:
|
||||
|
|
|
|||
|
|
@ -12,7 +12,15 @@ from avalon.vendor import requests, clique
|
|||
import pyblish.api
|
||||
|
||||
|
||||
def _get_script():
|
||||
def _get_script(path):
|
||||
|
||||
# pass input path if exists
|
||||
if path:
|
||||
if os.path.exists(path):
|
||||
return str(path)
|
||||
else:
|
||||
raise
|
||||
|
||||
"""Get path to the image sequence script."""
|
||||
try:
|
||||
from pathlib import Path
|
||||
|
|
@ -192,6 +200,38 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
families_transfer = ["render3d", "render2d", "ftrack", "slate"]
|
||||
plugin_python_version = "3.7"
|
||||
|
||||
# script path for publish_filesequence.py
|
||||
publishing_script = None
|
||||
|
||||
def _create_metadata_path(self, instance):
|
||||
ins_data = instance.data
|
||||
# Ensure output dir exists
|
||||
output_dir = ins_data.get("publishRenderFolder", ins_data["outputDir"])
|
||||
|
||||
try:
|
||||
if not os.path.isdir(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
except OSError:
|
||||
# directory is not available
|
||||
self.log.warning("Path is unreachable: `{}`".format(output_dir))
|
||||
|
||||
metadata_filename = "{}_metadata.json".format(ins_data["subset"])
|
||||
|
||||
metadata_path = os.path.join(output_dir, metadata_filename)
|
||||
|
||||
# Convert output dir to `{root}/rest/of/path/...` with Anatomy
|
||||
success, roothless_mtdt_p = self.anatomy.find_root_template_from_path(
|
||||
metadata_path)
|
||||
if not success:
|
||||
# `rootless_path` is not set to `output_dir` if none of roots match
|
||||
self.log.warning((
|
||||
"Could not find root path for remapping \"{}\"."
|
||||
" This may cause issues on farm."
|
||||
).format(output_dir))
|
||||
roothless_mtdt_p = metadata_path
|
||||
|
||||
return (metadata_path, roothless_mtdt_p)
|
||||
|
||||
def _submit_deadline_post_job(self, instance, job):
|
||||
"""Submit publish job to Deadline.
|
||||
|
||||
|
|
@ -205,17 +245,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
job_name = "Publish - {subset}".format(subset=subset)
|
||||
|
||||
output_dir = instance.data["outputDir"]
|
||||
# Convert output dir to `{root}/rest/of/path/...` with Anatomy
|
||||
success, rootless_path = (
|
||||
self.anatomy.find_root_template_from_path(output_dir)
|
||||
)
|
||||
if not success:
|
||||
# `rootless_path` is not set to `output_dir` if none of roots match
|
||||
self.log.warning((
|
||||
"Could not find root path for remapping \"{}\"."
|
||||
" This may cause issues on farm."
|
||||
).format(output_dir))
|
||||
rootless_path = output_dir
|
||||
|
||||
# Generate the payload for Deadline submission
|
||||
payload = {
|
||||
|
|
@ -239,7 +268,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
},
|
||||
"PluginInfo": {
|
||||
"Version": self.plugin_python_version,
|
||||
"ScriptFile": _get_script(),
|
||||
"ScriptFile": _get_script(self.publishing_script),
|
||||
"Arguments": "",
|
||||
"SingleFrameOnly": "True",
|
||||
},
|
||||
|
|
@ -249,11 +278,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
|
||||
# Transfer the environment from the original job to this dependent
|
||||
# job so they use the same environment
|
||||
metadata_filename = "{}_metadata.json".format(subset)
|
||||
metadata_path = os.path.join(rootless_path, metadata_filename)
|
||||
metadata_path, roothless_metadata_path = self._create_metadata_path(
|
||||
instance)
|
||||
|
||||
environment = job["Props"].get("Env", {})
|
||||
environment["PYPE_METADATA_FILE"] = metadata_path
|
||||
environment["PYPE_METADATA_FILE"] = roothless_metadata_path
|
||||
environment["AVALON_PROJECT"] = io.Session["AVALON_PROJECT"]
|
||||
environment["PYPE_LOG_NO_COLORS"] = "1"
|
||||
try:
|
||||
|
|
@ -488,7 +517,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
if bake_render_path:
|
||||
preview = False
|
||||
|
||||
if "celaction" in self.hosts:
|
||||
if "celaction" in pyblish.api.registered_hosts():
|
||||
preview = True
|
||||
|
||||
staging = os.path.dirname(list(collection)[0])
|
||||
|
|
@ -847,14 +876,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
}
|
||||
publish_job.update({"ftrack": ftrack})
|
||||
|
||||
# Ensure output dir exists
|
||||
output_dir = instance.data["outputDir"]
|
||||
if not os.path.isdir(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
metadata_path, roothless_metadata_path = self._create_metadata_path(
|
||||
instance)
|
||||
|
||||
metadata_filename = "{}_metadata.json".format(subset)
|
||||
|
||||
metadata_path = os.path.join(output_dir, metadata_filename)
|
||||
self.log.info("Writing json file: {}".format(metadata_path))
|
||||
with open(metadata_path, "w") as f:
|
||||
json.dump(publish_job, f, indent=4, sort_keys=True)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class ValidateContainers(pyblish.api.ContextPlugin):
|
|||
|
||||
label = "Validate Containers"
|
||||
order = pyblish.api.ValidatorOrder
|
||||
hosts = ["maya", "houdini", "nuke"]
|
||||
hosts = ["maya", "houdini", "nuke", "harmony", "photoshop"]
|
||||
optional = True
|
||||
actions = [ShowInventory]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import os
|
||||
import uuid
|
||||
|
||||
import clique
|
||||
|
||||
from avalon import api, harmony
|
||||
import pype.lib
|
||||
|
||||
copy_files = """function copyFile(srcFilename, dstFilename)
|
||||
{
|
||||
|
|
@ -98,33 +100,63 @@ function import_files(args)
|
|||
transparencyModeAttr.setValue(SGITransparencyMode);
|
||||
if (extension == "psd")
|
||||
transparencyModeAttr.setValue(FlatPSDTransparencyMode);
|
||||
if (extension == "jpg")
|
||||
transparencyModeAttr.setValue(LayeredPSDTransparencyMode);
|
||||
|
||||
node.linkAttr(read, "DRAWING.ELEMENT", uniqueColumnName);
|
||||
|
||||
// Create a drawing for each file.
|
||||
for( var i =0; i <= files.length - 1; ++i)
|
||||
if (files.length == 1)
|
||||
{
|
||||
timing = start_frame + i
|
||||
// Create a drawing drawing, 'true' indicate that the file exists.
|
||||
Drawing.create(elemId, timing, true);
|
||||
Drawing.create(elemId, 1, true);
|
||||
// Get the actual path, in tmp folder.
|
||||
var drawingFilePath = Drawing.filename(elemId, timing.toString());
|
||||
copyFile( files[i], drawingFilePath );
|
||||
var drawingFilePath = Drawing.filename(elemId, "1");
|
||||
copyFile(files[0], drawingFilePath);
|
||||
// Expose the image for the entire frame range.
|
||||
for( var i =0; i <= frame.numberOf() - 1; ++i)
|
||||
{
|
||||
timing = start_frame + i
|
||||
column.setEntry(uniqueColumnName, 1, timing, "1");
|
||||
}
|
||||
} else {
|
||||
// Create a drawing for each file.
|
||||
for( var i =0; i <= files.length - 1; ++i)
|
||||
{
|
||||
timing = start_frame + i
|
||||
// Create a drawing drawing, 'true' indicate that the file exists.
|
||||
Drawing.create(elemId, timing, true);
|
||||
// Get the actual path, in tmp folder.
|
||||
var drawingFilePath = Drawing.filename(elemId, timing.toString());
|
||||
copyFile( files[i], drawingFilePath );
|
||||
|
||||
column.setEntry(uniqueColumnName, 1, timing, timing.toString());
|
||||
column.setEntry(uniqueColumnName, 1, timing, timing.toString());
|
||||
}
|
||||
}
|
||||
|
||||
var green_color = new ColorRGBA(0, 255, 0, 255);
|
||||
node.setColor(read, green_color);
|
||||
|
||||
return read;
|
||||
}
|
||||
import_files
|
||||
"""
|
||||
|
||||
replace_files = """function replace_files(args)
|
||||
replace_files = """var PNGTransparencyMode = 0; //Premultiplied wih Black
|
||||
var TGATransparencyMode = 0; //Premultiplied wih Black
|
||||
var SGITransparencyMode = 0; //Premultiplied wih Black
|
||||
var LayeredPSDTransparencyMode = 1; //Straight
|
||||
var FlatPSDTransparencyMode = 2; //Premultiplied wih White
|
||||
|
||||
function replace_files(args)
|
||||
{
|
||||
var files = args[0];
|
||||
MessageLog.trace(files);
|
||||
MessageLog.trace(files.length);
|
||||
var _node = args[1];
|
||||
var start_frame = args[2];
|
||||
|
||||
var _column = node.linkedColumn(_node, "DRAWING.ELEMENT");
|
||||
var elemId = column.getElementIdOfDrawing(_column);
|
||||
|
||||
// Delete existing drawings.
|
||||
var timings = column.getDrawingTimings(_column);
|
||||
|
|
@ -133,20 +165,62 @@ replace_files = """function replace_files(args)
|
|||
column.deleteDrawingAt(_column, parseInt(timings[i]));
|
||||
}
|
||||
|
||||
// Create new drawings.
|
||||
for( var i =0; i <= files.length - 1; ++i)
|
||||
{
|
||||
timing = start_frame + i
|
||||
// Create a drawing drawing, 'true' indicate that the file exists.
|
||||
Drawing.create(node.getElementId(_node), timing, true);
|
||||
// Get the actual path, in tmp folder.
|
||||
var drawingFilePath = Drawing.filename(
|
||||
node.getElementId(_node), timing.toString()
|
||||
);
|
||||
copyFile( files[i], drawingFilePath );
|
||||
|
||||
column.setEntry(_column, 1, timing, timing.toString());
|
||||
var filename = files[0];
|
||||
var pos = filename.lastIndexOf(".");
|
||||
if( pos < 0 )
|
||||
return null;
|
||||
var extension = filename.substr(pos+1).toLowerCase();
|
||||
|
||||
if(extension == "jpeg")
|
||||
extension = "jpg";
|
||||
|
||||
var transparencyModeAttr = node.getAttr(
|
||||
_node, frame.current(), "applyMatteToColor"
|
||||
);
|
||||
if (extension == "png")
|
||||
transparencyModeAttr.setValue(PNGTransparencyMode);
|
||||
if (extension == "tga")
|
||||
transparencyModeAttr.setValue(TGATransparencyMode);
|
||||
if (extension == "sgi")
|
||||
transparencyModeAttr.setValue(SGITransparencyMode);
|
||||
if (extension == "psd")
|
||||
transparencyModeAttr.setValue(FlatPSDTransparencyMode);
|
||||
if (extension == "jpg")
|
||||
transparencyModeAttr.setValue(LayeredPSDTransparencyMode);
|
||||
|
||||
if (files.length == 1)
|
||||
{
|
||||
// Create a drawing drawing, 'true' indicate that the file exists.
|
||||
Drawing.create(elemId, 1, true);
|
||||
// Get the actual path, in tmp folder.
|
||||
var drawingFilePath = Drawing.filename(elemId, "1");
|
||||
copyFile(files[0], drawingFilePath);
|
||||
MessageLog.trace(files[0]);
|
||||
MessageLog.trace(drawingFilePath);
|
||||
// Expose the image for the entire frame range.
|
||||
for( var i =0; i <= frame.numberOf() - 1; ++i)
|
||||
{
|
||||
timing = start_frame + i
|
||||
column.setEntry(_column, 1, timing, "1");
|
||||
}
|
||||
} else {
|
||||
// Create a drawing for each file.
|
||||
for( var i =0; i <= files.length - 1; ++i)
|
||||
{
|
||||
timing = start_frame + i
|
||||
// Create a drawing drawing, 'true' indicate that the file exists.
|
||||
Drawing.create(elemId, timing, true);
|
||||
// Get the actual path, in tmp folder.
|
||||
var drawingFilePath = Drawing.filename(elemId, timing.toString());
|
||||
copyFile( files[i], drawingFilePath );
|
||||
|
||||
column.setEntry(_column, 1, timing, timing.toString());
|
||||
}
|
||||
}
|
||||
|
||||
var green_color = new ColorRGBA(0, 255, 0, 255);
|
||||
node.setColor(_node, green_color);
|
||||
}
|
||||
replace_files
|
||||
"""
|
||||
|
|
@ -156,8 +230,8 @@ class ImageSequenceLoader(api.Loader):
|
|||
"""Load images
|
||||
Stores the imported asset in a container named after the asset.
|
||||
"""
|
||||
families = ["shot", "render"]
|
||||
representations = ["jpeg", "png"]
|
||||
families = ["shot", "render", "image"]
|
||||
representations = ["jpeg", "png", "jpg"]
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
|
||||
|
|
@ -165,20 +239,29 @@ class ImageSequenceLoader(api.Loader):
|
|||
os.listdir(os.path.dirname(self.fname))
|
||||
)
|
||||
files = []
|
||||
for f in list(collections[0]):
|
||||
if collections:
|
||||
for f in list(collections[0]):
|
||||
files.append(
|
||||
os.path.join(
|
||||
os.path.dirname(self.fname), f
|
||||
).replace("\\", "/")
|
||||
)
|
||||
else:
|
||||
files.append(
|
||||
os.path.join(os.path.dirname(self.fname), f).replace("\\", "/")
|
||||
os.path.join(
|
||||
os.path.dirname(self.fname), remainder[0]
|
||||
).replace("\\", "/")
|
||||
)
|
||||
|
||||
name = context["subset"]["name"]
|
||||
name += "_{}".format(uuid.uuid4())
|
||||
read_node = harmony.send(
|
||||
{
|
||||
"function": copy_files + import_files,
|
||||
"args": ["Top", files, context["subset"]["name"], 1]
|
||||
"args": ["Top", files, name, 1]
|
||||
}
|
||||
)["result"]
|
||||
|
||||
self[:] = [read_node]
|
||||
|
||||
return harmony.containerise(
|
||||
name,
|
||||
namespace,
|
||||
|
|
@ -188,17 +271,25 @@ class ImageSequenceLoader(api.Loader):
|
|||
)
|
||||
|
||||
def update(self, container, representation):
|
||||
node = container.pop("node")
|
||||
node = harmony.find_node_by_name(container["name"], "READ")
|
||||
|
||||
path = api.get_representation_path(representation)
|
||||
collections, remainder = clique.assemble(
|
||||
os.listdir(
|
||||
os.path.dirname(api.get_representation_path(representation))
|
||||
)
|
||||
os.listdir(os.path.dirname(path))
|
||||
)
|
||||
files = []
|
||||
for f in list(collections[0]):
|
||||
if collections:
|
||||
for f in list(collections[0]):
|
||||
files.append(
|
||||
os.path.join(
|
||||
os.path.dirname(path), f
|
||||
).replace("\\", "/")
|
||||
)
|
||||
else:
|
||||
files.append(
|
||||
os.path.join(os.path.dirname(self.fname), f).replace("\\", "/")
|
||||
os.path.join(
|
||||
os.path.dirname(path), remainder[0]
|
||||
).replace("\\", "/")
|
||||
)
|
||||
|
||||
harmony.send(
|
||||
|
|
@ -208,12 +299,34 @@ class ImageSequenceLoader(api.Loader):
|
|||
}
|
||||
)
|
||||
|
||||
# Colour node.
|
||||
func = """function func(args){
|
||||
for( var i =0; i <= args[0].length - 1; ++i)
|
||||
{
|
||||
var red_color = new ColorRGBA(255, 0, 0, 255);
|
||||
var green_color = new ColorRGBA(0, 255, 0, 255);
|
||||
if (args[1] == "red"){
|
||||
node.setColor(args[0], red_color);
|
||||
}
|
||||
if (args[1] == "green"){
|
||||
node.setColor(args[0], green_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
func
|
||||
"""
|
||||
if pype.lib.is_latest(representation):
|
||||
harmony.send({"function": func, "args": [node, "green"]})
|
||||
else:
|
||||
harmony.send({"function": func, "args": [node, "red"]})
|
||||
|
||||
harmony.imprint(
|
||||
node, {"representation": str(representation["_id"])}
|
||||
)
|
||||
|
||||
def remove(self, container):
|
||||
node = container.pop("node")
|
||||
node = harmony.find_node_by_name(container["name"], "READ")
|
||||
|
||||
func = """function deleteNode(_node)
|
||||
{
|
||||
node.deleteNode(_node, true, true);
|
||||
|
|
|
|||
66
pype/plugins/harmony/load/load_palette.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import os
|
||||
import shutil
|
||||
|
||||
from avalon import api, harmony
|
||||
from avalon.vendor import Qt
|
||||
|
||||
|
||||
class ImportPaletteLoader(api.Loader):
|
||||
"""Import palettes."""
|
||||
|
||||
families = ["harmony.palette"]
|
||||
representations = ["plt"]
|
||||
label = "Import Palette"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
name = self.load_palette(context["representation"])
|
||||
|
||||
return harmony.containerise(
|
||||
name,
|
||||
namespace,
|
||||
name,
|
||||
context,
|
||||
self.__class__.__name__
|
||||
)
|
||||
|
||||
def load_palette(self, representation):
|
||||
subset_name = representation["context"]["subset"]
|
||||
name = subset_name.replace("palette", "")
|
||||
|
||||
# Overwrite palette on disk.
|
||||
scene_path = harmony.send(
|
||||
{"function": "scene.currentProjectPath"}
|
||||
)["result"]
|
||||
src = api.get_representation_path(representation)
|
||||
dst = os.path.join(
|
||||
scene_path,
|
||||
"palette-library",
|
||||
"{}.plt".format(name)
|
||||
)
|
||||
shutil.copy(src, dst)
|
||||
|
||||
harmony.save_scene()
|
||||
|
||||
# Dont allow instances with the same name.
|
||||
message_box = Qt.QtWidgets.QMessageBox()
|
||||
message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning)
|
||||
msg = "Updated {}.".format(subset_name)
|
||||
msg += " You need to reload the scene to see the changes."
|
||||
message_box.setText(msg)
|
||||
message_box.exec_()
|
||||
|
||||
return name
|
||||
|
||||
def remove(self, container):
|
||||
harmony.remove(container["name"])
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
||||
def update(self, container, representation):
|
||||
self.remove(container)
|
||||
name = self.load_palette(representation)
|
||||
|
||||
container["representation"] = str(representation["_id"])
|
||||
container["name"] = name
|
||||
harmony.imprint(name, container)
|
||||
|
|
@ -9,7 +9,7 @@ from avalon import api, harmony
|
|||
class ImportTemplateLoader(api.Loader):
|
||||
"""Import templates."""
|
||||
|
||||
families = ["harmony.template"]
|
||||
families = ["harmony.template", "workfile"]
|
||||
representations = ["*"]
|
||||
label = "Import Template"
|
||||
|
||||
|
|
@ -40,5 +40,5 @@ class ImportWorkfileLoader(ImportTemplateLoader):
|
|||
"""Import workfiles."""
|
||||
|
||||
families = ["workfile"]
|
||||
representations = ["*"]
|
||||
representations = ["zip"]
|
||||
label = "Import Workfile"
|
||||
|
|
|
|||
45
pype/plugins/harmony/publish/collect_palettes.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
import pyblish.api
|
||||
from avalon import harmony
|
||||
|
||||
|
||||
class CollectPalettes(pyblish.api.ContextPlugin):
|
||||
"""Gather palettes from scene when publishing templates."""
|
||||
|
||||
label = "Palettes"
|
||||
order = pyblish.api.CollectorOrder
|
||||
hosts = ["harmony"]
|
||||
|
||||
def process(self, context):
|
||||
func = """function func()
|
||||
{
|
||||
var palette_list = PaletteObjectManager.getScenePaletteList();
|
||||
|
||||
var palettes = {};
|
||||
for(var i=0; i < palette_list.numPalettes; ++i)
|
||||
{
|
||||
var palette = palette_list.getPaletteByIndex(i);
|
||||
palettes[palette.getName()] = palette.id;
|
||||
}
|
||||
|
||||
return palettes;
|
||||
}
|
||||
func
|
||||
"""
|
||||
palettes = harmony.send({"function": func})["result"]
|
||||
|
||||
for name, id in palettes.items():
|
||||
instance = context.create_instance(name)
|
||||
instance.data.update({
|
||||
"id": id,
|
||||
"family": "harmony.palette",
|
||||
"asset": os.environ["AVALON_ASSET"],
|
||||
"subset": "palette" + name
|
||||
})
|
||||
self.log.info(
|
||||
"Created instance:\n" + json.dumps(
|
||||
instance.data, sort_keys=True, indent=4
|
||||
)
|
||||
)
|
||||
34
pype/plugins/harmony/publish/extract_palette.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import os
|
||||
|
||||
from avalon import harmony
|
||||
import pype.api
|
||||
import pype.hosts.harmony
|
||||
|
||||
|
||||
class ExtractPalette(pype.api.Extractor):
|
||||
"""Extract palette."""
|
||||
|
||||
label = "Extract Palette"
|
||||
hosts = ["harmony"]
|
||||
families = ["harmony.palette"]
|
||||
|
||||
def process(self, instance):
|
||||
func = """function func(args)
|
||||
{
|
||||
var palette_list = PaletteObjectManager.getScenePaletteList();
|
||||
var palette = palette_list.getPaletteById(args[0]);
|
||||
return (palette.getPath() + "/" + palette.getName() + ".plt");
|
||||
}
|
||||
func
|
||||
"""
|
||||
palette_file = harmony.send(
|
||||
{"function": func, "args": [instance.data["id"]]}
|
||||
)["result"]
|
||||
|
||||
representation = {
|
||||
"name": "plt",
|
||||
"ext": "plt",
|
||||
"files": os.path.basename(palette_file),
|
||||
"stagingDir": os.path.dirname(palette_file)
|
||||
}
|
||||
instance.data["representations"] = [representation]
|
||||
|
|
@ -111,13 +111,22 @@ class ExtractRender(pyblish.api.InstancePlugin):
|
|||
|
||||
# Generate mov.
|
||||
mov_path = os.path.join(path, instance.data["name"] + ".mov")
|
||||
args = [
|
||||
"ffmpeg", "-y",
|
||||
"-i", audio_path,
|
||||
"-i",
|
||||
os.path.join(path, collection.head + "%04d" + collection.tail),
|
||||
mov_path
|
||||
]
|
||||
if os.path.isfile(audio_path):
|
||||
args = [
|
||||
"ffmpeg", "-y",
|
||||
"-i", audio_path,
|
||||
"-i",
|
||||
os.path.join(path, collection.head + "%04d" + collection.tail),
|
||||
mov_path
|
||||
]
|
||||
else:
|
||||
args = [
|
||||
"ffmpeg", "-y",
|
||||
"-i",
|
||||
os.path.join(path, collection.head + "%04d" + collection.tail),
|
||||
mov_path
|
||||
]
|
||||
|
||||
process = subprocess.Popen(
|
||||
args,
|
||||
stdout=subprocess.PIPE,
|
||||
|
|
|
|||
37
pype/plugins/harmony/publish/validate_audio.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import json
|
||||
import os
|
||||
|
||||
import pyblish.api
|
||||
|
||||
import avalon.harmony
|
||||
import pype.hosts.harmony
|
||||
|
||||
|
||||
class ValidateAudio(pyblish.api.InstancePlugin):
|
||||
"""Ensures that there is an audio file in the scene. If you are sure that you want to send render without audio, you can disable this validator before clicking on "publish" """
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Audio"
|
||||
families = ["render"]
|
||||
hosts = ["harmony"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
# Collect scene data.
|
||||
func = """function func(write_node)
|
||||
{
|
||||
return [
|
||||
sound.getSoundtrackAll().path()
|
||||
]
|
||||
}
|
||||
func
|
||||
"""
|
||||
result = avalon.harmony.send(
|
||||
{"function": func, "args": [instance[0]]}
|
||||
)["result"]
|
||||
|
||||
audio_path = result[0]
|
||||
|
||||
msg = "You are missing audio file:\n{}".format(audio_path)
|
||||
|
||||
assert os.path.isfile(audio_path), msg
|
||||
|
|
@ -50,8 +50,11 @@ class ImagePlaneLoader(api.Loader):
|
|||
|
||||
camera = selection[0]
|
||||
|
||||
camera.displayResolution.set(1)
|
||||
camera.farClipPlane.set(image_plane_depth * 10)
|
||||
try:
|
||||
camera.displayResolution.set(1)
|
||||
camera.farClipPlane.set(image_plane_depth * 10)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
# Create image plane
|
||||
image_plane_transform, image_plane_shape = pc.imagePlane(
|
||||
|
|
|
|||
|
|
@ -103,9 +103,7 @@ class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin):
|
|||
instance (:class:`pyblish.api.Instance`): published instance.
|
||||
|
||||
"""
|
||||
invalid = self.get_invalid(instance,
|
||||
self.SUFFIX_NAMING_TABLE,
|
||||
self.ALLOW_IF_NOT_IN_SUFFIX_TABLE)
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise ValueError("Incorrectly named geometry "
|
||||
"transforms: {0}".format(invalid))
|
||||
|
|
|
|||
|
|
@ -49,6 +49,24 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
render_path = instance.data['path']
|
||||
script_path = context.data["currentFile"]
|
||||
|
||||
for item in context:
|
||||
if "workfile" in item.data["families"]:
|
||||
msg = "Workfile (scene) must be published along"
|
||||
assert item.data["publish"] is True, msg
|
||||
|
||||
template_data = item.data.get("anatomyData")
|
||||
rep = item.data.get("representations")[0].get("name")
|
||||
template_data["representation"] = rep
|
||||
template_data["ext"] = rep
|
||||
template_data["comment"] = None
|
||||
anatomy_filled = context.data["anatomy"].format(template_data)
|
||||
template_filled = anatomy_filled["publish"]["path"]
|
||||
script_path = os.path.normpath(template_filled)
|
||||
|
||||
self.log.info(
|
||||
"Using published scene for render {}".format(script_path)
|
||||
)
|
||||
|
||||
# exception for slate workflow
|
||||
if "slate" in instance.data["families"]:
|
||||
self._frame_start -= 1
|
||||
|
|
@ -120,7 +138,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
chunk_size = self.deadline_chunk_size
|
||||
|
||||
priority = instance.data.get("deadlinePriority")
|
||||
if priority != 50:
|
||||
if not priority:
|
||||
priority = self.deadline_priority
|
||||
|
||||
payload = {
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ import pyblish.api
|
|||
import pype.api
|
||||
|
||||
|
||||
class ValidateNukeWriteKnobs(pyblish.api.ContextPlugin):
|
||||
class ValidateKnobs(pyblish.api.ContextPlugin):
|
||||
"""Ensure knobs are consistent.
|
||||
|
||||
Knobs to validate and their values comes from the
|
||||
|
||||
Example for presets in config:
|
||||
"presets/plugins/nuke/publish.json" preset, which needs this structure:
|
||||
"ValidateNukeWriteKnobs": {
|
||||
"ValidateKnobs": {
|
||||
"enabled": true,
|
||||
"knobs": {
|
||||
"family": {
|
||||
|
|
@ -22,22 +22,31 @@ class ValidateNukeWriteKnobs(pyblish.api.ContextPlugin):
|
|||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Write Knobs"
|
||||
label = "Validate Knobs"
|
||||
hosts = ["nuke"]
|
||||
actions = [pype.api.RepairContextAction]
|
||||
optional = True
|
||||
|
||||
def process(self, context):
|
||||
# Check for preset existence.
|
||||
if not getattr(self, "knobs"):
|
||||
nuke_presets = context.data["presets"].get("nuke")
|
||||
|
||||
if not nuke_presets:
|
||||
return
|
||||
|
||||
publish_presets = nuke_presets.get("publish")
|
||||
|
||||
if not publish_presets:
|
||||
return
|
||||
|
||||
plugin_preset = publish_presets.get("ValidateKnobs")
|
||||
|
||||
if not plugin_preset:
|
||||
return
|
||||
|
||||
self.log.debug("__ self.knobs: {}".format(self.knobs))
|
||||
|
||||
invalid = self.get_invalid(context, compute=True)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
"Found knobs with invalid values: {}".format(invalid)
|
||||
"Found knobs with invalid values:\n{}".format(invalid)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -51,6 +60,8 @@ class ValidateNukeWriteKnobs(pyblish.api.ContextPlugin):
|
|||
@classmethod
|
||||
def get_invalid_knobs(cls, context):
|
||||
invalid_knobs = []
|
||||
publish_presets = context.data["presets"]["nuke"]["publish"]
|
||||
knobs_preset = publish_presets["ValidateKnobs"]["knobs"]
|
||||
for instance in context:
|
||||
# Filter publisable instances.
|
||||
if not instance.data["publish"]:
|
||||
|
|
@ -59,15 +70,15 @@ class ValidateNukeWriteKnobs(pyblish.api.ContextPlugin):
|
|||
# Filter families.
|
||||
families = [instance.data["family"]]
|
||||
families += instance.data.get("families", [])
|
||||
families = list(set(families) & set(cls.knobs.keys()))
|
||||
families = list(set(families) & set(knobs_preset.keys()))
|
||||
if not families:
|
||||
continue
|
||||
|
||||
# Get all knobs to validate.
|
||||
knobs = {}
|
||||
for family in families:
|
||||
for preset in cls.knobs[family]:
|
||||
knobs.update({preset: cls.knobs[family][preset]})
|
||||
for preset in knobs_preset[family]:
|
||||
knobs.update({preset: knobs_preset[family][preset]})
|
||||
|
||||
# Get invalid knobs.
|
||||
nodes = []
|
||||
|
|
@ -82,16 +93,20 @@ class ValidateNukeWriteKnobs(pyblish.api.ContextPlugin):
|
|||
|
||||
for node in nodes:
|
||||
for knob in node.knobs():
|
||||
if knob in knobs.keys():
|
||||
expected = knobs[knob]
|
||||
if node[knob].value() != expected:
|
||||
invalid_knobs.append(
|
||||
{
|
||||
"knob": node[knob],
|
||||
"expected": expected,
|
||||
"current": node[knob].value()
|
||||
}
|
||||
)
|
||||
if knob not in knobs.keys():
|
||||
continue
|
||||
|
||||
expected = knobs[knob]
|
||||
if node[knob].value() != expected:
|
||||
invalid_knobs.append(
|
||||
{
|
||||
"knob": node[knob],
|
||||
"name": node[knob].name(),
|
||||
"label": node[knob].label(),
|
||||
"expected": expected,
|
||||
"current": node[knob].value()
|
||||
}
|
||||
)
|
||||
|
||||
context.data["invalid_knobs"] = invalid_knobs
|
||||
return invalid_knobs
|
||||
|
|
@ -74,4 +74,5 @@ class CreateImage(api.Creator):
|
|||
groups.append(group)
|
||||
|
||||
for group in groups:
|
||||
self.data.update({"subset": "image" + group.Name})
|
||||
photoshop.imprint(group, self.data)
|
||||
|
|
|
|||
36
pype/plugins/photoshop/publish/collect_review.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import os
|
||||
|
||||
import pythoncom
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectReview(pyblish.api.ContextPlugin):
|
||||
"""Gather the active document as review instance."""
|
||||
|
||||
label = "Review"
|
||||
order = pyblish.api.CollectorOrder
|
||||
hosts = ["photoshop"]
|
||||
|
||||
def process(self, context):
|
||||
# Necessary call when running in a different thread which pyblish-qml
|
||||
# can be.
|
||||
pythoncom.CoInitialize()
|
||||
|
||||
family = "review"
|
||||
task = os.getenv("AVALON_TASK", None)
|
||||
subset = family + task.capitalize()
|
||||
|
||||
file_path = context.data["currentFile"]
|
||||
base_name = os.path.basename(file_path)
|
||||
|
||||
instance = context.create_instance(subset)
|
||||
instance.data.update({
|
||||
"subset": subset,
|
||||
"label": base_name,
|
||||
"name": base_name,
|
||||
"family": family,
|
||||
"families": ["ftrack"],
|
||||
"representations": [],
|
||||
"asset": os.environ["AVALON_ASSET"]
|
||||
})
|
||||
105
pype/plugins/photoshop/publish/extract_review.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import os
|
||||
|
||||
import pype.api
|
||||
import pype.lib
|
||||
from avalon import photoshop
|
||||
|
||||
|
||||
class ExtractReview(pype.api.Extractor):
|
||||
"""Produce a flattened image file from all instances."""
|
||||
|
||||
label = "Extract Review"
|
||||
hosts = ["photoshop"]
|
||||
families = ["review"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
staging_dir = self.staging_dir(instance)
|
||||
self.log.info("Outputting image to {}".format(staging_dir))
|
||||
|
||||
layers = []
|
||||
for image_instance in instance.context:
|
||||
if image_instance.data["family"] != "image":
|
||||
continue
|
||||
layers.append(image_instance[0])
|
||||
|
||||
# Perform extraction
|
||||
output_image = "{} copy.jpg".format(
|
||||
os.path.splitext(photoshop.app().ActiveDocument.Name)[0]
|
||||
)
|
||||
with photoshop.maintained_visibility():
|
||||
# Hide all other layers.
|
||||
extract_ids = [
|
||||
x.id for x in photoshop.get_layers_in_layers(layers)
|
||||
]
|
||||
for layer in photoshop.get_layers_in_document():
|
||||
if layer.id in extract_ids:
|
||||
layer.Visible = True
|
||||
else:
|
||||
layer.Visible = False
|
||||
|
||||
photoshop.app().ActiveDocument.SaveAs(
|
||||
staging_dir, photoshop.com_objects.JPEGSaveOptions(), True
|
||||
)
|
||||
|
||||
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
|
||||
|
||||
instance.data["representations"].append({
|
||||
"name": "jpg",
|
||||
"ext": "jpg",
|
||||
"files": output_image,
|
||||
"stagingDir": staging_dir
|
||||
})
|
||||
instance.data["stagingDir"] = staging_dir
|
||||
|
||||
# Generate thumbnail.
|
||||
thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg")
|
||||
args = [
|
||||
ffmpeg_path, "-y",
|
||||
"-i", os.path.join(staging_dir, output_image),
|
||||
"-vf", "scale=300:-1",
|
||||
"-vframes", "1",
|
||||
thumbnail_path
|
||||
]
|
||||
output = pype.lib._subprocess(args)
|
||||
|
||||
self.log.debug(output)
|
||||
|
||||
instance.data["representations"].append({
|
||||
"name": "thumbnail",
|
||||
"ext": "jpg",
|
||||
"files": os.path.basename(thumbnail_path),
|
||||
"stagingDir": staging_dir,
|
||||
"tags": ["thumbnail"]
|
||||
})
|
||||
|
||||
# Generate mov.
|
||||
mov_path = os.path.join(staging_dir, "review.mov")
|
||||
args = [
|
||||
ffmpeg_path, "-y",
|
||||
"-i", os.path.join(staging_dir, output_image),
|
||||
"-vframes", "1",
|
||||
mov_path
|
||||
]
|
||||
output = pype.lib._subprocess(args)
|
||||
|
||||
self.log.debug(output)
|
||||
|
||||
instance.data["representations"].append({
|
||||
"name": "mov",
|
||||
"ext": "mov",
|
||||
"files": os.path.basename(mov_path),
|
||||
"stagingDir": staging_dir,
|
||||
"frameStart": 1,
|
||||
"frameEnd": 1,
|
||||
"fps": 25,
|
||||
"preview": True,
|
||||
"tags": ["review", "ftrackreview"]
|
||||
})
|
||||
|
||||
# Required for extract_review plugin (L222 onwards).
|
||||
instance.data["frameStart"] = 1
|
||||
instance.data["frameEnd"] = 1
|
||||
instance.data["fps"] = 25
|
||||
|
||||
self.log.info(f"Extracted {instance} to {staging_dir}")
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import pyblish.api
|
||||
import pype.api
|
||||
from avalon import photoshop
|
||||
|
||||
|
||||
class ValidateNamingRepair(pyblish.api.Action):
|
||||
|
|
@ -22,7 +23,11 @@ class ValidateNamingRepair(pyblish.api.Action):
|
|||
instances = pyblish.api.instances_by_plugin(failed, plugin)
|
||||
|
||||
for instance in instances:
|
||||
instance[0].Name = instance.data["name"].replace(" ", "_")
|
||||
name = instance.data["name"].replace(" ", "_")
|
||||
instance[0].Name = name
|
||||
data = photoshop.read(instance[0])
|
||||
data["subset"] = "image" + name
|
||||
photoshop.imprint(instance[0], data)
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -42,3 +47,6 @@ class ValidateNaming(pyblish.api.InstancePlugin):
|
|||
def process(self, instance):
|
||||
msg = "Name \"{}\" is not allowed.".format(instance.data["name"])
|
||||
assert " " not in instance.data["name"], msg
|
||||
|
||||
msg = "Subset \"{}\" is not allowed.".format(instance.data["subset"])
|
||||
assert " " not in instance.data["subset"], msg
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class CollectFrameranges(pyblish.api.InstancePlugin):
|
|||
"""
|
||||
|
||||
label = "Collect Clip Frameranges"
|
||||
order = pyblish.api.CollectorOrder
|
||||
order = pyblish.api.CollectorOrder - 0.01
|
||||
families = ['clip']
|
||||
|
||||
def process(self, instance):
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class CollectClipRepresentations(pyblish.api.InstancePlugin):
|
|||
"""
|
||||
|
||||
label = "Collect Clip Representations"
|
||||
order = pyblish.api.CollectorOrder
|
||||
order = pyblish.api.CollectorOrder + 0.1
|
||||
families = ['clip']
|
||||
|
||||
def process(self, instance):
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
import sys
|
||||
import pyblish.api
|
||||
import pype.api
|
||||
import avalon.api
|
||||
import six
|
||||
|
||||
|
||||
class ValidateAutoSyncOff(pyblish.api.ContextPlugin):
|
||||
"""Ensure that autosync value in ftrack project is set to False.
|
||||
|
||||
In case was set to True and event server with the sync to avalon event
|
||||
is running will cause integration to avalon will be override.
|
||||
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ['clip']
|
||||
label = 'Ftrack project\'s auto sync off'
|
||||
actions = [pype.api.RepairAction]
|
||||
|
||||
def process(self, context):
|
||||
session = context.data["ftrackSession"]
|
||||
project_name = avalon.api.Session["AVALON_PROJECT"]
|
||||
query = 'Project where full_name is "{}"'.format(project_name)
|
||||
project = session.query(query).one()
|
||||
invalid = self.get_invalid(context)
|
||||
|
||||
assert not invalid, (
|
||||
"Ftrack Project has 'Auto sync' set to On."
|
||||
" That may cause issues during integration."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_invalid(context):
|
||||
session = context.data["ftrackSession"]
|
||||
project_name = avalon.api.Session["AVALON_PROJECT"]
|
||||
query = 'Project where full_name is "{}"'.format(project_name)
|
||||
project = session.query(query).one()
|
||||
|
||||
invalid = None
|
||||
|
||||
if project.get('custom_attributes', {}).get(
|
||||
'avalon_auto_sync', False):
|
||||
invalid = project
|
||||
|
||||
return invalid
|
||||
|
||||
@classmethod
|
||||
def repair(cls, context):
|
||||
session = context.data["ftrackSession"]
|
||||
invalid = cls.get_invalid(context)
|
||||
invalid['custom_attributes']['avalon_auto_sync'] = False
|
||||
try:
|
||||
session.commit()
|
||||
except Exception:
|
||||
tp, value, tb = sys.exc_info()
|
||||
session.rollback()
|
||||
six.reraise(tp, value, tb)
|
||||
|
|
@ -56,12 +56,18 @@ class CollectShots(pyblish.api.InstancePlugin):
|
|||
asset_entity = instance.context.data["assetEntity"]
|
||||
asset_name = asset_entity["name"]
|
||||
|
||||
# Ask user for sequence start. Usually 10:00:00:00.
|
||||
sequence_start_frame = 900000
|
||||
|
||||
# Project specific prefix naming. This needs to be replaced with some
|
||||
# options to be more flexible.
|
||||
asset_name = asset_name.split("_")[0]
|
||||
|
||||
instances = []
|
||||
for track in tracks:
|
||||
track_start_frame = (
|
||||
abs(track.source_range.start_time.value) - sequence_start_frame
|
||||
)
|
||||
for child in track.each_child():
|
||||
|
||||
# Transitions are ignored, because Clips have the full frame
|
||||
|
|
@ -69,12 +75,17 @@ class CollectShots(pyblish.api.InstancePlugin):
|
|||
if isinstance(child, otio.schema.transition.Transition):
|
||||
continue
|
||||
|
||||
if child.name is None:
|
||||
continue
|
||||
|
||||
# Hardcoded to expect a shot name of "[name].[extension]"
|
||||
child_name = os.path.splitext(child.name)[0].lower()
|
||||
name = f"{asset_name}_{child_name}"
|
||||
|
||||
frame_start = child.range_in_parent().start_time.value
|
||||
frame_end = child.range_in_parent().end_time_inclusive().value
|
||||
frame_start = track_start_frame
|
||||
frame_start += child.range_in_parent().start_time.value
|
||||
frame_end = track_start_frame
|
||||
frame_end += child.range_in_parent().end_time_inclusive().value
|
||||
|
||||
label = f"{name} (framerange: {frame_start}-{frame_end})"
|
||||
instances.append(
|
||||
|
|
|
|||
|
|
@ -14,3 +14,25 @@ def get_resource(*args):
|
|||
*args
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def pype_icon_filepath(debug=None):
|
||||
if debug is None:
|
||||
debug = bool(os.getenv("PYPE_DEV"))
|
||||
|
||||
if debug:
|
||||
icon_file_name = "pype_icon_dev.png"
|
||||
else:
|
||||
icon_file_name = "pype_icon.png"
|
||||
return get_resource("icons", icon_file_name)
|
||||
|
||||
|
||||
def pype_splash_filepath(debug=None):
|
||||
if debug is None:
|
||||
debug = bool(os.getenv("PYPE_DEV"))
|
||||
|
||||
if debug:
|
||||
splash_file_name = "pype_splash_dev.png"
|
||||
else:
|
||||
splash_file_name = "pype_splash.png"
|
||||
return get_resource("icons", splash_file_name)
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 257 KiB After Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 247 KiB After Width: | Height: | Size: 247 KiB |