mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch '2.x/develop' into feature/maya-tile-rendering
This commit is contained in:
commit
3040eed159
37 changed files with 1101 additions and 724 deletions
|
|
@ -40,7 +40,8 @@ from .lib import (
|
|||
get_version_from_path,
|
||||
get_last_version_from_path,
|
||||
modified_environ,
|
||||
add_tool_to_environment
|
||||
add_tool_to_environment,
|
||||
get_latest_version
|
||||
)
|
||||
|
||||
# Special naming case for subprocess since its a built-in method.
|
||||
|
|
@ -85,5 +86,6 @@ __all__ = [
|
|||
"modified_environ",
|
||||
"add_tool_to_environment",
|
||||
|
||||
"subprocess"
|
||||
"subprocess",
|
||||
"get_latest_version"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -151,27 +151,31 @@ def application_launch():
|
|||
def export_template(backdrops, nodes, filepath):
|
||||
func = """function func(args)
|
||||
{
|
||||
// Add an extra node just so a new group can be created.
|
||||
|
||||
var temp_node = node.add("Top", "temp_note", "NOTE", 0, 0, 0);
|
||||
var template_group = node.createGroup(temp_node, "temp_group");
|
||||
node.deleteNode( template_group + "/temp_note" );
|
||||
|
||||
// This will make Node View to focus on the new group.
|
||||
selection.clearSelection();
|
||||
for (var f = 0; f < args[1].length; f++)
|
||||
{
|
||||
selection.addNodeToSelection(args[1][f]);
|
||||
}
|
||||
|
||||
Action.perform("copy()", "Node View");
|
||||
|
||||
selection.clearSelection();
|
||||
selection.addNodeToSelection(template_group);
|
||||
Action.perform("onActionEnterGroup()", "Node View");
|
||||
Action.perform("paste()", "Node View");
|
||||
|
||||
// Recreate backdrops in group.
|
||||
for (var i = 0 ; i < args[0].length; i++)
|
||||
{
|
||||
MessageLog.trace(args[0][i]);
|
||||
Backdrop.addBackdrop(template_group, args[0][i]);
|
||||
};
|
||||
|
||||
// Copy-paste the selected nodes into the new group.
|
||||
var drag_object = copyPaste.copy(args[1], 1, frame.numberOf, "");
|
||||
copyPaste.pasteNewNodes(drag_object, template_group, "");
|
||||
|
||||
// Select all nodes within group and export as template.
|
||||
Action.perform( "selectAll()", "Node View" );
|
||||
copyPaste.createTemplateFromSelection(args[2], args[3]);
|
||||
|
||||
|
|
|
|||
|
|
@ -158,6 +158,25 @@ class AExpectedFiles:
|
|||
"""To be implemented by renderer class."""
|
||||
pass
|
||||
|
||||
def sanitize_camera_name(self, camera):
|
||||
"""Sanitize camera name.
|
||||
|
||||
Remove Maya illegal characters from camera name.
|
||||
|
||||
Args:
|
||||
camera (str): Maya camera name.
|
||||
|
||||
Returns:
|
||||
(str): sanitized camera name
|
||||
|
||||
Example:
|
||||
>>> sanizite_camera_name('test:camera_01')
|
||||
test_camera_01
|
||||
|
||||
"""
|
||||
sanitized = re.sub('[^0-9a-zA-Z_]+', '_', camera)
|
||||
return sanitized
|
||||
|
||||
def get_renderer_prefix(self):
|
||||
"""Return prefix for specific renderer.
|
||||
|
||||
|
|
@ -252,7 +271,7 @@ class AExpectedFiles:
|
|||
mappings = (
|
||||
(R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]),
|
||||
(R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]),
|
||||
(R_SUBSTITUTE_CAMERA_TOKEN, cam),
|
||||
(R_SUBSTITUTE_CAMERA_TOKEN, self.sanitize_camera_name(cam)),
|
||||
# this is required to remove unfilled aov token, for example
|
||||
# in Redshift
|
||||
(R_REMOVE_AOV_TOKEN, ""),
|
||||
|
|
@ -287,7 +306,8 @@ class AExpectedFiles:
|
|||
mappings = (
|
||||
(R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]),
|
||||
(R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]),
|
||||
(R_SUBSTITUTE_CAMERA_TOKEN, cam),
|
||||
(R_SUBSTITUTE_CAMERA_TOKEN,
|
||||
self.sanitize_camera_name(cam)),
|
||||
(R_SUBSTITUTE_AOV_TOKEN, aov[0]),
|
||||
(R_CLEAN_FRAME_TOKEN, ""),
|
||||
(R_CLEAN_EXT_TOKEN, ""),
|
||||
|
|
@ -314,7 +334,8 @@ class AExpectedFiles:
|
|||
# camera name to AOV to allow per camera AOVs.
|
||||
aov_name = aov[0]
|
||||
if len(layer_data["cameras"]) > 1:
|
||||
aov_name = "{}_{}".format(aov[0], cam)
|
||||
aov_name = "{}_{}".format(aov[0],
|
||||
self.sanitize_camera_name(cam))
|
||||
|
||||
aov_file_list[aov_name] = aov_files
|
||||
file_prefix = layer_data["filePrefix"]
|
||||
|
|
|
|||
|
|
@ -1445,7 +1445,7 @@ class ExporterReview:
|
|||
anlib.reset_selection()
|
||||
ipn_orig = None
|
||||
for v in [n for n in nuke.allNodes()
|
||||
if "Viewer" in n.Class()]:
|
||||
if "Viewer" == n.Class()]:
|
||||
ip = v['input_process'].getValue()
|
||||
ipn = v['input_process_node'].getValue()
|
||||
if "VIEWER_INPUT" not in ipn and ip:
|
||||
|
|
|
|||
262
pype/hosts/nukestudio/tags.json
Normal file
262
pype/hosts/nukestudio/tags.json
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
{
|
||||
"Hierarchy": {
|
||||
"editable": "1",
|
||||
"note": "{folder}/{sequence}/{shot}",
|
||||
"icon": {
|
||||
"path": "hierarchy.png"
|
||||
},
|
||||
"metadata": {
|
||||
"folder": "FOLDER_NAME",
|
||||
"shot": "{clip}",
|
||||
"track": "{track}",
|
||||
"sequence": "{sequence}",
|
||||
"episode": "EPISODE_NAME",
|
||||
"root": "{projectroot}"
|
||||
}
|
||||
},
|
||||
"Source Resolution": {
|
||||
"editable": "1",
|
||||
"note": "Use source resolution",
|
||||
"icon": {
|
||||
"path": "resolution.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "resolution"
|
||||
}
|
||||
},
|
||||
"Retiming": {
|
||||
"editable": "1",
|
||||
"note": "Clip has retime or TimeWarp effects (or multiple effects stacked on the clip)",
|
||||
"icon": {
|
||||
"path": "retiming.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "retiming",
|
||||
"marginIn": 1,
|
||||
"marginOut": 1
|
||||
}
|
||||
},
|
||||
"Frame start": {
|
||||
"editable": "1",
|
||||
"note": "Starting frame for comps. \n\n> Use `value` and add either number or write `source` (if you want to preserve source frame numbering)",
|
||||
"icon": {
|
||||
"path": "icons:TagBackground.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "frameStart",
|
||||
"value": "1001"
|
||||
}
|
||||
},
|
||||
"[Lenses]": {
|
||||
"Set lense here": {
|
||||
"editable": "1",
|
||||
"note": "Adjust parameters of your lense and then drop to clip. Remember! You can always overwrite on clip",
|
||||
"icon": {
|
||||
"path": "lense.png"
|
||||
},
|
||||
"metadata": {
|
||||
"focalLengthMm": 57
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
"[Subsets]": {
|
||||
"Audio": {
|
||||
"editable": "1",
|
||||
"note": "Export with Audio",
|
||||
"icon": {
|
||||
"path": "volume.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "audio",
|
||||
"subset": "main"
|
||||
}
|
||||
},
|
||||
"plateFg": {
|
||||
"editable": "1",
|
||||
"note": "Add to publish to \"forground\" subset. Change metadata subset name if different order number",
|
||||
"icon": {
|
||||
"path": "z_layer_fg.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "plate",
|
||||
"subset": "Fg01"
|
||||
}
|
||||
},
|
||||
"plateBg": {
|
||||
"editable": "1",
|
||||
"note": "Add to publish to \"background\" subset. Change metadata subset name if different order number",
|
||||
"icon": {
|
||||
"path": "z_layer_bg.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "plate",
|
||||
"subset": "Bg01"
|
||||
}
|
||||
},
|
||||
"plateRef": {
|
||||
"editable": "1",
|
||||
"note": "Add to publish to \"reference\" subset.",
|
||||
"icon": {
|
||||
"path": "icons:Reference.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "plate",
|
||||
"subset": "Ref"
|
||||
}
|
||||
},
|
||||
"plateMain": {
|
||||
"editable": "1",
|
||||
"note": "Add to publish to \"main\" subset.",
|
||||
"icon": {
|
||||
"path": "z_layer_main.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "plate",
|
||||
"subset": "main"
|
||||
}
|
||||
},
|
||||
"plateProxy": {
|
||||
"editable": "1",
|
||||
"note": "Add to publish to \"proxy\" subset.",
|
||||
"icon": {
|
||||
"path": "z_layer_main.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "plate",
|
||||
"subset": "proxy"
|
||||
}
|
||||
},
|
||||
"review": {
|
||||
"editable": "1",
|
||||
"note": "Upload to Ftrack as review component.",
|
||||
"icon": {
|
||||
"path": "review.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "review",
|
||||
"track": "review"
|
||||
}
|
||||
}
|
||||
},
|
||||
"[Handles]": {
|
||||
"start: add 20 frames": {
|
||||
"editable": "1",
|
||||
"note": "Adding frames to start of selected clip",
|
||||
"icon": {
|
||||
"path": "3_add_handles_start.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "handles",
|
||||
"value": "20",
|
||||
"args": "{'op':'add','where':'start'}"
|
||||
}
|
||||
},
|
||||
"start: add 10 frames": {
|
||||
"editable": "1",
|
||||
"note": "Adding frames to start of selected clip",
|
||||
"icon": {
|
||||
"path": "3_add_handles_start.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "handles",
|
||||
"value": "10",
|
||||
"args": "{'op':'add','where':'start'}"
|
||||
}
|
||||
},
|
||||
"start: add 5 frames": {
|
||||
"editable": "1",
|
||||
"note": "Adding frames to start of selected clip",
|
||||
"icon": {
|
||||
"path": "3_add_handles_start.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "handles",
|
||||
"value": "5",
|
||||
"args": "{'op':'add','where':'start'}"
|
||||
}
|
||||
},
|
||||
"start: add 0 frames": {
|
||||
"editable": "1",
|
||||
"note": "Adding frames to start of selected clip",
|
||||
"icon": {
|
||||
"path": "3_add_handles_start.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "handles",
|
||||
"value": "0",
|
||||
"args": "{'op':'add','where':'start'}"
|
||||
}
|
||||
},
|
||||
"end: add 20 frames": {
|
||||
"editable": "1",
|
||||
"note": "Adding frames to end of selected clip",
|
||||
"icon": {
|
||||
"path": "1_add_handles_end.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "handles",
|
||||
"value": "20",
|
||||
"args": "{'op':'add','where':'end'}"
|
||||
}
|
||||
},
|
||||
"end: add 10 frames": {
|
||||
"editable": "1",
|
||||
"note": "Adding frames to end of selected clip",
|
||||
"icon": {
|
||||
"path": "1_add_handles_end.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "handles",
|
||||
"value": "10",
|
||||
"args": "{'op':'add','where':'end'}"
|
||||
}
|
||||
},
|
||||
"end: add 5 frames": {
|
||||
"editable": "1",
|
||||
"note": "Adding frames to end of selected clip",
|
||||
"icon": {
|
||||
"path": "1_add_handles_end.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "handles",
|
||||
"value": "5",
|
||||
"args": "{'op':'add','where':'end'}"
|
||||
}
|
||||
},
|
||||
"end: add 0 frames": {
|
||||
"editable": "1",
|
||||
"note": "Adding frames to end of selected clip",
|
||||
"icon": {
|
||||
"path": "1_add_handles_end.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "handles",
|
||||
"value": "0",
|
||||
"args": "{'op':'add','where':'end'}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"NukeScript": {
|
||||
"editable": "1",
|
||||
"note": "Collecting track items to Nuke scripts.",
|
||||
"icon": {
|
||||
"path": "icons:TagNuke.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "nukescript",
|
||||
"subset": "main"
|
||||
}
|
||||
},
|
||||
"Comment": {
|
||||
"editable": "1",
|
||||
"note": "Comment on a shot.",
|
||||
"icon": {
|
||||
"path": "icons:TagComment.png"
|
||||
},
|
||||
"metadata": {
|
||||
"family": "comment",
|
||||
"subset": "main"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,22 @@
|
|||
import re
|
||||
import os
|
||||
import json
|
||||
import hiero
|
||||
|
||||
from pype.api import (
|
||||
config,
|
||||
Logger
|
||||
)
|
||||
from pype.api import Logger
|
||||
from avalon import io
|
||||
|
||||
log = Logger().get_logger(__name__, "nukestudio")
|
||||
|
||||
|
||||
def tag_data():
|
||||
current_dir = os.path.dirname(__file__)
|
||||
json_path = os.path.join(current_dir, "tags.json")
|
||||
with open(json_path, "r") as json_stream:
|
||||
data = json.load(json_stream)
|
||||
return data
|
||||
|
||||
|
||||
def create_tag(key, value):
|
||||
"""
|
||||
Creating Tag object.
|
||||
|
|
@ -58,13 +64,9 @@ def add_tags_from_presets():
|
|||
return
|
||||
|
||||
log.debug("Setting default tags on project: {}".format(project.name()))
|
||||
|
||||
# get all presets
|
||||
presets = config.get_presets()
|
||||
|
||||
# get nukestudio tag.json from presets
|
||||
nks_pres = presets["nukestudio"]
|
||||
nks_pres_tags = nks_pres.get("tags", None)
|
||||
# get nukestudio tags.json
|
||||
nks_pres_tags = tag_data()
|
||||
|
||||
# Get project task types.
|
||||
tasks = io.find_one({"type": "project"})["config"]["tasks"]
|
||||
|
|
|
|||
37
pype/lib.py
37
pype/lib.py
|
|
@ -1379,3 +1379,40 @@ def ffprobe_streams(path_to_file):
|
|||
popen_output = popen.communicate()[0]
|
||||
log.debug("FFprobe output: {}".format(popen_output))
|
||||
return json.loads(popen_output)["streams"]
|
||||
|
||||
|
||||
def get_latest_version(asset_name, subset_name):
|
||||
"""Retrieve latest version from `asset_name`, and `subset_name`.
|
||||
|
||||
Args:
|
||||
asset_name (str): Name of asset.
|
||||
subset_name (str): Name of subset.
|
||||
"""
|
||||
# Get asset
|
||||
asset_name = io.find_one(
|
||||
{"type": "asset", "name": asset_name}, projection={"name": True}
|
||||
)
|
||||
|
||||
subset = io.find_one(
|
||||
{"type": "subset", "name": subset_name, "parent": asset_name["_id"]},
|
||||
projection={"_id": True, "name": True},
|
||||
)
|
||||
|
||||
# Check if subsets actually exists.
|
||||
assert subset, "No subsets found."
|
||||
|
||||
# Get version
|
||||
version_projection = {
|
||||
"name": True,
|
||||
"parent": True,
|
||||
}
|
||||
|
||||
version = io.find_one(
|
||||
{"type": "version", "parent": subset["_id"]},
|
||||
projection=version_projection,
|
||||
sort=[("name", -1)],
|
||||
)
|
||||
|
||||
assert version, "No version found, this is a bug"
|
||||
|
||||
return version
|
||||
|
|
|
|||
|
|
@ -105,11 +105,34 @@ class DeleteOldVersions(BaseAction):
|
|||
"value": False
|
||||
})
|
||||
|
||||
items.append(self.splitter_item)
|
||||
|
||||
items.append({
|
||||
"type": "label",
|
||||
"value": (
|
||||
"<i>This will <b>NOT</b> delete any files and only return the "
|
||||
"total size of the files.</i>"
|
||||
)
|
||||
})
|
||||
items.append({
|
||||
"type": "boolean",
|
||||
"name": "only_calculate",
|
||||
"label": "Only calculate size of files.",
|
||||
"value": False
|
||||
})
|
||||
|
||||
return {
|
||||
"items": items,
|
||||
"title": self.inteface_title
|
||||
}
|
||||
|
||||
def sizeof_fmt(self, num, suffix='B'):
|
||||
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
||||
if abs(num) < 1024.0:
|
||||
return "%3.1f%s%s" % (num, unit, suffix)
|
||||
num /= 1024.0
|
||||
return "%.1f%s%s" % (num, 'Yi', suffix)
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
values = event["data"].get("values")
|
||||
if not values:
|
||||
|
|
@ -117,6 +140,7 @@ class DeleteOldVersions(BaseAction):
|
|||
|
||||
versions_count = int(values["last_versions_count"])
|
||||
force_to_remove = values["force_delete_publish_folder"]
|
||||
only_calculate = values["only_calculate"]
|
||||
|
||||
_val1 = "OFF"
|
||||
if force_to_remove:
|
||||
|
|
@ -318,10 +342,29 @@ class DeleteOldVersions(BaseAction):
|
|||
"Folder does not exist. Deleting it's files skipped: {}"
|
||||
).format(paths_msg))
|
||||
|
||||
# Size of files.
|
||||
size = 0
|
||||
|
||||
if only_calculate:
|
||||
if force_to_remove:
|
||||
size = self.delete_whole_dir_paths(
|
||||
dir_paths.values(), delete=False
|
||||
)
|
||||
else:
|
||||
size = self.delete_only_repre_files(
|
||||
dir_paths, file_paths_by_dir, delete=False
|
||||
)
|
||||
|
||||
msg = "Total size of files: " + self.sizeof_fmt(size)
|
||||
|
||||
self.log.warning(msg)
|
||||
|
||||
return {"success": True, "message": msg}
|
||||
|
||||
if force_to_remove:
|
||||
self.delete_whole_dir_paths(dir_paths.values())
|
||||
size = self.delete_whole_dir_paths(dir_paths.values())
|
||||
else:
|
||||
self.delete_only_repre_files(dir_paths, file_paths_by_dir)
|
||||
size = self.delete_only_repre_files(dir_paths, file_paths_by_dir)
|
||||
|
||||
mongo_changes_bulk = []
|
||||
for version in versions:
|
||||
|
|
@ -383,17 +426,31 @@ class DeleteOldVersions(BaseAction):
|
|||
"message": msg
|
||||
}
|
||||
|
||||
return True
|
||||
msg = "Total size of files deleted: " + self.sizeof_fmt(size)
|
||||
|
||||
self.log.warning(msg)
|
||||
|
||||
return {"success": True, "message": msg}
|
||||
|
||||
def delete_whole_dir_paths(self, dir_paths, delete=True):
|
||||
size = 0
|
||||
|
||||
def delete_whole_dir_paths(self, dir_paths):
|
||||
for dir_path in dir_paths:
|
||||
# Delete all files and fodlers in dir path
|
||||
for root, dirs, files in os.walk(dir_path, topdown=False):
|
||||
for name in files:
|
||||
os.remove(os.path.join(root, name))
|
||||
file_path = os.path.join(root, name)
|
||||
size += os.path.getsize(file_path)
|
||||
if delete:
|
||||
os.remove(file_path)
|
||||
self.log.debug("Removed file: {}".format(file_path))
|
||||
|
||||
for name in dirs:
|
||||
os.rmdir(os.path.join(root, name))
|
||||
if delete:
|
||||
os.rmdir(os.path.join(root, name))
|
||||
|
||||
if not delete:
|
||||
continue
|
||||
|
||||
# Delete even the folder and it's parents folders if they are empty
|
||||
while True:
|
||||
|
|
@ -406,7 +463,11 @@ class DeleteOldVersions(BaseAction):
|
|||
|
||||
os.rmdir(os.path.join(dir_path))
|
||||
|
||||
def delete_only_repre_files(self, dir_paths, file_paths):
|
||||
return size
|
||||
|
||||
def delete_only_repre_files(self, dir_paths, file_paths, delete=True):
|
||||
size = 0
|
||||
|
||||
for dir_id, dir_path in dir_paths.items():
|
||||
dir_files = os.listdir(dir_path)
|
||||
collections, remainders = clique.assemble(dir_files)
|
||||
|
|
@ -420,8 +481,13 @@ class DeleteOldVersions(BaseAction):
|
|||
"File was not found: {}".format(file_path)
|
||||
)
|
||||
continue
|
||||
os.remove(file_path)
|
||||
self.log.debug("Removed file: {}".format(file_path))
|
||||
|
||||
size += os.path.getsize(file_path)
|
||||
|
||||
if delete:
|
||||
os.remove(file_path)
|
||||
self.log.debug("Removed file: {}".format(file_path))
|
||||
|
||||
remainders.remove(file_path_base)
|
||||
continue
|
||||
|
||||
|
|
@ -440,21 +506,34 @@ class DeleteOldVersions(BaseAction):
|
|||
final_col.head = os.path.join(dir_path, final_col.head)
|
||||
for _file_path in final_col:
|
||||
if os.path.exists(_file_path):
|
||||
os.remove(_file_path)
|
||||
|
||||
size += os.path.getsize(_file_path)
|
||||
|
||||
if delete:
|
||||
os.remove(_file_path)
|
||||
self.log.debug(
|
||||
"Removed file: {}".format(_file_path)
|
||||
)
|
||||
|
||||
_seq_path = final_col.format("{head}{padding}{tail}")
|
||||
self.log.debug("Removed files: {}".format(_seq_path))
|
||||
collections.remove(final_col)
|
||||
|
||||
elif os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
self.log.debug("Removed file: {}".format(file_path))
|
||||
size += os.path.getsize(file_path)
|
||||
|
||||
if delete:
|
||||
os.remove(file_path)
|
||||
self.log.debug("Removed file: {}".format(file_path))
|
||||
else:
|
||||
self.log.warning(
|
||||
"File was not found: {}".format(file_path)
|
||||
)
|
||||
|
||||
# Delete as much as possible parent folders
|
||||
if not delete:
|
||||
return size
|
||||
|
||||
for dir_path in dir_paths.values():
|
||||
while True:
|
||||
if not os.path.exists(dir_path):
|
||||
|
|
@ -467,6 +546,8 @@ class DeleteOldVersions(BaseAction):
|
|||
self.log.debug("Removed folder: {}".format(dir_path))
|
||||
os.rmdir(dir_path)
|
||||
|
||||
return size
|
||||
|
||||
def path_from_represenation(self, representation, anatomy):
|
||||
try:
|
||||
template = representation["data"]["template"]
|
||||
|
|
|
|||
|
|
@ -81,13 +81,15 @@ class Delivery(BaseAction):
|
|||
anatomy = Anatomy(project_name)
|
||||
new_anatomies = []
|
||||
first = None
|
||||
for key in (anatomy.templates.get("delivery") or {}):
|
||||
new_anatomies.append({
|
||||
"label": key,
|
||||
"value": key
|
||||
})
|
||||
if first is None:
|
||||
first = key
|
||||
for key, template in (anatomy.templates.get("delivery") or {}).items():
|
||||
# Use only keys with `{root}` or `{root[*]}` in value
|
||||
if isinstance(template, str) and "{root" in template:
|
||||
new_anatomies.append({
|
||||
"label": key,
|
||||
"value": key
|
||||
})
|
||||
if first is None:
|
||||
first = key
|
||||
|
||||
skipped = False
|
||||
# Add message if there are any common components
|
||||
|
|
@ -293,6 +295,20 @@ class Delivery(BaseAction):
|
|||
repres_to_deliver.append(repre)
|
||||
|
||||
anatomy = Anatomy(project_name)
|
||||
|
||||
format_dict = {}
|
||||
if location_path:
|
||||
location_path = location_path.replace("\\", "/")
|
||||
root_names = anatomy.root_names_from_templates(
|
||||
anatomy.templates["delivery"]
|
||||
)
|
||||
if root_names is None:
|
||||
format_dict["root"] = location_path
|
||||
else:
|
||||
format_dict["root"] = {}
|
||||
for name in root_names:
|
||||
format_dict["root"][name] = location_path
|
||||
|
||||
for repre in repres_to_deliver:
|
||||
# Get destination repre path
|
||||
anatomy_data = copy.deepcopy(repre["context"])
|
||||
|
|
@ -339,25 +355,33 @@ class Delivery(BaseAction):
|
|||
repre_path = self.path_from_represenation(repre, anatomy)
|
||||
# TODO add backup solution where root of path from component
|
||||
# is repalced with root
|
||||
if not frame:
|
||||
self.process_single_file(
|
||||
repre_path, anatomy, anatomy_name, anatomy_data
|
||||
)
|
||||
args = (
|
||||
repre_path,
|
||||
anatomy,
|
||||
anatomy_name,
|
||||
anatomy_data,
|
||||
format_dict
|
||||
)
|
||||
|
||||
if not frame:
|
||||
self.process_single_file(*args)
|
||||
else:
|
||||
self.process_sequence(
|
||||
repre_path, anatomy, anatomy_name, anatomy_data
|
||||
)
|
||||
self.process_sequence(*args)
|
||||
|
||||
self.db_con.uninstall()
|
||||
|
||||
return self.report()
|
||||
|
||||
def process_single_file(
|
||||
self, repre_path, anatomy, anatomy_name, anatomy_data
|
||||
self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict
|
||||
):
|
||||
anatomy_filled = anatomy.format(anatomy_data)
|
||||
delivery_path = anatomy_filled["delivery"][anatomy_name]
|
||||
if format_dict:
|
||||
template_result = anatomy_filled["delivery"][anatomy_name]
|
||||
delivery_path = template_result.rootless.format(**format_dict)
|
||||
else:
|
||||
delivery_path = anatomy_filled["delivery"][anatomy_name]
|
||||
|
||||
delivery_folder = os.path.dirname(delivery_path)
|
||||
if not os.path.exists(delivery_folder):
|
||||
os.makedirs(delivery_folder)
|
||||
|
|
@ -365,7 +389,7 @@ class Delivery(BaseAction):
|
|||
self.copy_file(repre_path, delivery_path)
|
||||
|
||||
def process_sequence(
|
||||
self, repre_path, anatomy, anatomy_name, anatomy_data
|
||||
self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict
|
||||
):
|
||||
dir_path, file_name = os.path.split(str(repre_path))
|
||||
|
||||
|
|
@ -408,8 +432,12 @@ class Delivery(BaseAction):
|
|||
anatomy_data["frame"] = frame_indicator
|
||||
anatomy_filled = anatomy.format(anatomy_data)
|
||||
|
||||
delivery_path = anatomy_filled["delivery"][anatomy_name]
|
||||
print(delivery_path)
|
||||
if format_dict:
|
||||
template_result = anatomy_filled["delivery"][anatomy_name]
|
||||
delivery_path = template_result.rootless.format(**format_dict)
|
||||
else:
|
||||
delivery_path = anatomy_filled["delivery"][anatomy_name]
|
||||
|
||||
delivery_folder = os.path.dirname(delivery_path)
|
||||
dst_head, dst_tail = delivery_path.split(frame_indicator)
|
||||
dst_padding = src_collection.padding
|
||||
|
|
|
|||
|
|
@ -81,11 +81,12 @@ def check_regex(name, entity_type, in_schema=None, schema_patterns=None):
|
|||
def get_pype_attr(session, split_hierarchical=True):
|
||||
custom_attributes = []
|
||||
hier_custom_attributes = []
|
||||
# TODO remove deprecated "avalon" group from query
|
||||
cust_attrs_query = (
|
||||
"select id, entity_type, object_type_id, is_hierarchical, default"
|
||||
" from CustomAttributeConfiguration"
|
||||
" where group.name = \"{}\""
|
||||
).format(CUST_ATTR_GROUP)
|
||||
" where group.name in (\"avalon\", \"pype\")"
|
||||
)
|
||||
all_avalon_attr = session.query(cust_attrs_query).all()
|
||||
for cust_attr in all_avalon_attr:
|
||||
if split_hierarchical and cust_attr["is_hierarchical"]:
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ class LogsWindow(QtWidgets.QWidget):
|
|||
super(LogsWindow, self).__init__(parent)
|
||||
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
self.resize(1200, 800)
|
||||
logs_widget = LogsWidget(parent=self)
|
||||
self.resize(1400, 800)
|
||||
log_detail = OutputWidget(parent=self)
|
||||
logs_widget = LogsWidget(log_detail, parent=self)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout()
|
||||
|
||||
|
|
@ -18,8 +18,6 @@ class LogsWindow(QtWidgets.QWidget):
|
|||
log_splitter.setOrientation(QtCore.Qt.Horizontal)
|
||||
log_splitter.addWidget(logs_widget)
|
||||
log_splitter.addWidget(log_detail)
|
||||
log_splitter.setStretchFactor(0, 65)
|
||||
log_splitter.setStretchFactor(1, 35)
|
||||
|
||||
main_layout.addWidget(log_splitter)
|
||||
|
||||
|
|
@ -28,10 +26,3 @@ class LogsWindow(QtWidgets.QWidget):
|
|||
|
||||
self.setLayout(main_layout)
|
||||
self.setWindowTitle("Logs")
|
||||
|
||||
self.logs_widget.active_changed.connect(self.on_selection_changed)
|
||||
|
||||
def on_selection_changed(self):
|
||||
index = self.logs_widget.selected_log()
|
||||
node = index.data(self.logs_widget.model.NodeRole)
|
||||
self.log_detail.set_detail(node)
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
import contextlib
|
||||
from Qt import QtCore
|
||||
|
||||
|
||||
def _iter_model_rows(
|
||||
model, column, include_root=False
|
||||
):
|
||||
"""Iterate over all row indices in a model"""
|
||||
indices = [QtCore.QModelIndex()] # start iteration at root
|
||||
|
||||
for index in indices:
|
||||
# Add children to the iterations
|
||||
child_rows = model.rowCount(index)
|
||||
for child_row in range(child_rows):
|
||||
child_index = model.index(child_row, column, index)
|
||||
indices.append(child_index)
|
||||
|
||||
if not include_root and not index.isValid():
|
||||
continue
|
||||
|
||||
yield index
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def preserve_states(
|
||||
tree_view, column=0, role=None,
|
||||
preserve_expanded=True, preserve_selection=True,
|
||||
expanded_role=QtCore.Qt.DisplayRole, selection_role=QtCore.Qt.DisplayRole
|
||||
|
||||
):
|
||||
"""Preserves row selection in QTreeView by column's data role.
|
||||
|
||||
This function is created to maintain the selection status of
|
||||
the model items. When refresh is triggered the items which are expanded
|
||||
will stay expanded and vise versa.
|
||||
|
||||
tree_view (QWidgets.QTreeView): the tree view nested in the application
|
||||
column (int): the column to retrieve the data from
|
||||
role (int): the role which dictates what will be returned
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
# When `role` is set then override both expanded and selection roles
|
||||
if role:
|
||||
expanded_role = role
|
||||
selection_role = role
|
||||
|
||||
model = tree_view.model()
|
||||
selection_model = tree_view.selectionModel()
|
||||
flags = selection_model.Select | selection_model.Rows
|
||||
|
||||
expanded = set()
|
||||
|
||||
if preserve_expanded:
|
||||
for index in _iter_model_rows(
|
||||
model, column=column, include_root=False
|
||||
):
|
||||
if tree_view.isExpanded(index):
|
||||
value = index.data(expanded_role)
|
||||
expanded.add(value)
|
||||
|
||||
selected = None
|
||||
|
||||
if preserve_selection:
|
||||
selected_rows = selection_model.selectedRows()
|
||||
if selected_rows:
|
||||
selected = set(row.data(selection_role) for row in selected_rows)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if expanded:
|
||||
for index in _iter_model_rows(
|
||||
model, column=0, include_root=False
|
||||
):
|
||||
value = index.data(expanded_role)
|
||||
is_expanded = value in expanded
|
||||
# skip if new index was created meanwhile
|
||||
if is_expanded is None:
|
||||
continue
|
||||
tree_view.setExpanded(index, is_expanded)
|
||||
|
||||
if selected:
|
||||
# Go through all indices, select the ones with similar data
|
||||
for index in _iter_model_rows(
|
||||
model, column=column, include_root=False
|
||||
):
|
||||
value = index.data(selection_role)
|
||||
state = value in selected
|
||||
if state:
|
||||
tree_view.scrollTo(index) # Ensure item is visible
|
||||
selection_model.select(index, flags)
|
||||
|
|
@ -1,21 +1,20 @@
|
|||
import collections
|
||||
from Qt import QtCore
|
||||
from Qt import QtCore, QtGui
|
||||
from pype.api import Logger
|
||||
from pypeapp.lib.log import _bootstrap_mongo_log, LOG_COLLECTION_NAME
|
||||
|
||||
log = Logger().get_logger("LogModel", "LoggingModule")
|
||||
|
||||
|
||||
class LogModel(QtCore.QAbstractItemModel):
|
||||
COLUMNS = [
|
||||
class LogModel(QtGui.QStandardItemModel):
|
||||
COLUMNS = (
|
||||
"process_name",
|
||||
"hostname",
|
||||
"hostip",
|
||||
"username",
|
||||
"system_name",
|
||||
"started"
|
||||
]
|
||||
|
||||
)
|
||||
colums_mapping = {
|
||||
"process_name": "Process Name",
|
||||
"process_id": "Process Id",
|
||||
|
|
@ -25,30 +24,53 @@ class LogModel(QtCore.QAbstractItemModel):
|
|||
"system_name": "System name",
|
||||
"started": "Started at"
|
||||
}
|
||||
process_keys = [
|
||||
process_keys = (
|
||||
"process_id", "hostname", "hostip",
|
||||
"username", "system_name", "process_name"
|
||||
]
|
||||
log_keys = [
|
||||
)
|
||||
log_keys = (
|
||||
"timestamp", "level", "thread", "threadName", "message", "loggerName",
|
||||
"fileName", "module", "method", "lineNumber"
|
||||
]
|
||||
)
|
||||
default_value = "- Not set -"
|
||||
NodeRole = QtCore.Qt.UserRole + 1
|
||||
|
||||
ROLE_LOGS = QtCore.Qt.UserRole + 2
|
||||
ROLE_PROCESS_ID = QtCore.Qt.UserRole + 3
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(LogModel, self).__init__(parent)
|
||||
self._root_node = Node()
|
||||
|
||||
self.log_by_process = None
|
||||
self.dbcon = None
|
||||
|
||||
# Crash if connection is not possible to skip this module
|
||||
database = _bootstrap_mongo_log()
|
||||
if LOG_COLLECTION_NAME in database.list_collection_names():
|
||||
self.dbcon = database[LOG_COLLECTION_NAME]
|
||||
|
||||
def add_log(self, log):
|
||||
node = Node(log)
|
||||
self._root_node.add_child(node)
|
||||
def headerData(self, section, orientation, role):
|
||||
if (
|
||||
role == QtCore.Qt.DisplayRole
|
||||
and orientation == QtCore.Qt.Horizontal
|
||||
):
|
||||
if section < len(self.COLUMNS):
|
||||
key = self.COLUMNS[section]
|
||||
return self.colums_mapping.get(key, key)
|
||||
|
||||
super(LogModel, self).headerData(section, orientation, role)
|
||||
|
||||
def add_process_logs(self, process_logs):
|
||||
items = []
|
||||
first_item = True
|
||||
for key in self.COLUMNS:
|
||||
display_value = str(process_logs[key])
|
||||
item = QtGui.QStandardItem(display_value)
|
||||
if first_item:
|
||||
first_item = False
|
||||
item.setData(process_logs["_logs"], self.ROLE_LOGS)
|
||||
item.setData(process_logs["process_id"], self.ROLE_PROCESS_ID)
|
||||
items.append(item)
|
||||
self.appendRow(items)
|
||||
|
||||
def refresh(self):
|
||||
self.log_by_process = collections.defaultdict(list)
|
||||
|
|
@ -65,16 +87,13 @@ class LogModel(QtCore.QAbstractItemModel):
|
|||
continue
|
||||
|
||||
if process_id not in self.process_info:
|
||||
proc_dict = {}
|
||||
proc_dict = {"_logs": []}
|
||||
for key in self.process_keys:
|
||||
proc_dict[key] = (
|
||||
item.get(key) or self.default_value
|
||||
)
|
||||
self.process_info[process_id] = proc_dict
|
||||
|
||||
if "_logs" not in self.process_info[process_id]:
|
||||
self.process_info[process_id]["_logs"] = []
|
||||
|
||||
log_item = {}
|
||||
for key in self.log_keys:
|
||||
log_item[key] = item.get(key) or self.default_value
|
||||
|
|
@ -89,114 +108,29 @@ class LogModel(QtCore.QAbstractItemModel):
|
|||
item["_logs"], key=lambda item: item["timestamp"]
|
||||
)
|
||||
item["started"] = item["_logs"][0]["timestamp"]
|
||||
self.add_log(item)
|
||||
self.add_process_logs(item)
|
||||
|
||||
self.endResetModel()
|
||||
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
return None
|
||||
|
||||
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
|
||||
node = index.internalPointer()
|
||||
column = index.column()
|
||||
class LogsFilterProxy(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LogsFilterProxy, self).__init__(*args, **kwargs)
|
||||
self.col_usernames = None
|
||||
self.filter_usernames = set()
|
||||
|
||||
key = self.COLUMNS[column]
|
||||
if key == "started":
|
||||
return str(node.get(key, None))
|
||||
return node.get(key, None)
|
||||
def update_users_filter(self, users):
|
||||
self.filter_usernames = set()
|
||||
for user in users or tuple():
|
||||
self.filter_usernames.add(user)
|
||||
self.invalidateFilter()
|
||||
|
||||
if role == self.NodeRole:
|
||||
return index.internalPointer()
|
||||
|
||||
def index(self, row, column, parent):
|
||||
"""Return index for row/column under parent"""
|
||||
|
||||
if not parent.isValid():
|
||||
parent_node = self._root_node
|
||||
else:
|
||||
parent_node = parent.internalPointer()
|
||||
|
||||
child_item = parent_node.child(row)
|
||||
if child_item:
|
||||
return self.createIndex(row, column, child_item)
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
def rowCount(self, parent):
|
||||
node = self._root_node
|
||||
if parent.isValid():
|
||||
node = parent.internalPointer()
|
||||
return node.childCount()
|
||||
|
||||
def columnCount(self, parent):
|
||||
return len(self.COLUMNS)
|
||||
|
||||
def parent(self, index):
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
if section < len(self.COLUMNS):
|
||||
key = self.COLUMNS[section]
|
||||
return self.colums_mapping.get(key, key)
|
||||
|
||||
super(LogModel, self).headerData(section, orientation, role)
|
||||
|
||||
def flags(self, index):
|
||||
return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable)
|
||||
|
||||
def clear(self):
|
||||
self.beginResetModel()
|
||||
self._root_node = Node()
|
||||
self.endResetModel()
|
||||
|
||||
|
||||
class Node(dict):
|
||||
"""A node that can be represented in a tree view.
|
||||
|
||||
The node can store data just like a dictionary.
|
||||
|
||||
>>> data = {"name": "John", "score": 10}
|
||||
>>> node = Node(data)
|
||||
>>> assert node["name"] == "John"
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, data=None):
|
||||
super(Node, self).__init__()
|
||||
|
||||
self._children = list()
|
||||
self._parent = None
|
||||
|
||||
if data is not None:
|
||||
assert isinstance(data, dict)
|
||||
self.update(data)
|
||||
|
||||
def childCount(self):
|
||||
return len(self._children)
|
||||
|
||||
def child(self, row):
|
||||
if row >= len(self._children):
|
||||
log.warning("Invalid row as child: {0}".format(row))
|
||||
return
|
||||
|
||||
return self._children[row]
|
||||
|
||||
def children(self):
|
||||
return self._children
|
||||
|
||||
def parent(self):
|
||||
return self._parent
|
||||
|
||||
def row(self):
|
||||
"""
|
||||
Returns:
|
||||
int: Index of this node under parent"""
|
||||
if self._parent is not None:
|
||||
siblings = self.parent().children()
|
||||
return siblings.index(self)
|
||||
|
||||
def add_child(self, child):
|
||||
"""Add a child to this node"""
|
||||
child._parent = self
|
||||
self._children.append(child)
|
||||
def filterAcceptsRow(self, source_row, source_parent):
|
||||
if self.col_usernames is not None:
|
||||
index = self.sourceModel().index(
|
||||
source_row, self.col_usernames, source_parent
|
||||
)
|
||||
user = index.data(QtCore.Qt.DisplayRole)
|
||||
if user not in self.filter_usernames:
|
||||
return False
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from Qt import QtCore, QtWidgets, QtGui
|
||||
from PyQt5.QtCore import QVariant
|
||||
from .models import LogModel
|
||||
from Qt import QtCore, QtWidgets
|
||||
from avalon.vendor import qtawesome
|
||||
from .models import LogModel, LogsFilterProxy
|
||||
|
||||
|
||||
class SearchComboBox(QtWidgets.QComboBox):
|
||||
|
|
@ -50,37 +50,6 @@ class SearchComboBox(QtWidgets.QComboBox):
|
|||
return text
|
||||
|
||||
|
||||
class CheckableComboBox2(QtWidgets.QComboBox):
|
||||
def __init__(self, parent=None):
|
||||
super(CheckableComboBox, self).__init__(parent)
|
||||
self.view().pressed.connect(self.handleItemPressed)
|
||||
self._changed = False
|
||||
|
||||
def handleItemPressed(self, index):
|
||||
item = self.model().itemFromIndex(index)
|
||||
if item.checkState() == QtCore.Qt.Checked:
|
||||
item.setCheckState(QtCore.Qt.Unchecked)
|
||||
else:
|
||||
item.setCheckState(QtCore.Qt.Checked)
|
||||
self._changed = True
|
||||
|
||||
def hidePopup(self):
|
||||
if not self._changed:
|
||||
super(CheckableComboBox, self).hidePopup()
|
||||
self._changed = False
|
||||
|
||||
def itemChecked(self, index):
|
||||
item = self.model().item(index, self.modelColumn())
|
||||
return item.checkState() == QtCore.Qt.Checked
|
||||
|
||||
def setItemChecked(self, index, checked=True):
|
||||
item = self.model().item(index, self.modelColumn())
|
||||
if checked:
|
||||
item.setCheckState(QtCore.Qt.Checked)
|
||||
else:
|
||||
item.setCheckState(QtCore.Qt.Unchecked)
|
||||
|
||||
|
||||
class SelectableMenu(QtWidgets.QMenu):
|
||||
|
||||
selection_changed = QtCore.Signal()
|
||||
|
|
@ -137,144 +106,108 @@ class CustomCombo(QtWidgets.QWidget):
|
|||
yield action
|
||||
|
||||
|
||||
class CheckableComboBox(QtWidgets.QComboBox):
|
||||
def __init__(self, parent=None):
|
||||
super(CheckableComboBox, self).__init__(parent)
|
||||
|
||||
view = QtWidgets.QTreeView()
|
||||
view.header().hide()
|
||||
view.setRootIsDecorated(False)
|
||||
|
||||
model = QtGui.QStandardItemModel()
|
||||
|
||||
view.pressed.connect(self.handleItemPressed)
|
||||
self._changed = False
|
||||
|
||||
self.setView(view)
|
||||
self.setModel(model)
|
||||
|
||||
self.view = view
|
||||
self.model = model
|
||||
|
||||
def handleItemPressed(self, index):
|
||||
item = self.model.itemFromIndex(index)
|
||||
if item.checkState() == QtCore.Qt.Checked:
|
||||
item.setCheckState(QtCore.Qt.Unchecked)
|
||||
else:
|
||||
item.setCheckState(QtCore.Qt.Checked)
|
||||
self._changed = True
|
||||
|
||||
def hidePopup(self):
|
||||
if not self._changed:
|
||||
super(CheckableComboBox, self).hidePopup()
|
||||
self._changed = False
|
||||
|
||||
def itemChecked(self, index):
|
||||
item = self.model.item(index, self.modelColumn())
|
||||
return item.checkState() == QtCore.Qt.Checked
|
||||
|
||||
def setItemChecked(self, index, checked=True):
|
||||
item = self.model.item(index, self.modelColumn())
|
||||
if checked:
|
||||
item.setCheckState(QtCore.Qt.Checked)
|
||||
else:
|
||||
item.setCheckState(QtCore.Qt.Unchecked)
|
||||
|
||||
def addItems(self, items):
|
||||
for text, checked in items:
|
||||
text_item = QtGui.QStandardItem(text)
|
||||
checked_item = QtGui.QStandardItem()
|
||||
checked_item.setData(QVariant(checked), QtCore.Qt.CheckStateRole)
|
||||
self.model.appendRow([text_item, checked_item])
|
||||
|
||||
|
||||
class LogsWidget(QtWidgets.QWidget):
|
||||
"""A widget that lists the published subsets for an asset"""
|
||||
|
||||
active_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, detail_widget, parent=None):
|
||||
super(LogsWidget, self).__init__(parent=parent)
|
||||
|
||||
model = LogModel()
|
||||
proxy_model = LogsFilterProxy()
|
||||
proxy_model.setSourceModel(model)
|
||||
proxy_model.col_usernames = model.COLUMNS.index("username")
|
||||
|
||||
filter_layout = QtWidgets.QHBoxLayout()
|
||||
|
||||
# user_filter = SearchComboBox(self, "Users")
|
||||
user_filter = CustomCombo("Users", self)
|
||||
users = model.dbcon.distinct("user")
|
||||
users = model.dbcon.distinct("username")
|
||||
user_filter.populate(users)
|
||||
user_filter.selection_changed.connect(self.user_changed)
|
||||
user_filter.selection_changed.connect(self._user_changed)
|
||||
|
||||
proxy_model.update_users_filter(users)
|
||||
|
||||
level_filter = CustomCombo("Levels", self)
|
||||
# levels = [(level, True) for level in model.dbcon.distinct("level")]
|
||||
levels = model.dbcon.distinct("level")
|
||||
level_filter.addItems(levels)
|
||||
level_filter.selection_changed.connect(self._level_changed)
|
||||
|
||||
date_from_label = QtWidgets.QLabel("From:")
|
||||
date_filter_from = QtWidgets.QDateTimeEdit()
|
||||
detail_widget.update_level_filter(levels)
|
||||
|
||||
date_from_layout = QtWidgets.QVBoxLayout()
|
||||
date_from_layout.addWidget(date_from_label)
|
||||
date_from_layout.addWidget(date_filter_from)
|
||||
spacer = QtWidgets.QWidget()
|
||||
|
||||
# now = datetime.datetime.now()
|
||||
# QtCore.QDateTime(
|
||||
# now.year,
|
||||
# now.month,
|
||||
# now.day,
|
||||
# now.hour,
|
||||
# now.minute,
|
||||
# second=0,
|
||||
# msec=0,
|
||||
# timeSpec=0
|
||||
# )
|
||||
date_to_label = QtWidgets.QLabel("To:")
|
||||
date_filter_to = QtWidgets.QDateTimeEdit()
|
||||
|
||||
date_to_layout = QtWidgets.QVBoxLayout()
|
||||
date_to_layout.addWidget(date_to_label)
|
||||
date_to_layout.addWidget(date_filter_to)
|
||||
icon = qtawesome.icon("fa.refresh", color="white")
|
||||
refresh_btn = QtWidgets.QPushButton(icon, "")
|
||||
|
||||
filter_layout.addWidget(user_filter)
|
||||
filter_layout.addWidget(level_filter)
|
||||
|
||||
filter_layout.addLayout(date_from_layout)
|
||||
filter_layout.addLayout(date_to_layout)
|
||||
filter_layout.addWidget(spacer, 1)
|
||||
filter_layout.addWidget(refresh_btn)
|
||||
|
||||
view = QtWidgets.QTreeView(self)
|
||||
view.setAllColumnsShowFocus(True)
|
||||
view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addLayout(filter_layout)
|
||||
layout.addWidget(view)
|
||||
|
||||
view.setModel(proxy_model)
|
||||
|
||||
view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
view.setSortingEnabled(True)
|
||||
view.sortByColumn(
|
||||
model.COLUMNS.index("started"),
|
||||
QtCore.Qt.AscendingOrder
|
||||
QtCore.Qt.DescendingOrder
|
||||
)
|
||||
|
||||
view.setModel(model)
|
||||
view.pressed.connect(self._on_activated)
|
||||
# prepare
|
||||
model.refresh()
|
||||
view.selectionModel().selectionChanged.connect(self._on_index_change)
|
||||
refresh_btn.clicked.connect(self._on_refresh_clicked)
|
||||
|
||||
# Store to memory
|
||||
self.model = model
|
||||
self.proxy_model = proxy_model
|
||||
self.view = view
|
||||
|
||||
self.user_filter = user_filter
|
||||
self.level_filter = level_filter
|
||||
|
||||
def _on_activated(self, *args, **kwargs):
|
||||
self.active_changed.emit()
|
||||
self.detail_widget = detail_widget
|
||||
self.refresh_btn = refresh_btn
|
||||
|
||||
def user_changed(self):
|
||||
# prepare
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
self.model.refresh()
|
||||
self.detail_widget.refresh()
|
||||
|
||||
def _on_refresh_clicked(self):
|
||||
self.refresh()
|
||||
|
||||
def _on_index_change(self, to_index, from_index):
|
||||
index = self._selected_log()
|
||||
if index:
|
||||
logs = index.data(self.model.ROLE_LOGS)
|
||||
else:
|
||||
logs = []
|
||||
self.detail_widget.set_detail(logs)
|
||||
|
||||
def _user_changed(self):
|
||||
checked_values = set()
|
||||
for action in self.user_filter.items():
|
||||
print(action)
|
||||
if action.isChecked():
|
||||
checked_values.add(action.text())
|
||||
self.proxy_model.update_users_filter(checked_values)
|
||||
|
||||
def _level_changed(self):
|
||||
checked_values = set()
|
||||
for action in self.level_filter.items():
|
||||
if action.isChecked():
|
||||
checked_values.add(action.text())
|
||||
self.detail_widget.update_level_filter(checked_values)
|
||||
|
||||
def on_context_menu(self, point):
|
||||
# TODO will be any actions? it's ready
|
||||
|
|
@ -288,7 +221,7 @@ class LogsWidget(QtWidgets.QWidget):
|
|||
selection = self.view.selectionModel()
|
||||
rows = selection.selectedRows(column=0)
|
||||
|
||||
def selected_log(self):
|
||||
def _selected_log(self):
|
||||
selection = self.view.selectionModel()
|
||||
rows = selection.selectedRows(column=0)
|
||||
if len(rows) == 1:
|
||||
|
|
@ -300,22 +233,55 @@ class OutputWidget(QtWidgets.QWidget):
|
|||
def __init__(self, parent=None):
|
||||
super(OutputWidget, self).__init__(parent=parent)
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
|
||||
show_timecode_checkbox = QtWidgets.QCheckBox("Show timestamp")
|
||||
|
||||
output_text = QtWidgets.QTextEdit()
|
||||
output_text.setReadOnly(True)
|
||||
# output_text.setLineWrapMode(QtWidgets.QTextEdit.FixedPixelWidth)
|
||||
|
||||
layout.addWidget(show_timecode_checkbox)
|
||||
layout.addWidget(output_text)
|
||||
|
||||
show_timecode_checkbox.stateChanged.connect(
|
||||
self.on_show_timecode_change
|
||||
)
|
||||
self.setLayout(layout)
|
||||
self.output_text = output_text
|
||||
self.show_timecode_checkbox = show_timecode_checkbox
|
||||
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
self.set_detail()
|
||||
|
||||
def show_timecode(self):
|
||||
return self.show_timecode_checkbox.isChecked()
|
||||
|
||||
def on_show_timecode_change(self):
|
||||
self.set_detail(self.las_logs)
|
||||
|
||||
def update_level_filter(self, levels):
|
||||
self.filter_levels = set()
|
||||
for level in levels or tuple():
|
||||
self.filter_levels.add(level.lower())
|
||||
|
||||
self.set_detail(self.las_logs)
|
||||
|
||||
def add_line(self, line):
|
||||
self.output_text.append(line)
|
||||
|
||||
def set_detail(self, node):
|
||||
def set_detail(self, logs=None):
|
||||
self.las_logs = logs
|
||||
self.output_text.clear()
|
||||
for log in node["_logs"]:
|
||||
if not logs:
|
||||
return
|
||||
|
||||
show_timecode = self.show_timecode()
|
||||
for log in logs:
|
||||
level = log["level"].lower()
|
||||
if level not in self.filter_levels:
|
||||
continue
|
||||
|
||||
line_f = "<font color=\"White\">{message}"
|
||||
if level == "debug":
|
||||
|
|
@ -353,66 +319,13 @@ class OutputWidget(QtWidgets.QWidget):
|
|||
|
||||
line = line_f.format(**log)
|
||||
|
||||
if show_timecode:
|
||||
timestamp = log["timestamp"]
|
||||
line = timestamp.strftime("%Y-%d-%m %H:%M:%S") + " " + line
|
||||
|
||||
self.add_line(line)
|
||||
|
||||
if not exc:
|
||||
continue
|
||||
for _line in exc["stackTrace"].split("\n"):
|
||||
self.add_line(_line)
|
||||
|
||||
|
||||
class LogDetailWidget(QtWidgets.QWidget):
|
||||
"""A Widget that display information about a specific version"""
|
||||
data_rows = [
|
||||
"user",
|
||||
"message",
|
||||
"level",
|
||||
"logname",
|
||||
"method",
|
||||
"module",
|
||||
"fileName",
|
||||
"lineNumber",
|
||||
"host",
|
||||
"timestamp"
|
||||
]
|
||||
|
||||
html_text = u"""
|
||||
<h3>{user} - {timestamp}</h3>
|
||||
<b>User</b><br>{user}<br>
|
||||
<br><b>Level</b><br>{level}<br>
|
||||
<br><b>Message</b><br>{message}<br>
|
||||
<br><b>Log Name</b><br>{logname}<br><br><b>Method</b><br>{method}<br>
|
||||
<br><b>File</b><br>{fileName}<br>
|
||||
<br><b>Line</b><br>{lineNumber}<br>
|
||||
<br><b>Host</b><br>{host}<br>
|
||||
<br><b>Timestamp</b><br>{timestamp}<br>
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(LogDetailWidget, self).__init__(parent=parent)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
|
||||
label = QtWidgets.QLabel("Detail")
|
||||
detail_widget = QtWidgets.QTextEdit()
|
||||
detail_widget.setReadOnly(True)
|
||||
layout.addWidget(label)
|
||||
layout.addWidget(detail_widget)
|
||||
|
||||
self.detail_widget = detail_widget
|
||||
|
||||
self.setEnabled(True)
|
||||
|
||||
self.set_detail(None)
|
||||
|
||||
def set_detail(self, detail_data):
|
||||
if not detail_data:
|
||||
self.detail_widget.setText("")
|
||||
return
|
||||
|
||||
data = dict()
|
||||
for row in self.data_rows:
|
||||
value = detail_data.get(row) or "< Not set >"
|
||||
data[row] = value
|
||||
|
||||
self.detail_widget.setHtml(self.html_text.format(**data))
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class CollectRenderPath(pyblish.api.InstancePlugin):
|
|||
|
||||
# Presets
|
||||
anatomy_render_key = None
|
||||
anatomy_publish_render_key = None
|
||||
publish_render_metadata = None
|
||||
|
||||
def process(self, instance):
|
||||
anatomy = instance.context.data["anatomy"]
|
||||
|
|
@ -28,7 +28,7 @@ class CollectRenderPath(pyblish.api.InstancePlugin):
|
|||
|
||||
# get anatomy rendering keys
|
||||
anatomy_render_key = self.anatomy_render_key or "render"
|
||||
anatomy_publish_render_key = self.anatomy_publish_render_key or "render"
|
||||
publish_render_metadata = self.publish_render_metadata or "render"
|
||||
|
||||
# get folder and path for rendering images from celaction
|
||||
render_dir = anatomy_filled[anatomy_render_key]["folder"]
|
||||
|
|
@ -46,8 +46,11 @@ class CollectRenderPath(pyblish.api.InstancePlugin):
|
|||
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"]
|
||||
if anatomy_filled.get(publish_render_metadata):
|
||||
instance.data["publishRenderMetadataFolder"] = anatomy_filled[
|
||||
publish_render_metadata]["folder"]
|
||||
self.log.info("Metadata render path: `{}`".format(
|
||||
instance.data["publishRenderMetadataFolder"]
|
||||
))
|
||||
|
||||
self.log.info(f"Render output path set to: `{render_path}`")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import shutil
|
||||
import re
|
||||
import pype
|
||||
import pyblish.api
|
||||
|
||||
|
||||
|
|
@ -12,57 +12,9 @@ class VersionUpScene(pyblish.api.ContextPlugin):
|
|||
|
||||
def process(self, context):
|
||||
current_file = context.data.get('currentFile')
|
||||
v_up = get_version_up(current_file)
|
||||
v_up = pype.lib.version_up(current_file)
|
||||
self.log.debug('Current file is: {}'.format(current_file))
|
||||
self.log.debug('Version up: {}'.format(v_up))
|
||||
|
||||
shutil.copy2(current_file, v_up)
|
||||
self.log.info('Scene saved into new version: {}'.format(v_up))
|
||||
|
||||
|
||||
def version_get(string, prefix, suffix=None):
|
||||
"""Extract version information from filenames used by DD (and Weta, apparently)
|
||||
These are _v# or /v# or .v# where v is a prefix string, in our case
|
||||
we use "v" for render version and "c" for camera track version.
|
||||
See the version.py and camera.py plugins for usage."""
|
||||
|
||||
if string is None:
|
||||
raise ValueError("Empty version string - no match")
|
||||
|
||||
regex = r"[/_.]{}\d+".format(prefix)
|
||||
matches = re.findall(regex, string, re.IGNORECASE)
|
||||
if not len(matches):
|
||||
msg = f"No `_{prefix}#` found in `{string}`"
|
||||
raise ValueError(msg)
|
||||
return (matches[-1:][0][1], re.search(r"\d+", matches[-1:][0]).group())
|
||||
|
||||
|
||||
def version_set(string, prefix, oldintval, newintval):
|
||||
"""Changes version information from filenames used by DD (and Weta, apparently)
|
||||
These are _v# or /v# or .v# where v is a prefix string, in our case
|
||||
we use "v" for render version and "c" for camera track version.
|
||||
See the version.py and camera.py plugins for usage."""
|
||||
|
||||
regex = r"[/_.]{}\d+".format(prefix)
|
||||
matches = re.findall(regex, string, re.IGNORECASE)
|
||||
if not len(matches):
|
||||
return ""
|
||||
|
||||
# Filter to retain only version strings with matching numbers
|
||||
matches = filter(lambda s: int(s[2:]) == oldintval, matches)
|
||||
|
||||
# Replace all version strings with matching numbers
|
||||
for match in matches:
|
||||
# use expression instead of expr so 0 prefix does not make octal
|
||||
fmt = "%%(#)0%dd" % (len(match) - 2)
|
||||
newfullvalue = match[0] + prefix + str(fmt % {"#": newintval})
|
||||
string = re.sub(match, newfullvalue, string)
|
||||
return string
|
||||
|
||||
|
||||
def get_version_up(path):
|
||||
""" Returns the next version of the path """
|
||||
|
||||
(prefix, v) = version_get(path, 'v')
|
||||
v = int(v)
|
||||
return version_set(path, prefix, v, v + 1)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Cleanup leftover files from publish."""
|
||||
import os
|
||||
import shutil
|
||||
import pyblish.api
|
||||
|
||||
|
||||
def clean_renders(instance):
|
||||
transfers = instance.data.get("transfers", list())
|
||||
"""Delete renders after publishing.
|
||||
|
||||
Args:
|
||||
instance (pyblish.api.Instace): Instance to work on.
|
||||
|
||||
"""
|
||||
transfers = instance.data.get("transfers", list())
|
||||
current_families = instance.data.get("families", list())
|
||||
instance_family = instance.data.get("family", None)
|
||||
dirnames = []
|
||||
|
|
@ -40,6 +47,7 @@ class CleanUp(pyblish.api.InstancePlugin):
|
|||
active = True
|
||||
|
||||
def process(self, instance):
|
||||
"""Plugin entry point."""
|
||||
# Get the errored instances
|
||||
failed = []
|
||||
for result in instance.context.data["results"]:
|
||||
|
|
@ -52,7 +60,7 @@ class CleanUp(pyblish.api.InstancePlugin):
|
|||
)
|
||||
)
|
||||
|
||||
self.log.info("Cleaning renders ...")
|
||||
self.log.info("Performing cleanup on {}".format(instance))
|
||||
clean_renders(instance)
|
||||
|
||||
if [ef for ef in self.exclude_families
|
||||
|
|
@ -60,16 +68,21 @@ class CleanUp(pyblish.api.InstancePlugin):
|
|||
return
|
||||
import tempfile
|
||||
|
||||
temp_root = tempfile.gettempdir()
|
||||
staging_dir = instance.data.get("stagingDir", None)
|
||||
if not staging_dir or not os.path.exists(staging_dir):
|
||||
self.log.info("No staging directory found: %s" % staging_dir)
|
||||
|
||||
if not staging_dir:
|
||||
self.log.info("Staging dir not set.")
|
||||
return
|
||||
|
||||
temp_root = tempfile.gettempdir()
|
||||
if not os.path.normpath(staging_dir).startswith(temp_root):
|
||||
self.log.info("Skipping cleanup. Staging directory is not in the "
|
||||
"temp folder: %s" % staging_dir)
|
||||
return
|
||||
|
||||
self.log.info("Removing staging directory ...")
|
||||
if not os.path.exists(staging_dir):
|
||||
self.log.info("No staging directory found: %s" % staging_dir)
|
||||
return
|
||||
|
||||
self.log.info("Removing staging directory {}".format(staging_dir))
|
||||
shutil.rmtree(staging_dir)
|
||||
|
|
|
|||
|
|
@ -206,7 +206,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
def _create_metadata_path(self, instance):
|
||||
ins_data = instance.data
|
||||
# Ensure output dir exists
|
||||
output_dir = ins_data.get("publishRenderFolder", ins_data["outputDir"])
|
||||
output_dir = ins_data.get(
|
||||
"publishRenderMetadataFolder", ins_data["outputDir"])
|
||||
|
||||
try:
|
||||
if not os.path.isdir(output_dir):
|
||||
|
|
|
|||
133
pype/plugins/global/publish/validate_instance_in_context.py
Normal file
133
pype/plugins/global/publish/validate_instance_in_context.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validate if instance asset is the same as context asset."""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import pyblish.api
|
||||
import pype.api
|
||||
|
||||
|
||||
class SelectInvalidInstances(pyblish.api.Action):
|
||||
"""Select invalid instances in Outliner."""
|
||||
|
||||
label = "Select Instances"
|
||||
icon = "briefcase"
|
||||
on = "failed"
|
||||
|
||||
def process(self, context, plugin):
|
||||
"""Process invalid validators and select invalid instances."""
|
||||
# Get the errored instances
|
||||
failed = []
|
||||
for result in context.data["results"]:
|
||||
if result["error"] is None:
|
||||
continue
|
||||
if result["instance"] is None:
|
||||
continue
|
||||
if result["instance"] in failed:
|
||||
continue
|
||||
if result["plugin"] != plugin:
|
||||
continue
|
||||
|
||||
failed.append(result["instance"])
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(failed, plugin)
|
||||
|
||||
if instances:
|
||||
self.log.info(
|
||||
"Selecting invalid nodes: %s" % ", ".join(
|
||||
[str(x) for x in instances]
|
||||
)
|
||||
)
|
||||
self.select(instances)
|
||||
else:
|
||||
self.log.info("No invalid nodes found.")
|
||||
self.deselect()
|
||||
|
||||
def select(self, instances):
|
||||
if "nuke" in pyblish.api.registered_hosts():
|
||||
import avalon.nuke.lib
|
||||
import nuke
|
||||
avalon.nuke.lib.select_nodes(
|
||||
[nuke.toNode(str(x)) for x in instances]
|
||||
)
|
||||
|
||||
if "maya" in pyblish.api.registered_hosts():
|
||||
from maya import cmds
|
||||
cmds.select(instances, replace=True, noExpand=True)
|
||||
|
||||
def deselect(self):
|
||||
if "nuke" in pyblish.api.registered_hosts():
|
||||
import avalon.nuke.lib
|
||||
avalon.nuke.lib.reset_selection()
|
||||
|
||||
if "maya" in pyblish.api.registered_hosts():
|
||||
from maya import cmds
|
||||
cmds.select(deselect=True)
|
||||
|
||||
|
||||
class RepairSelectInvalidInstances(pyblish.api.Action):
|
||||
"""Repair the instance asset."""
|
||||
|
||||
label = "Repair"
|
||||
icon = "wrench"
|
||||
on = "failed"
|
||||
|
||||
def process(self, context, plugin):
|
||||
# Get the errored instances
|
||||
failed = []
|
||||
for result in context.data["results"]:
|
||||
if result["error"] is None:
|
||||
continue
|
||||
if result["instance"] is None:
|
||||
continue
|
||||
if result["instance"] in failed:
|
||||
continue
|
||||
if result["plugin"] != plugin:
|
||||
continue
|
||||
|
||||
failed.append(result["instance"])
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(failed, plugin)
|
||||
|
||||
context_asset = context.data["assetEntity"]["name"]
|
||||
for instance in instances:
|
||||
self.set_attribute(instance, context_asset)
|
||||
|
||||
def set_attribute(self, instance, context_asset):
|
||||
if "nuke" in pyblish.api.registered_hosts():
|
||||
import nuke
|
||||
nuke.toNode(
|
||||
instance.data.get("name")
|
||||
)["avalon:asset"].setValue(context_asset)
|
||||
|
||||
if "maya" in pyblish.api.registered_hosts():
|
||||
from maya import cmds
|
||||
cmds.setAttr(
|
||||
instance.data.get("name") + ".asset",
|
||||
context_asset,
|
||||
type="string"
|
||||
)
|
||||
|
||||
|
||||
class ValidateInstanceInContext(pyblish.api.InstancePlugin):
|
||||
"""Validator to check if instance asset match context asset.
|
||||
|
||||
When working in per-shot style you always publish data in context of
|
||||
current asset (shot). This validator checks if this is so. It is optional
|
||||
so it can be disabled when needed.
|
||||
|
||||
Action on this validator will select invalid instances in Outliner.
|
||||
"""
|
||||
|
||||
order = pype.api.ValidateContentsOrder
|
||||
label = "Instance in same Context"
|
||||
optional = True
|
||||
hosts = ["maya", "nuke"]
|
||||
actions = [SelectInvalidInstances, RepairSelectInvalidInstances]
|
||||
|
||||
def process(self, instance):
|
||||
asset = instance.data.get("asset")
|
||||
context_asset = instance.context.data["assetEntity"]["name"]
|
||||
msg = "{} has asset {}".format(instance.name, asset)
|
||||
assert asset == context_asset, msg
|
||||
|
|
@ -72,19 +72,27 @@ class ExtractRender(pyblish.api.InstancePlugin):
|
|||
self.log.info(output.decode("utf-8"))
|
||||
|
||||
# Collect rendered files.
|
||||
self.log.debug(path)
|
||||
files = os.listdir(path)
|
||||
self.log.debug(files)
|
||||
collections, remainder = clique.assemble(files, minimum_items=1)
|
||||
assert not remainder, (
|
||||
"There should not be a remainder for {0}: {1}".format(
|
||||
instance[0], remainder
|
||||
)
|
||||
)
|
||||
assert len(collections) == 1, (
|
||||
"There should only be one image sequence in {}. Found: {}".format(
|
||||
path, len(collections)
|
||||
)
|
||||
)
|
||||
collection = collections[0]
|
||||
self.log.debug(collections)
|
||||
if len(collections) > 1:
|
||||
for col in collections:
|
||||
if len(list(col)) > 1:
|
||||
collection = col
|
||||
else:
|
||||
# assert len(collections) == 1, (
|
||||
# "There should only be one image sequence in {}. Found: {}".format(
|
||||
# path, len(collections)
|
||||
# )
|
||||
# )
|
||||
collection = collections[0]
|
||||
|
||||
# Generate thumbnail.
|
||||
thumbnail_path = os.path.join(path, "thumbnail.png")
|
||||
|
|
|
|||
|
|
@ -28,8 +28,11 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin):
|
|||
hosts = ["harmony"]
|
||||
actions = [ValidateSceneSettingsRepair]
|
||||
|
||||
frame_check_filter = ["_ch_", "_pr_", "_intd_", "_extd_"]
|
||||
|
||||
def process(self, instance):
|
||||
expected_settings = pype.hosts.harmony.get_asset_settings()
|
||||
self.log.info(expected_settings)
|
||||
|
||||
# Harmony is expected to start at 1.
|
||||
frame_start = expected_settings["frameStart"]
|
||||
|
|
@ -37,6 +40,14 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin):
|
|||
expected_settings["frameEnd"] = frame_end - frame_start + 1
|
||||
expected_settings["frameStart"] = 1
|
||||
|
||||
|
||||
|
||||
self.log.info(instance.context.data['anatomyData']['asset'])
|
||||
|
||||
if any(string in instance.context.data['anatomyData']['asset']
|
||||
for string in frame_check_filter):
|
||||
expected_settings.pop("frameEnd")
|
||||
|
||||
func = """function func()
|
||||
{
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,25 @@
|
|||
from avalon import api
|
||||
import maya.app.renderSetup.model.renderSetup as renderSetup
|
||||
from avalon.maya import lib
|
||||
from maya import cmds
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Load and update RenderSetup settings.
|
||||
|
||||
Working with RenderSetup setting is Maya is done utilizing json files.
|
||||
When this json is loaded, it will overwrite all settings on RenderSetup
|
||||
instance.
|
||||
"""
|
||||
|
||||
import json
|
||||
import six
|
||||
import sys
|
||||
|
||||
from avalon import api
|
||||
from avalon.maya import lib
|
||||
from pype.hosts.maya import lib as pypelib
|
||||
|
||||
from maya import cmds
|
||||
import maya.app.renderSetup.model.renderSetup as renderSetup
|
||||
|
||||
|
||||
class RenderSetupLoader(api.Loader):
|
||||
"""
|
||||
This will load json preset for RenderSetup, overwriting current one.
|
||||
"""
|
||||
"""Load json preset for RenderSetup overwriting current one."""
|
||||
|
||||
families = ["rendersetup"]
|
||||
representations = ["json"]
|
||||
|
|
@ -19,7 +30,7 @@ class RenderSetupLoader(api.Loader):
|
|||
color = "orange"
|
||||
|
||||
def load(self, context, name, namespace, data):
|
||||
|
||||
"""Load RenderSetup settings."""
|
||||
from avalon.maya.pipeline import containerise
|
||||
# from pype.hosts.maya.lib import namespaced
|
||||
|
||||
|
|
@ -29,7 +40,7 @@ class RenderSetupLoader(api.Loader):
|
|||
prefix="_" if asset[0].isdigit() else "",
|
||||
suffix="_",
|
||||
)
|
||||
|
||||
self.log.info(">>> loading json [ {} ]".format(self.fname))
|
||||
with open(self.fname, "r") as file:
|
||||
renderSetup.instance().decode(
|
||||
json.load(file), renderSetup.DECODE_AND_OVERWRITE, None)
|
||||
|
|
@ -42,9 +53,56 @@ class RenderSetupLoader(api.Loader):
|
|||
if not nodes:
|
||||
return
|
||||
|
||||
self.log.info(">>> containerising [ {} ]".format(name))
|
||||
return containerise(
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
nodes=nodes,
|
||||
context=context,
|
||||
loader=self.__class__.__name__)
|
||||
|
||||
def remove(self, container):
|
||||
"""Remove RenderSetup settings instance."""
|
||||
from maya import cmds
|
||||
|
||||
container_name = container["objectName"]
|
||||
|
||||
self.log.info("Removing '%s' from Maya.." % container["name"])
|
||||
|
||||
container_content = cmds.sets(container_name, query=True)
|
||||
nodes = cmds.ls(container_content, long=True)
|
||||
|
||||
nodes.append(container_name)
|
||||
|
||||
try:
|
||||
cmds.delete(nodes)
|
||||
except ValueError:
|
||||
# Already implicitly deleted by Maya upon removing reference
|
||||
pass
|
||||
|
||||
def update(self, container, representation):
|
||||
"""Update RenderSetup setting by overwriting existing settings."""
|
||||
pypelib.show_message(
|
||||
"Render setup update",
|
||||
"Render setup setting will be overwritten by new version. All "
|
||||
"setting specified by user not included in loaded version "
|
||||
"will be lost.")
|
||||
path = api.get_representation_path(representation)
|
||||
with open(path, "r") as file:
|
||||
try:
|
||||
renderSetup.instance().decode(
|
||||
json.load(file), renderSetup.DECODE_AND_OVERWRITE, None)
|
||||
except Exception:
|
||||
self.log.error("There were errors during loading")
|
||||
six.reraise(*sys.exc_info())
|
||||
|
||||
# Update metadata
|
||||
node = container["objectName"]
|
||||
cmds.setAttr("{}.representation".format(node),
|
||||
str(representation["_id"]),
|
||||
type="string")
|
||||
self.log.info("... updated")
|
||||
|
||||
def switch(self, container, representation):
|
||||
"""Switch representations."""
|
||||
self.update(container, representation)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ class ExtractCameraAlembic(pype.api.Extractor):
|
|||
label = "Camera (Alembic)"
|
||||
hosts = ["maya"]
|
||||
families = ["camera"]
|
||||
bake_attributes = []
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
|
|
@ -66,6 +67,14 @@ class ExtractCameraAlembic(pype.api.Extractor):
|
|||
|
||||
job_str += ' -file "{0}"'.format(path)
|
||||
|
||||
# bake specified attributes in preset
|
||||
assert isinstance(self.bake_attributes, (list, tuple)), (
|
||||
"Attributes to bake must be specified as a list"
|
||||
)
|
||||
for attr in self.bake_attributes:
|
||||
self.log.info("Adding {} attribute".format(attr))
|
||||
job_str += " -attr {0}".format(attr)
|
||||
|
||||
with lib.evaluation("off"):
|
||||
with avalon.maya.suspended_refresh():
|
||||
cmds.AbcExport(j=job_str, verbose=False)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Extract camera as Maya Scene."""
|
||||
import os
|
||||
|
||||
from maya import cmds
|
||||
|
|
@ -65,8 +67,8 @@ def unlock(plug):
|
|||
cmds.disconnectAttr(source, destination)
|
||||
|
||||
|
||||
class ExtractCameraMayaAscii(pype.api.Extractor):
|
||||
"""Extract a Camera as Maya Ascii.
|
||||
class ExtractCameraMayaScene(pype.api.Extractor):
|
||||
"""Extract a Camera as Maya Scene.
|
||||
|
||||
This will create a duplicate of the camera that will be baked *with*
|
||||
substeps and handles for the required frames. This temporary duplicate
|
||||
|
|
@ -81,13 +83,28 @@ class ExtractCameraMayaAscii(pype.api.Extractor):
|
|||
|
||||
"""
|
||||
|
||||
label = "Camera (Maya Ascii)"
|
||||
label = "Camera (Maya Scene)"
|
||||
hosts = ["maya"]
|
||||
families = ["camera"]
|
||||
scene_type = "ma"
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
"""Plugin entry point."""
|
||||
# get settings
|
||||
ext_mapping = instance.context.data["presets"]["maya"].get("ext_mapping") # noqa: E501
|
||||
if ext_mapping:
|
||||
self.log.info("Looking in presets for scene type ...")
|
||||
# use extension mapping for first family found
|
||||
for family in self.families:
|
||||
try:
|
||||
self.scene_type = ext_mapping[family]
|
||||
self.log.info(
|
||||
"Using {} as scene type".format(self.scene_type))
|
||||
break
|
||||
except AttributeError:
|
||||
# no preset found
|
||||
pass
|
||||
|
||||
framerange = [instance.data.get("frameStart", 1),
|
||||
instance.data.get("frameEnd", 1)]
|
||||
handles = instance.data.get("handles", 0)
|
||||
|
|
@ -95,7 +112,7 @@ class ExtractCameraMayaAscii(pype.api.Extractor):
|
|||
bake_to_worldspace = instance.data("bakeToWorldSpace", True)
|
||||
|
||||
if not bake_to_worldspace:
|
||||
self.log.warning("Camera (Maya Ascii) export only supports world"
|
||||
self.log.warning("Camera (Maya Scene) export only supports world"
|
||||
"space baked camera extractions. The disabled "
|
||||
"bake to world space is ignored...")
|
||||
|
||||
|
|
@ -115,7 +132,7 @@ class ExtractCameraMayaAscii(pype.api.Extractor):
|
|||
|
||||
# Define extract output file path
|
||||
dir_path = self.staging_dir(instance)
|
||||
filename = "{0}.ma".format(instance.name)
|
||||
filename = "{0}.{1}".format(instance.name, self.scene_type)
|
||||
path = os.path.join(dir_path, filename)
|
||||
|
||||
# Perform extraction
|
||||
|
|
@ -152,7 +169,7 @@ class ExtractCameraMayaAscii(pype.api.Extractor):
|
|||
cmds.select(baked_shapes, noExpand=True)
|
||||
cmds.file(path,
|
||||
force=True,
|
||||
typ="mayaAscii",
|
||||
typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501
|
||||
exportSelected=True,
|
||||
preserveReferences=False,
|
||||
constructionHistory=False,
|
||||
|
|
@ -164,15 +181,15 @@ class ExtractCameraMayaAscii(pype.api.Extractor):
|
|||
# Delete the baked hierarchy
|
||||
if bake_to_worldspace:
|
||||
cmds.delete(baked)
|
||||
|
||||
massage_ma_file(path)
|
||||
if self.scene_type == "ma":
|
||||
massage_ma_file(path)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'ma',
|
||||
'ext': 'ma',
|
||||
'name': self.scene_type,
|
||||
'ext': self.scene_type,
|
||||
'files': filename,
|
||||
"stagingDir": dir_path,
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Extract data as Maya scene (raw)."""
|
||||
import os
|
||||
|
||||
from maya import cmds
|
||||
|
|
@ -6,24 +8,37 @@ import avalon.maya
|
|||
import pype.api
|
||||
|
||||
|
||||
class ExtractMayaAsciiRaw(pype.api.Extractor):
|
||||
"""Extract as Maya Ascii (raw)
|
||||
class ExtractMayaSceneRaw(pype.api.Extractor):
|
||||
"""Extract as Maya Scene (raw).
|
||||
|
||||
This will preserve all references, construction history, etc.
|
||||
|
||||
"""
|
||||
|
||||
label = "Maya ASCII (Raw)"
|
||||
label = "Maya Scene (Raw)"
|
||||
hosts = ["maya"]
|
||||
families = ["mayaAscii",
|
||||
"setdress",
|
||||
"layout"]
|
||||
scene_type = "ma"
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
"""Plugin entry point."""
|
||||
ext_mapping = instance.context.data["presets"]["maya"].get("ext_mapping") # noqa: E501
|
||||
if ext_mapping:
|
||||
self.log.info("Looking in presets for scene type ...")
|
||||
# use extension mapping for first family found
|
||||
for family in self.families:
|
||||
try:
|
||||
self.scene_type = ext_mapping[family]
|
||||
self.log.info(
|
||||
"Using {} as scene type".format(self.scene_type))
|
||||
break
|
||||
except AttributeError:
|
||||
# no preset found
|
||||
pass
|
||||
# Define extract output file path
|
||||
dir_path = self.staging_dir(instance)
|
||||
filename = "{0}.ma".format(instance.name)
|
||||
filename = "{0}.{1}".format(instance.name, self.scene_type)
|
||||
path = os.path.join(dir_path, filename)
|
||||
|
||||
# Whether to include all nodes in the instance (including those from
|
||||
|
|
@ -38,12 +53,12 @@ class ExtractMayaAsciiRaw(pype.api.Extractor):
|
|||
members = instance[:]
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
self.log.info("Performing extraction ...")
|
||||
with avalon.maya.maintained_selection():
|
||||
cmds.select(members, noExpand=True)
|
||||
cmds.file(path,
|
||||
force=True,
|
||||
typ="mayaAscii",
|
||||
typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501
|
||||
exportSelected=True,
|
||||
preserveReferences=True,
|
||||
constructionHistory=True,
|
||||
|
|
@ -55,8 +70,8 @@ class ExtractMayaAsciiRaw(pype.api.Extractor):
|
|||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'ma',
|
||||
'ext': 'ma',
|
||||
'name': self.scene_type,
|
||||
'ext': self.scene_type,
|
||||
'files': filename,
|
||||
"stagingDir": dir_path
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Extract model as Maya Scene."""
|
||||
import os
|
||||
|
||||
from maya import cmds
|
||||
|
|
@ -8,7 +10,7 @@ from pype.hosts.maya import lib
|
|||
|
||||
|
||||
class ExtractModel(pype.api.Extractor):
|
||||
"""Extract as Model (Maya Ascii)
|
||||
"""Extract as Model (Maya Scene).
|
||||
|
||||
Only extracts contents based on the original "setMembers" data to ensure
|
||||
publishing the least amount of required shapes. From that it only takes
|
||||
|
|
@ -22,19 +24,33 @@ class ExtractModel(pype.api.Extractor):
|
|||
|
||||
"""
|
||||
|
||||
label = "Model (Maya ASCII)"
|
||||
label = "Model (Maya Scene)"
|
||||
hosts = ["maya"]
|
||||
families = ["model"]
|
||||
scene_type = "ma"
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
"""Plugin entry point."""
|
||||
ext_mapping = instance.context.data["presets"]["maya"].get("ext_mapping") # noqa: E501
|
||||
if ext_mapping:
|
||||
self.log.info("Looking in presets for scene type ...")
|
||||
# use extension mapping for first family found
|
||||
for family in self.families:
|
||||
try:
|
||||
self.scene_type = ext_mapping[family]
|
||||
self.log.info(
|
||||
"Using {} as scene type".format(self.scene_type))
|
||||
break
|
||||
except AttributeError:
|
||||
# no preset found
|
||||
pass
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = "{0}.ma".format(instance.name)
|
||||
filename = "{0}.{1}".format(instance.name, self.scene_type)
|
||||
path = os.path.join(stagingdir, filename)
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
self.log.info("Performing extraction ...")
|
||||
|
||||
# Get only the shape contents we need in such a way that we avoid
|
||||
# taking along intermediateObjects
|
||||
|
|
@ -59,7 +75,7 @@ class ExtractModel(pype.api.Extractor):
|
|||
cmds.select(members, noExpand=True)
|
||||
cmds.file(path,
|
||||
force=True,
|
||||
typ="mayaAscii",
|
||||
typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501
|
||||
exportSelected=True,
|
||||
preserveReferences=False,
|
||||
channels=False,
|
||||
|
|
@ -73,8 +89,8 @@ class ExtractModel(pype.api.Extractor):
|
|||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'ma',
|
||||
'ext': 'ma',
|
||||
'name': self.scene_type,
|
||||
'ext': self.scene_type,
|
||||
'files': filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Extract rig as Maya Scene."""
|
||||
import os
|
||||
|
||||
from maya import cmds
|
||||
|
|
@ -7,26 +9,40 @@ import pype.api
|
|||
|
||||
|
||||
class ExtractRig(pype.api.Extractor):
|
||||
"""Extract rig as Maya Ascii"""
|
||||
"""Extract rig as Maya Scene."""
|
||||
|
||||
label = "Extract Rig (Maya ASCII)"
|
||||
label = "Extract Rig (Maya Scene)"
|
||||
hosts = ["maya"]
|
||||
families = ["rig"]
|
||||
scene_type = "ma"
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
"""Plugin entry point."""
|
||||
ext_mapping = instance.context.data["presets"]["maya"].get("ext_mapping") # noqa: E501
|
||||
if ext_mapping:
|
||||
self.log.info("Looking in presets for scene type ...")
|
||||
# use extension mapping for first family found
|
||||
for family in self.families:
|
||||
try:
|
||||
self.scene_type = ext_mapping[family]
|
||||
self.log.info(
|
||||
"Using {} as scene type".format(self.scene_type))
|
||||
break
|
||||
except AttributeError:
|
||||
# no preset found
|
||||
pass
|
||||
# Define extract output file path
|
||||
dir_path = self.staging_dir(instance)
|
||||
filename = "{0}.ma".format(instance.name)
|
||||
filename = "{0}.{1}".format(instance.name, self.scene_type)
|
||||
path = os.path.join(dir_path, filename)
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
self.log.info("Performing extraction ...")
|
||||
with avalon.maya.maintained_selection():
|
||||
cmds.select(instance, noExpand=True)
|
||||
cmds.file(path,
|
||||
force=True,
|
||||
typ="mayaAscii",
|
||||
typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501
|
||||
exportSelected=True,
|
||||
preserveReferences=False,
|
||||
channels=True,
|
||||
|
|
@ -38,12 +54,11 @@ class ExtractRig(pype.api.Extractor):
|
|||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'ma',
|
||||
'ext': 'ma',
|
||||
'name': self.scene_type,
|
||||
'ext': self.scene_type,
|
||||
'files': filename,
|
||||
"stagingDir": dir_path
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s" % (instance.name, path))
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Extract Yeti rig."""
|
||||
|
||||
import os
|
||||
import json
|
||||
import contextlib
|
||||
|
|
@ -11,7 +14,7 @@ import pype.hosts.maya.lib as maya
|
|||
|
||||
@contextlib.contextmanager
|
||||
def disconnect_plugs(settings, members):
|
||||
|
||||
"""Disconnect and store attribute connections."""
|
||||
members = cmds.ls(members, long=True)
|
||||
original_connections = []
|
||||
try:
|
||||
|
|
@ -55,7 +58,7 @@ def disconnect_plugs(settings, members):
|
|||
|
||||
@contextlib.contextmanager
|
||||
def yetigraph_attribute_values(assumed_destination, resources):
|
||||
|
||||
"""Get values from Yeti attributes in graph."""
|
||||
try:
|
||||
for resource in resources:
|
||||
if "graphnode" not in resource:
|
||||
|
|
@ -89,14 +92,28 @@ def yetigraph_attribute_values(assumed_destination, resources):
|
|||
|
||||
|
||||
class ExtractYetiRig(pype.api.Extractor):
|
||||
"""Extract the Yeti rig to a MayaAscii and write the Yeti rig data"""
|
||||
"""Extract the Yeti rig to a Maya Scene and write the Yeti rig data."""
|
||||
|
||||
label = "Extract Yeti Rig"
|
||||
hosts = ["maya"]
|
||||
families = ["yetiRig"]
|
||||
scene_type = "ma"
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
"""Plugin entry point."""
|
||||
ext_mapping = instance.context.data["presets"]["maya"].get("ext_mapping") # noqa: E501
|
||||
if ext_mapping:
|
||||
self.log.info("Looking in presets for scene type ...")
|
||||
# use extension mapping for first family found
|
||||
for family in self.families:
|
||||
try:
|
||||
self.scene_type = ext_mapping[family]
|
||||
self.log.info(
|
||||
"Using {} as scene type".format(self.scene_type))
|
||||
break
|
||||
except AttributeError:
|
||||
# no preset found
|
||||
pass
|
||||
yeti_nodes = cmds.ls(instance, type="pgYetiMaya")
|
||||
if not yeti_nodes:
|
||||
raise RuntimeError("No pgYetiMaya nodes found in the instance")
|
||||
|
|
@ -106,7 +123,8 @@ class ExtractYetiRig(pype.api.Extractor):
|
|||
settings_path = os.path.join(dirname, "yeti.rigsettings")
|
||||
|
||||
# Yeti related staging dirs
|
||||
maya_path = os.path.join(dirname, "yeti_rig.ma")
|
||||
maya_path = os.path.join(
|
||||
dirname, "yeti_rig.{}".format(self.scene_type))
|
||||
|
||||
self.log.info("Writing metadata file")
|
||||
|
||||
|
|
@ -153,7 +171,7 @@ class ExtractYetiRig(pype.api.Extractor):
|
|||
cmds.file(maya_path,
|
||||
force=True,
|
||||
exportSelected=True,
|
||||
typ="mayaAscii",
|
||||
typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501
|
||||
preserveReferences=False,
|
||||
constructionHistory=True,
|
||||
shader=False)
|
||||
|
|
@ -163,21 +181,21 @@ class ExtractYetiRig(pype.api.Extractor):
|
|||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
self.log.info("rig file: {}".format("yeti_rig.ma"))
|
||||
self.log.info("rig file: {}".format(maya_path))
|
||||
instance.data["representations"].append(
|
||||
{
|
||||
'name': "ma",
|
||||
'ext': 'ma',
|
||||
'files': "yeti_rig.ma",
|
||||
'name': self.scene_type,
|
||||
'ext': self.scene_type,
|
||||
'files': os.path.basename(maya_path),
|
||||
'stagingDir': dirname
|
||||
}
|
||||
)
|
||||
self.log.info("settings file: {}".format("yeti.rigsettings"))
|
||||
self.log.info("settings file: {}".format(settings))
|
||||
instance.data["representations"].append(
|
||||
{
|
||||
'name': 'rigsettings',
|
||||
'ext': 'rigsettings',
|
||||
'files': 'yeti.rigsettings',
|
||||
'files': os.path.basename(settings),
|
||||
'stagingDir': dirname
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -62,9 +62,16 @@ class ValidateAttributes(pyblish.api.ContextPlugin):
|
|||
for family in families:
|
||||
for preset in presets[family]:
|
||||
[node_name, attribute_name] = preset.split(".")
|
||||
attributes.update(
|
||||
{node_name: {attribute_name: presets[family][preset]}}
|
||||
)
|
||||
try:
|
||||
attributes[node_name].update(
|
||||
{attribute_name: presets[family][preset]}
|
||||
)
|
||||
except KeyError:
|
||||
attributes.update({
|
||||
node_name: {
|
||||
attribute_name: presets[family][preset]
|
||||
}
|
||||
})
|
||||
|
||||
# Get invalid attributes.
|
||||
nodes = pm.ls()
|
||||
|
|
|
|||
|
|
@ -1,108 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validate if instance asset is the same as context asset."""
|
||||
from __future__ import absolute_import
|
||||
import pyblish.api
|
||||
from pype.action import get_errored_instances_from_context
|
||||
import pype.api
|
||||
|
||||
|
||||
class SelectInvalidInstances(pyblish.api.Action):
|
||||
"""Select invalid instances in Outliner."""
|
||||
|
||||
label = "Show Instances"
|
||||
icon = "briefcase"
|
||||
on = "failed"
|
||||
|
||||
def process(self, context, plugin):
|
||||
"""Process invalid validators and select invalid instances."""
|
||||
try:
|
||||
from maya import cmds
|
||||
except ImportError:
|
||||
raise ImportError("Current host is not Maya")
|
||||
|
||||
errored_instances = get_errored_instances_from_context(context)
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(errored_instances, plugin)
|
||||
|
||||
# Get the invalid nodes for the plug-ins
|
||||
self.log.info("Finding invalid nodes..")
|
||||
invalid = list()
|
||||
for _instance in instances:
|
||||
invalid_instances = plugin.get_invalid(context)
|
||||
if invalid_instances:
|
||||
if isinstance(invalid_instances, (list, tuple)):
|
||||
invalid.extend(invalid_instances)
|
||||
else:
|
||||
self.log.warning("Plug-in returned to be invalid, "
|
||||
"but has no selectable nodes.")
|
||||
|
||||
# Ensure unique (process each node only once)
|
||||
invalid = list(set(invalid))
|
||||
|
||||
if invalid:
|
||||
self.log.info("Selecting invalid nodes: %s" % ", ".join(invalid))
|
||||
cmds.select(invalid, replace=True, noExpand=True)
|
||||
else:
|
||||
self.log.info("No invalid nodes found.")
|
||||
cmds.select(deselect=True)
|
||||
|
||||
|
||||
class RepairSelectInvalidInstances(pyblish.api.Action):
|
||||
"""Repair the instance asset."""
|
||||
|
||||
label = "Repair"
|
||||
icon = "wrench"
|
||||
on = "failed"
|
||||
|
||||
def process(self, context, plugin):
|
||||
from maya import cmds
|
||||
# Get the errored instances
|
||||
failed = []
|
||||
for result in context.data["results"]:
|
||||
if (result["error"] is not None and result["instance"] is not None
|
||||
and result["instance"] not in failed):
|
||||
failed.append(result["instance"])
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(failed, plugin)
|
||||
context_asset = context.data["assetEntity"]["name"]
|
||||
for instance in instances:
|
||||
cmds.setAttr(instance.data.get("name") + ".asset",
|
||||
context_asset, type="string")
|
||||
|
||||
|
||||
class ValidateInstanceInContext(pyblish.api.ContextPlugin):
|
||||
"""Validator to check if instance asset match context asset.
|
||||
|
||||
When working in per-shot style you always publish data in context of
|
||||
current asset (shot). This validator checks if this is so. It is optional
|
||||
so it can be disabled when needed.
|
||||
|
||||
Action on this validator will select invalid instances in Outliner.
|
||||
"""
|
||||
|
||||
order = pype.api.ValidateContentsOrder
|
||||
label = "Instance in same Context"
|
||||
optional = True
|
||||
actions = [SelectInvalidInstances, RepairSelectInvalidInstances]
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, context):
|
||||
"""Get invalid instances."""
|
||||
invalid = []
|
||||
context_asset = context.data["assetEntity"]["name"]
|
||||
cls.log.info("we are in {}".format(context_asset))
|
||||
for instance in context:
|
||||
asset = instance.data.get("asset")
|
||||
if asset != context_asset:
|
||||
cls.log.warning("{} has asset {}".format(instance.name, asset))
|
||||
invalid.append(instance.name)
|
||||
|
||||
return invalid
|
||||
|
||||
def process(self, context):
|
||||
"""Check instances."""
|
||||
invalid = self.get_invalid(context)
|
||||
if invalid:
|
||||
raise AssertionError("Some instances doesn't share same context")
|
||||
|
|
@ -106,7 +106,7 @@ class CollectNukeInstances(pyblish.api.ContextPlugin):
|
|||
|
||||
instance.data.update({
|
||||
"subset": subset,
|
||||
"asset": os.environ["AVALON_ASSET"],
|
||||
"asset": avalon_knob_data["asset"],
|
||||
"label": node.name(),
|
||||
"name": node.name(),
|
||||
"subset": subset,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import pyblish.api
|
||||
import pype.api
|
||||
from avalon import io, api
|
||||
|
||||
import nuke
|
||||
|
||||
|
||||
|
|
@ -23,6 +26,21 @@ class CollectReview(pyblish.api.InstancePlugin):
|
|||
if not node["review"].value():
|
||||
return
|
||||
|
||||
# Add audio to instance if it exists.
|
||||
try:
|
||||
version = pype.api.get_latest_version(
|
||||
instance.context.data["assetEntity"]["name"], "audioMain"
|
||||
)
|
||||
representation = io.find_one(
|
||||
{"type": "representation", "parent": version["_id"]}
|
||||
)
|
||||
instance.data["audio"] = [{
|
||||
"offset": 0,
|
||||
"filename": api.get_representation_path(representation)
|
||||
}]
|
||||
except AssertionError:
|
||||
pass
|
||||
|
||||
instance.data["families"].append("review")
|
||||
instance.data['families'].append('ftrack')
|
||||
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ class ExtractThumbnail(pype.api.Extractor):
|
|||
|
||||
ipn_orig = None
|
||||
for v in [n for n in nuke.allNodes()
|
||||
if "Viewer" in n.Class()]:
|
||||
if "Viewer" == n.Class()]:
|
||||
ip = v['input_process'].getValue()
|
||||
ipn = v['input_process_node'].getValue()
|
||||
if "VIEWER_INPUT" not in ipn and ip:
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class ExtractImage(pype.api.Extractor):
|
|||
label = "Extract Image"
|
||||
hosts = ["photoshop"]
|
||||
families = ["image"]
|
||||
formats = ["png", "jpg"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
|
|
@ -32,20 +33,22 @@ class ExtractImage(pype.api.Extractor):
|
|||
if layer.id not in extract_ids:
|
||||
layer.Visible = False
|
||||
|
||||
save_options = {
|
||||
"png": photoshop.com_objects.PNGSaveOptions(),
|
||||
"jpg": photoshop.com_objects.JPEGSaveOptions()
|
||||
}
|
||||
save_options = {}
|
||||
if "png" in self.formats:
|
||||
save_options["png"] = photoshop.com_objects.PNGSaveOptions()
|
||||
if "jpg" in self.formats:
|
||||
save_options["jpg"] = photoshop.com_objects.JPEGSaveOptions()
|
||||
|
||||
file_basename = os.path.splitext(
|
||||
photoshop.app().ActiveDocument.Name
|
||||
)[0]
|
||||
for extension, save_option in save_options.items():
|
||||
_filename = "{}.{}".format(file_basename, extension)
|
||||
files[extension] = _filename
|
||||
|
||||
full_filename = os.path.join(staging_dir, _filename)
|
||||
photoshop.app().ActiveDocument.SaveAs(
|
||||
staging_dir, save_option, True
|
||||
)
|
||||
files[extension] = "{} copy.{}".format(
|
||||
os.path.splitext(
|
||||
photoshop.app().ActiveDocument.Name
|
||||
)[0],
|
||||
extension
|
||||
full_filename, save_option, True
|
||||
)
|
||||
|
||||
representations = []
|
||||
|
|
|
|||
|
|
@ -24,9 +24,10 @@ class ExtractReview(pype.api.Extractor):
|
|||
layers.append(image_instance[0])
|
||||
|
||||
# Perform extraction
|
||||
output_image = "{} copy.jpg".format(
|
||||
output_image = "{}.jpg".format(
|
||||
os.path.splitext(photoshop.app().ActiveDocument.Name)[0]
|
||||
)
|
||||
output_image_path = os.path.join(staging_dir, output_image)
|
||||
with photoshop.maintained_visibility():
|
||||
# Hide all other layers.
|
||||
extract_ids = [
|
||||
|
|
@ -39,7 +40,9 @@ class ExtractReview(pype.api.Extractor):
|
|||
layer.Visible = False
|
||||
|
||||
photoshop.app().ActiveDocument.SaveAs(
|
||||
staging_dir, photoshop.com_objects.JPEGSaveOptions(), True
|
||||
output_image_path,
|
||||
photoshop.com_objects.JPEGSaveOptions(),
|
||||
True
|
||||
)
|
||||
|
||||
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
|
||||
|
|
@ -56,7 +59,7 @@ class ExtractReview(pype.api.Extractor):
|
|||
thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg")
|
||||
args = [
|
||||
ffmpeg_path, "-y",
|
||||
"-i", os.path.join(staging_dir, output_image),
|
||||
"-i", output_image_path,
|
||||
"-vf", "scale=300:-1",
|
||||
"-vframes", "1",
|
||||
thumbnail_path
|
||||
|
|
@ -77,7 +80,7 @@ class ExtractReview(pype.api.Extractor):
|
|||
mov_path = os.path.join(staging_dir, "review.mov")
|
||||
args = [
|
||||
ffmpeg_path, "-y",
|
||||
"-i", os.path.join(staging_dir, output_image),
|
||||
"-i", output_image_path,
|
||||
"-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2",
|
||||
"-vframes", "1",
|
||||
mov_path
|
||||
|
|
|
|||
|
|
@ -528,6 +528,9 @@ def burnins_from_data(
|
|||
if pix_fmt:
|
||||
ffmpeg_args.append("-pix_fmt {}".format(pix_fmt))
|
||||
|
||||
# Use group one (same as `-intra` argument, which is deprecated)
|
||||
ffmpeg_args.append("-g 1")
|
||||
|
||||
ffmpeg_args_str = " ".join(ffmpeg_args)
|
||||
burnin.render(
|
||||
output_path, args=ffmpeg_args_str, overwrite=overwrite, **data
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__version__ = "2.11.0"
|
||||
__version__ = "2.11.3"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue