diff --git a/pype/api.py b/pype/api.py
index 44a31f2626..ce13688d13 100644
--- a/pype/api.py
+++ b/pype/api.py
@@ -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"
]
diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py
index 3cae695852..d4b7d91fdb 100644
--- a/pype/hosts/harmony/__init__.py
+++ b/pype/hosts/harmony/__init__.py
@@ -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]);
diff --git a/pype/hosts/maya/expected_files.py b/pype/hosts/maya/expected_files.py
index a7204cba93..77d55eb1c1 100644
--- a/pype/hosts/maya/expected_files.py
+++ b/pype/hosts/maya/expected_files.py
@@ -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"]
diff --git a/pype/hosts/nuke/lib.py b/pype/hosts/nuke/lib.py
index 72a8836a03..8c0e37b15d 100644
--- a/pype/hosts/nuke/lib.py
+++ b/pype/hosts/nuke/lib.py
@@ -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:
diff --git a/pype/hosts/nukestudio/tags.json b/pype/hosts/nukestudio/tags.json
new file mode 100644
index 0000000000..56fcfcbce9
--- /dev/null
+++ b/pype/hosts/nukestudio/tags.json
@@ -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"
+ }
+ }
+}
diff --git a/pype/hosts/nukestudio/tags.py b/pype/hosts/nukestudio/tags.py
index c97f13d17c..c2b1d0d728 100644
--- a/pype/hosts/nukestudio/tags.py
+++ b/pype/hosts/nukestudio/tags.py
@@ -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"]
diff --git a/pype/lib.py b/pype/lib.py
index b62840f3a4..f99cd73e09 100644
--- a/pype/lib.py
+++ b/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
diff --git a/pype/modules/ftrack/actions/action_delete_old_versions.py b/pype/modules/ftrack/actions/action_delete_old_versions.py
index 46652b136a..6a4c5a0cae 100644
--- a/pype/modules/ftrack/actions/action_delete_old_versions.py
+++ b/pype/modules/ftrack/actions/action_delete_old_versions.py
@@ -105,11 +105,34 @@ class DeleteOldVersions(BaseAction):
"value": False
})
+ items.append(self.splitter_item)
+
+ items.append({
+ "type": "label",
+ "value": (
+ "This will NOT delete any files and only return the "
+ "total size of the files."
+ )
+ })
+ 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"]
diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py
index d4b86d1278..231aebdf7a 100644
--- a/pype/modules/ftrack/actions/action_delivery.py
+++ b/pype/modules/ftrack/actions/action_delivery.py
@@ -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
diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py
index f78e617f3a..4bab1676d4 100644
--- a/pype/modules/ftrack/lib/avalon_sync.py
+++ b/pype/modules/ftrack/lib/avalon_sync.py
@@ -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"]:
diff --git a/pype/modules/logging/gui/app.py b/pype/modules/logging/gui/app.py
index 99b0b230a9..c0e180c8a1 100644
--- a/pype/modules/logging/gui/app.py
+++ b/pype/modules/logging/gui/app.py
@@ -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)
diff --git a/pype/modules/logging/gui/lib.py b/pype/modules/logging/gui/lib.py
deleted file mode 100644
index 85782e071e..0000000000
--- a/pype/modules/logging/gui/lib.py
+++ /dev/null
@@ -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)
diff --git a/pype/modules/logging/gui/models.py b/pype/modules/logging/gui/models.py
index ce1fa236a9..ae2666f501 100644
--- a/pype/modules/logging/gui/models.py
+++ b/pype/modules/logging/gui/models.py
@@ -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
diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py
index cf20066397..cd0df283bf 100644
--- a/pype/modules/logging/gui/widgets.py
+++ b/pype/modules/logging/gui/widgets.py
@@ -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 = "{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"""
-{user} - {timestamp}
-User
{user}
-
Level
{level}
-
Message
{message}
-
Log Name
{logname}
Method
{method}
-
File
{fileName}
-
Line
{lineNumber}
-
Host
{host}
-
Timestamp
{timestamp}
-"""
-
- 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))
diff --git a/pype/plugins/celaction/publish/collect_render_path.py b/pype/plugins/celaction/publish/collect_render_path.py
index a3918a52b6..9cbb0e4880 100644
--- a/pype/plugins/celaction/publish/collect_render_path.py
+++ b/pype/plugins/celaction/publish/collect_render_path.py
@@ -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}`")
diff --git a/pype/plugins/celaction/publish/integrate_version_up.py b/pype/plugins/celaction/publish/integrate_version_up.py
index 1822ceabcb..e15c5d5bf6 100644
--- a/pype/plugins/celaction/publish/integrate_version_up.py
+++ b/pype/plugins/celaction/publish/integrate_version_up.py
@@ -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)
diff --git a/pype/plugins/global/publish/cleanup.py b/pype/plugins/global/publish/cleanup.py
index 3ab41f90ca..e891b7b7f6 100644
--- a/pype/plugins/global/publish/cleanup.py
+++ b/pype/plugins/global/publish/cleanup.py
@@ -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)
diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py
index dd9e84aade..3053c80b11 100644
--- a/pype/plugins/global/publish/submit_publish_job.py
+++ b/pype/plugins/global/publish/submit_publish_job.py
@@ -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):
diff --git a/pype/plugins/global/publish/validate_instance_in_context.py b/pype/plugins/global/publish/validate_instance_in_context.py
new file mode 100644
index 0000000000..a4fc555161
--- /dev/null
+++ b/pype/plugins/global/publish/validate_instance_in_context.py
@@ -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
diff --git a/pype/plugins/harmony/publish/extract_render.py b/pype/plugins/harmony/publish/extract_render.py
index fe1352f9f9..45b52e0307 100644
--- a/pype/plugins/harmony/publish/extract_render.py
+++ b/pype/plugins/harmony/publish/extract_render.py
@@ -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")
diff --git a/pype/plugins/harmony/publish/validate_scene_settings.py b/pype/plugins/harmony/publish/validate_scene_settings.py
index aa9a70bd85..3602f1ca22 100644
--- a/pype/plugins/harmony/publish/validate_scene_settings.py
+++ b/pype/plugins/harmony/publish/validate_scene_settings.py
@@ -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 {
diff --git a/pype/plugins/maya/load/load_rendersetup.py b/pype/plugins/maya/load/load_rendersetup.py
index b38e2988b1..45a314a9d1 100644
--- a/pype/plugins/maya/load/load_rendersetup.py
+++ b/pype/plugins/maya/load/load_rendersetup.py
@@ -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)
diff --git a/pype/plugins/maya/publish/extract_camera_alembic.py b/pype/plugins/maya/publish/extract_camera_alembic.py
index cc090760ff..c61ec5e19e 100644
--- a/pype/plugins/maya/publish/extract_camera_alembic.py
+++ b/pype/plugins/maya/publish/extract_camera_alembic.py
@@ -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)
diff --git a/pype/plugins/maya/publish/extract_camera_mayaAscii.py b/pype/plugins/maya/publish/extract_camera_mayaScene.py
similarity index 82%
rename from pype/plugins/maya/publish/extract_camera_mayaAscii.py
rename to pype/plugins/maya/publish/extract_camera_mayaScene.py
index 973d8d452a..03dde031e9 100644
--- a/pype/plugins/maya/publish/extract_camera_mayaAscii.py
+++ b/pype/plugins/maya/publish/extract_camera_mayaScene.py
@@ -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,
}
diff --git a/pype/plugins/maya/publish/extract_maya_ascii_raw.py b/pype/plugins/maya/publish/extract_maya_scene_raw.py
similarity index 60%
rename from pype/plugins/maya/publish/extract_maya_ascii_raw.py
rename to pype/plugins/maya/publish/extract_maya_scene_raw.py
index 895b6acbfe..2971572552 100644
--- a/pype/plugins/maya/publish/extract_maya_ascii_raw.py
+++ b/pype/plugins/maya/publish/extract_maya_scene_raw.py
@@ -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
}
diff --git a/pype/plugins/maya/publish/extract_model.py b/pype/plugins/maya/publish/extract_model.py
index ba56194eea..330e471e53 100644
--- a/pype/plugins/maya/publish/extract_model.py
+++ b/pype/plugins/maya/publish/extract_model.py
@@ -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,
}
diff --git a/pype/plugins/maya/publish/extract_rig.py b/pype/plugins/maya/publish/extract_rig.py
index c98e562313..8ebeae4184 100644
--- a/pype/plugins/maya/publish/extract_rig.py
+++ b/pype/plugins/maya/publish/extract_rig.py
@@ -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))
diff --git a/pype/plugins/maya/publish/extract_yeti_rig.py b/pype/plugins/maya/publish/extract_yeti_rig.py
index d7bbcd6555..2f66d3e026 100644
--- a/pype/plugins/maya/publish/extract_yeti_rig.py
+++ b/pype/plugins/maya/publish/extract_yeti_rig.py
@@ -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
}
)
diff --git a/pype/plugins/maya/publish/validate_attributes.py b/pype/plugins/maya/publish/validate_attributes.py
index 6ecebfa107..a77fbe5e93 100644
--- a/pype/plugins/maya/publish/validate_attributes.py
+++ b/pype/plugins/maya/publish/validate_attributes.py
@@ -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()
diff --git a/pype/plugins/maya/publish/validate_instance_in_context.py b/pype/plugins/maya/publish/validate_instance_in_context.py
deleted file mode 100644
index 542249bb2d..0000000000
--- a/pype/plugins/maya/publish/validate_instance_in_context.py
+++ /dev/null
@@ -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")
diff --git a/pype/plugins/nuke/publish/collect_instances.py b/pype/plugins/nuke/publish/collect_instances.py
index 0bbede11c0..9085e12bd8 100644
--- a/pype/plugins/nuke/publish/collect_instances.py
+++ b/pype/plugins/nuke/publish/collect_instances.py
@@ -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,
diff --git a/pype/plugins/nuke/publish/collect_review.py b/pype/plugins/nuke/publish/collect_review.py
index c95c94541d..e7e8da19a1 100644
--- a/pype/plugins/nuke/publish/collect_review.py
+++ b/pype/plugins/nuke/publish/collect_review.py
@@ -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')
diff --git a/pype/plugins/nuke/publish/extract_thumbnail.py b/pype/plugins/nuke/publish/extract_thumbnail.py
index 5e9302a01a..a3ef09bc9f 100644
--- a/pype/plugins/nuke/publish/extract_thumbnail.py
+++ b/pype/plugins/nuke/publish/extract_thumbnail.py
@@ -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:
diff --git a/pype/plugins/photoshop/publish/extract_image.py b/pype/plugins/photoshop/publish/extract_image.py
index da3197c7da..6dfccdc4f2 100644
--- a/pype/plugins/photoshop/publish/extract_image.py
+++ b/pype/plugins/photoshop/publish/extract_image.py
@@ -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 = []
diff --git a/pype/plugins/photoshop/publish/extract_review.py b/pype/plugins/photoshop/publish/extract_review.py
index 8aebd1ea87..078ee53899 100644
--- a/pype/plugins/photoshop/publish/extract_review.py
+++ b/pype/plugins/photoshop/publish/extract_review.py
@@ -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
diff --git a/pype/scripts/otio_burnin.py b/pype/scripts/otio_burnin.py
index 104ff0255c..16e24757dd 100644
--- a/pype/scripts/otio_burnin.py
+++ b/pype/scripts/otio_burnin.py
@@ -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
diff --git a/pype/version.py b/pype/version.py
index 7f6646a762..ddcf716b76 100644
--- a/pype/version.py
+++ b/pype/version.py
@@ -1 +1 @@
-__version__ = "2.11.0"
+__version__ = "2.11.3"