Merge branch 'release/2.11.0'

This commit is contained in:
Milan Kolar 2020-07-27 13:15:15 +02:00
commit 062d40681d
148 changed files with 2972 additions and 1230 deletions

25
.gitignore vendored
View file

@ -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

View file

@ -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

View file

@ -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*",

View file

@ -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"]

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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

View file

@ -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,

View file

@ -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():

View file

@ -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."""

View file

@ -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;

View file

@ -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)

View file

@ -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"

View file

@ -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()

View 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/"

View file

@ -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()

View 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()

View file

@ -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:]))

View file

@ -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")

View file

@ -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")

View file

@ -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)

View file

@ -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(

View file

@ -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 {

View file

@ -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:]))

View file

@ -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)

View file

@ -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"])

View file

@ -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)

View file

@ -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)

View file

@ -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__()

View file

@ -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

View file

@ -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 = []

View file

@ -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()

View file

@ -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"]

View file

@ -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(

View file

@ -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(

View file

@ -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()

View file

@ -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(

View file

@ -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()

View file

@ -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):

View file

@ -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']:

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View 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

View file

@ -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)

View file

@ -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':

View 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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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):

View file

@ -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}`")

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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)

View file

@ -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]

View file

@ -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);

View 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)

View file

@ -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"

View 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
)
)

View 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]

View file

@ -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,

View 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

View file

@ -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(

View file

@ -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))

View file

@ -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 = {

View file

@ -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

View file

@ -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)

View 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"]
})

View 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}")

View file

@ -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

View file

@ -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):

View file

@ -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):

View file

@ -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)

View file

@ -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(

View file

@ -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)

View file

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 257 KiB

After

Width:  |  Height:  |  Size: 257 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 247 KiB

Before After
Before After

Some files were not shown because too many files have changed in this diff Show more