From 52feeabb447f4fd0c79ff324fd83e3caafd65678 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Dec 2020 17:47:13 +0100 Subject: [PATCH 001/106] DWAA support Redo of PR on 'master' --- pype/lib/__init__.py | 10 +- pype/lib/plugin_tools.py | 97 +++++++++++++++++++ pype/plugins/global/publish/extract_burnin.py | 48 +++++++-- pype/plugins/global/publish/extract_jpeg.py | 29 ++++-- pype/plugins/global/publish/extract_review.py | 42 ++++++-- 5 files changed, 200 insertions(+), 26 deletions(-) diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 78fd69da98..03cab2aad2 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -29,7 +29,11 @@ from .plugin_tools import ( filter_pyblish_plugins, source_hash, get_unique_layer_name, - get_background_layers + get_background_layers, + oiio_supported, + decompress, + get_decompress_dir, + should_decompress ) from .path_tools import ( @@ -64,6 +68,10 @@ __all__ = [ "filter_pyblish_plugins", "get_unique_layer_name", "get_background_layers", + "oiio_supported", + "decompress", + "get_decompress_dir", + "should_decompress", "version_up", "get_version_from_path", diff --git a/pype/lib/plugin_tools.py b/pype/lib/plugin_tools.py index f5eb354ca3..2fc99bcf16 100644 --- a/pype/lib/plugin_tools.py +++ b/pype/lib/plugin_tools.py @@ -5,6 +5,8 @@ import inspect import logging import re import json +import pype.api +import tempfile from ..api import config @@ -134,3 +136,98 @@ def get_background_layers(file_url): layer.get("filename")). replace("\\", "/")) return layers + + +def oiio_supported(): + """ + Checks if oiiotool is configured for this platform. + + 'should_decompress' will throw exception if configured, + but not present or working. + """ + return os.getenv("PYPE_OIIO_PATH", "") != "" + + +def decompress(target_dir, file_url, + input_frame_start=None, input_frame_end=None, log=None): + """ + Decompresses DWAA 'file_url' .exr to 'target_dir'. + + Creates uncompressed files in 'target_dir', they need to be cleaned. + + File url could be for single file or for a sequence, in that case + %0Xd will be as a placeholder for frame number AND input_frame* will + be filled. + In that case single oiio command with '--frames' will be triggered for + all frames, this should be faster then looping and running sequentially + + Args: + target_dir (str): extended from stagingDir + file_url (str): full urls to source file (with or without %0Xd) + input_frame_start (int) (optional): first frame + input_frame_end (int) (optional): last frame + log (Logger) (optional): pype logger + """ + is_sequence = input_frame_start is not None and \ + input_frame_end is not None and \ + (int(input_frame_end) > int(input_frame_start)) + + oiio_cmd = [] + oiio_cmd.append(os.getenv("PYPE_OIIO_PATH")) + + oiio_cmd.append("--compression none") + + base_file_name = os.path.basename(file_url) + oiio_cmd.append(file_url) + + if is_sequence: + oiio_cmd.append("--frames {}-{}".format(input_frame_start, + input_frame_end)) + + oiio_cmd.append("-o") + oiio_cmd.append(os.path.join(target_dir, base_file_name)) + + subprocess_exr = " ".join(oiio_cmd) + + if not log: + log = logging.getLogger(__name__) + + log.debug("Decompressing {}".format(subprocess_exr)) + pype.api.subprocess( + subprocess_exr, shell=True, logger=log + ) + + +def get_decompress_dir(): + """ + Creates temporary folder for decompressing. + Its local, in case of farm it is 'local' to the farm machine. + + Should be much faster, needs to be cleaned up later. + """ + return os.path.normpath( + tempfile.mkdtemp(prefix="pyblish_tmp_") + ) + + +def should_decompress(file_url): + """ + Tests that 'file_url' is compressed with DWAA. + + Uses 'oiio_supported' to check that OIIO tool is available for this + platform + + Args: + file_url (str): path to rendered file (in sequence it would be + first file, if that compressed it is expected that whole seq + will be too) + Returns: + (bool): 'file_url' is DWAA compressed and should be decompressed + """ + if oiio_supported(): + output = pype.api.subprocess([os.getenv("PYPE_OIIO_PATH"), + "--info", "-v", file_url]) + return "compression: \"dwaa\"" in output or \ + "compression: \"dwab\"" in output + + return False diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py index 5be5060590..79b02ed01c 100644 --- a/pype/plugins/global/publish/extract_burnin.py +++ b/pype/plugins/global/publish/extract_burnin.py @@ -6,6 +6,9 @@ import tempfile import pype.api import pyblish +from pype.lib import oiio_supported, should_decompress, \ + get_decompress_dir, decompress +import shutil class ExtractBurnin(pype.api.Extractor): @@ -28,7 +31,8 @@ class ExtractBurnin(pype.api.Extractor): "premiere", "standalonepublisher", "harmony", - "fusion" + "fusion", + "aftereffects" ] optional = True @@ -54,15 +58,16 @@ class ExtractBurnin(pype.api.Extractor): def process(self, instance): # ffmpeg doesn't support multipart exrs if instance.data.get("multipartExr") is True: - instance_label = ( - getattr(instance, "label", None) - or instance.data.get("label") - or instance.data.get("name") - ) - self.log.info(( - "Instance \"{}\" contain \"multipartExr\". Skipped." - ).format(instance_label)) - return + if not oiio_supported(): + instance_label = ( + getattr(instance, "label", None) + or instance.data.get("label") + or instance.data.get("name") + ) + self.log.info(( + "Instance \"{}\" contain \"multipartExr\". Skipped." + ).format(instance_label)) + return # QUESTION what is this for and should we raise an exception? if "representations" not in instance.data: @@ -212,6 +217,26 @@ class ExtractBurnin(pype.api.Extractor): # Prepare paths and files for process. self.input_output_paths(new_repre, temp_data, filename_suffix) + decompressed_dir = '' + full_input_path = temp_data["full_input_path"] + do_decompress = should_decompress(full_input_path) + if do_decompress: + decompressed_dir = get_decompress_dir() + + decompress( + decompressed_dir, + full_input_path, + temp_data["frame_start"], + temp_data["frame_end"], + self.log + ) + + # input path changed, 'decompressed' added + input_file = os.path.basename(full_input_path) + temp_data["full_input_path"] = os.path.join( + decompressed_dir, + input_file) + # Data for burnin script script_data = { "input": temp_data["full_input_path"], @@ -271,6 +296,9 @@ class ExtractBurnin(pype.api.Extractor): os.remove(filepath) self.log.debug("Removed: \"{}\"".format(filepath)) + if do_decompress and os.path.exists(decompressed_dir): + shutil.rmtree(decompressed_dir) + def prepare_basic_data(self, instance): """Pick data from instance for processing and for burnin strings. diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index 551e57796a..85bc60ddfc 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -3,6 +3,9 @@ import os import pyblish.api import pype.api import pype.lib +from pype.lib import oiio_supported, should_decompress, \ + get_decompress_dir, decompress +import shutil class ExtractJpegEXR(pyblish.api.InstancePlugin): @@ -22,9 +25,11 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): if 'crypto' in instance.data['subset']: return - # ffmpeg doesn't support multipart exrs + do_decompress = False + # ffmpeg doesn't support multipart exrs, use oiiotool if available if instance.data.get("multipartExr") is True: - return + if not oiio_supported(): + return # Skip review when requested. if not instance.data.get("review", True): @@ -36,10 +41,6 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): # filter out mov and img sequences representations_new = representations[:] - if instance.data.get("multipartExr"): - # ffmpeg doesn't support multipart exrs - return - for repre in representations: tags = repre.get("tags", []) self.log.debug(repre) @@ -60,6 +61,19 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): full_input_path = os.path.join(stagingdir, input_file) self.log.info("input {}".format(full_input_path)) + decompressed_dir = '' + do_decompress = should_decompress(full_input_path) + if do_decompress: + decompressed_dir = get_decompress_dir() + + decompress( + decompressed_dir, + full_input_path) + # input path changed, 'decompressed' added + full_input_path = os.path.join( + decompressed_dir, + input_file) + filename = os.path.splitext(input_file)[0] if not filename.endswith('.'): filename += "." @@ -111,4 +125,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): self.log.debug("Adding: {}".format(representation)) representations_new.append(representation) + if do_decompress and os.path.exists(decompressed_dir): + shutil.rmtree(decompressed_dir) + instance.data["representations"] = representations_new diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index aa8d8accb5..a40a943559 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -6,6 +6,8 @@ import pyblish.api import clique import pype.api import pype.lib +from pype.lib import oiio_supported, should_decompress, \ + get_decompress_dir, decompress class ExtractReview(pyblish.api.InstancePlugin): @@ -14,7 +16,7 @@ class ExtractReview(pyblish.api.InstancePlugin): Compulsory attribute of representation is tags list with "review", otherwise the representation is ignored. - All new represetnations are created and encoded by ffmpeg following + All new representations are created and encoded by ffmpeg following presets found in `pype-config/presets/plugins/global/ publish.json:ExtractReview:outputs`. """ @@ -58,7 +60,9 @@ class ExtractReview(pyblish.api.InstancePlugin): return # ffmpeg doesn't support multipart exrs - if instance.data.get("multipartExr") is True: + if instance.data.get("multipartExr") is True \ + and not oiio_supported(): + instance_label = ( getattr(instance, "label", None) or instance.data.get("label") @@ -318,9 +322,9 @@ class ExtractReview(pyblish.api.InstancePlugin): Args: output_def (dict): Currently processed output definition. instance (Instance): Currently processed instance. - new_repre (dict): Reprensetation representing output of this + new_repre (dict): Representation representing output of this process. - temp_data (dict): Base data for successfull process. + temp_data (dict): Base data for successful process. """ # Get FFmpeg arguments from profile presets @@ -331,9 +335,29 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_video_filters = out_def_ffmpeg_args.get("video_filters") or [] ffmpeg_audio_filters = out_def_ffmpeg_args.get("audio_filters") or [] + input_files_urls = [os.path.join(new_repre["stagingDir"], f) for f + in new_repre['files']] + do_decompress = should_decompress(input_files_urls[0]) + if do_decompress: + # change stagingDir, decompress first + # calculate all paths with modified directory, used on too many + # places + # will be purged by cleanup.py automatically + orig_staging_dir = new_repre["stagingDir"] + new_repre["stagingDir"] = get_decompress_dir() + # Prepare input and output filepaths self.input_output_paths(new_repre, output_def, temp_data) + if do_decompress: + input_file = temp_data["full_input_path"].\ + replace(new_repre["stagingDir"], orig_staging_dir) + + decompress(new_repre["stagingDir"], input_file, + temp_data["frame_start"], + temp_data["frame_end"], + self.log) + # Set output frames len to 1 when ouput is single image if ( temp_data["output_ext_is_image"] @@ -930,7 +954,7 @@ class ExtractReview(pyblish.api.InstancePlugin): return regexes def validate_value_by_regexes(self, value, in_list): - """Validates in any regexe from list match entered value. + """Validates in any regex from list match entered value. Args: in_list (list): List with regexes. @@ -955,9 +979,9 @@ class ExtractReview(pyblish.api.InstancePlugin): def profile_exclusion(self, matching_profiles): """Find out most matching profile byt host, task and family match. - Profiles are selectivelly filtered. Each profile should have + Profiles are selectively filtered. Each profile should have "__value__" key with list of booleans. Each boolean represents - existence of filter for specific key (host, taks, family). + existence of filter for specific key (host, tasks, family). Profiles are looped in sequence. In each sequence are split into true_list and false_list. For next sequence loop are used profiles in true_list if there are any profiles else false_list is used. @@ -1036,7 +1060,7 @@ class ExtractReview(pyblish.api.InstancePlugin): highest_profile_points = -1 # Each profile get 1 point for each matching filter. Profile with most - # points is returnd. For cases when more than one profile will match + # points is returned. For cases when more than one profile will match # are also stored ordered lists of matching values. for profile in self.profiles: profile_points = 0 @@ -1648,7 +1672,7 @@ class ExtractReview(pyblish.api.InstancePlugin): def add_video_filter_args(self, args, inserting_arg): """ - Fixing video filter argumets to be one long string + Fixing video filter arguments to be one long string Args: args (list): list of string arguments From b74dc3a7a79bdf6067f8c7ea7d11e0088487cf5b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 9 Dec 2020 10:07:18 +0100 Subject: [PATCH 002/106] fix(global): two types on repre["files"] support and better exception oiio_supported didn't test path existence --- pype/lib/plugin_tools.py | 6 +++++- pype/plugins/global/publish/extract_review.py | 12 +++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/pype/lib/plugin_tools.py b/pype/lib/plugin_tools.py index 2fc99bcf16..460665935a 100644 --- a/pype/lib/plugin_tools.py +++ b/pype/lib/plugin_tools.py @@ -145,7 +145,11 @@ def oiio_supported(): 'should_decompress' will throw exception if configured, but not present or working. """ - return os.getenv("PYPE_OIIO_PATH", "") != "" + oiio_path = os.getenv("PYPE_OIIO_PATH", "") + if not os.path.exists(oiio_path) or not oiio_path: + raise IOError("Files do not exists in `{}`".format(oiio_path)) + + return True def decompress(target_dir, file_url, diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index a40a943559..26e60fbd48 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -335,9 +335,15 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_video_filters = out_def_ffmpeg_args.get("video_filters") or [] ffmpeg_audio_filters = out_def_ffmpeg_args.get("audio_filters") or [] - input_files_urls = [os.path.join(new_repre["stagingDir"], f) for f - in new_repre['files']] - do_decompress = should_decompress(input_files_urls[0]) + if isinstance(new_repre['files'], list): + input_files_urls = [os.path.join(new_repre["stagingDir"], f) for f + in new_repre['files']] + do_decompress = should_decompress(input_files_urls[0]) + else: + test_path = os.path.join( + new_repre["stagingDir"], new_repre['files']) + do_decompress = should_decompress(test_path) + if do_decompress: # change stagingDir, decompress first # calculate all paths with modified directory, used on too many From 5ca4f470970bebbc4aea7695a1d7bb7a639b6531 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Dec 2020 11:30:24 +0100 Subject: [PATCH 003/106] Changed order of conditions --- pype/lib/plugin_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/lib/plugin_tools.py b/pype/lib/plugin_tools.py index 460665935a..57019e6a72 100644 --- a/pype/lib/plugin_tools.py +++ b/pype/lib/plugin_tools.py @@ -146,7 +146,7 @@ def oiio_supported(): but not present or working. """ oiio_path = os.getenv("PYPE_OIIO_PATH", "") - if not os.path.exists(oiio_path) or not oiio_path: + if not oiio_path or not os.path.exists(oiio_path): raise IOError("Files do not exists in `{}`".format(oiio_path)) return True From 7d9a7c4625023ddd4082aea139b5b9d5065c1300 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Dec 2020 21:59:57 +0100 Subject: [PATCH 004/106] Rework oiio_supported - return only boolean Removed multipartExr part, not implemented yet, nothing to do with DWAA Try catch added temporarily to maximalize chance to finish publish, possible failures beause of DWAA and no oiio results in log and empty result, not an exception and hard fail. --- pype/lib/plugin_tools.py | 18 ++++++++++++--- pype/plugins/global/publish/extract_burnin.py | 19 ++++++++-------- pype/plugins/global/publish/extract_jpeg.py | 12 +++++++--- pype/plugins/global/publish/extract_review.py | 22 ++++++++++++------- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/pype/lib/plugin_tools.py b/pype/lib/plugin_tools.py index 57019e6a72..c2b938c9bb 100644 --- a/pype/lib/plugin_tools.py +++ b/pype/lib/plugin_tools.py @@ -142,12 +142,18 @@ def oiio_supported(): """ Checks if oiiotool is configured for this platform. + Expects full path to executable. + 'should_decompress' will throw exception if configured, - but not present or working. + but not present or not working. + Returns: + (bool) """ oiio_path = os.getenv("PYPE_OIIO_PATH", "") if not oiio_path or not os.path.exists(oiio_path): - raise IOError("Files do not exists in `{}`".format(oiio_path)) + log.debug("OIIOTool is not configured or not present at {}". + format(oiio_path)) + return False return True @@ -219,7 +225,12 @@ def should_decompress(file_url): Tests that 'file_url' is compressed with DWAA. Uses 'oiio_supported' to check that OIIO tool is available for this - platform + platform. + + Shouldn't throw exception as oiiotool is guarded by check function. + Currently implemented this way as there is no support for Mac and Linux + In the future, it should be more strict and throws exception on + misconfiguration. Args: file_url (str): path to rendered file (in sequence it would be @@ -227,6 +238,7 @@ def should_decompress(file_url): will be too) Returns: (bool): 'file_url' is DWAA compressed and should be decompressed + and we can decompress (oiiotool supported) """ if oiio_supported(): output = pype.api.subprocess([os.getenv("PYPE_OIIO_PATH"), diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py index 79b02ed01c..d9b12a5dba 100644 --- a/pype/plugins/global/publish/extract_burnin.py +++ b/pype/plugins/global/publish/extract_burnin.py @@ -58,16 +58,15 @@ class ExtractBurnin(pype.api.Extractor): def process(self, instance): # ffmpeg doesn't support multipart exrs if instance.data.get("multipartExr") is True: - if not oiio_supported(): - instance_label = ( - getattr(instance, "label", None) - or instance.data.get("label") - or instance.data.get("name") - ) - self.log.info(( - "Instance \"{}\" contain \"multipartExr\". Skipped." - ).format(instance_label)) - return + instance_label = ( + getattr(instance, "label", None) + or instance.data.get("label") + or instance.data.get("name") + ) + self.log.info(( + "Instance \"{}\" contain \"multipartExr\". Skipped." + ).format(instance_label)) + return # QUESTION what is this for and should we raise an exception? if "representations" not in instance.data: diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index 85bc60ddfc..f667382665 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -28,8 +28,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): do_decompress = False # ffmpeg doesn't support multipart exrs, use oiiotool if available if instance.data.get("multipartExr") is True: - if not oiio_supported(): - return + return # Skip review when requested. if not instance.data.get("review", True): @@ -107,7 +106,14 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): # run subprocess self.log.debug("{}".format(subprocess_jpeg)) - pype.api.subprocess(subprocess_jpeg, shell=True) + try: # temporary until oiiotool is supported cross platform + pype.api.subprocess(subprocess_jpeg, shell=True) + except RuntimeError as exp: + if "Compression" in str(exp): + self.log.debug("Unsupported compression on input files. " + + "Skipping!!!") + return + raise if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 26e60fbd48..e0caba1a20 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -60,9 +60,7 @@ class ExtractReview(pyblish.api.InstancePlugin): return # ffmpeg doesn't support multipart exrs - if instance.data.get("multipartExr") is True \ - and not oiio_supported(): - + if instance.data.get("multipartExr") is True: instance_label = ( getattr(instance, "label", None) or instance.data.get("label") @@ -192,9 +190,17 @@ class ExtractReview(pyblish.api.InstancePlugin): temp_data = self.prepare_temp_data(instance, repre, output_def) - ffmpeg_args = self._ffmpeg_arguments( - output_def, instance, new_repre, temp_data - ) + try: # temporary until oiiotool is supported cross platform + ffmpeg_args = self._ffmpeg_arguments( + output_def, instance, new_repre, temp_data + ) + except ZeroDivisionError: + if 'exr' in temp_data["origin_repre"]["ext"]: + self.log.debug("Unsupported compression on input " + + "files. Skipping!!!") + return + raise + subprcs_cmd = " ".join(ffmpeg_args) # run subprocess @@ -338,11 +344,11 @@ class ExtractReview(pyblish.api.InstancePlugin): if isinstance(new_repre['files'], list): input_files_urls = [os.path.join(new_repre["stagingDir"], f) for f in new_repre['files']] - do_decompress = should_decompress(input_files_urls[0]) + test_path = input_files_urls[0] else: test_path = os.path.join( new_repre["stagingDir"], new_repre['files']) - do_decompress = should_decompress(test_path) + do_decompress = should_decompress(test_path) if do_decompress: # change stagingDir, decompress first From 2ca672b7b78ec457b9a4f480ba127078738d74b0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Dec 2020 22:06:53 +0100 Subject: [PATCH 005/106] Hound --- pype/lib/plugin_tools.py | 7 ++++--- pype/plugins/global/publish/extract_burnin.py | 2 +- pype/plugins/global/publish/extract_jpeg.py | 2 +- pype/plugins/global/publish/extract_review.py | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pype/lib/plugin_tools.py b/pype/lib/plugin_tools.py index c2b938c9bb..d310ac2b8d 100644 --- a/pype/lib/plugin_tools.py +++ b/pype/lib/plugin_tools.py @@ -241,9 +241,10 @@ def should_decompress(file_url): and we can decompress (oiiotool supported) """ if oiio_supported(): - output = pype.api.subprocess([os.getenv("PYPE_OIIO_PATH"), - "--info", "-v", file_url]) + output = pype.api.subprocess([ + os.getenv("PYPE_OIIO_PATH"), + "--info", "-v", file_url]) return "compression: \"dwaa\"" in output or \ - "compression: \"dwab\"" in output + "compression: \"dwab\"" in output return False diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py index d9b12a5dba..f776846eab 100644 --- a/pype/plugins/global/publish/extract_burnin.py +++ b/pype/plugins/global/publish/extract_burnin.py @@ -6,7 +6,7 @@ import tempfile import pype.api import pyblish -from pype.lib import oiio_supported, should_decompress, \ +from pype.lib import should_decompress, \ get_decompress_dir, decompress import shutil diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index f667382665..af90d4366d 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -3,7 +3,7 @@ import os import pyblish.api import pype.api import pype.lib -from pype.lib import oiio_supported, should_decompress, \ +from pype.lib import should_decompress, \ get_decompress_dir, decompress import shutil diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index e0caba1a20..37fe83bf10 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -6,7 +6,7 @@ import pyblish.api import clique import pype.api import pype.lib -from pype.lib import oiio_supported, should_decompress, \ +from pype.lib import should_decompress, \ get_decompress_dir, decompress From 636b5eaead345be0ca036b093c00d678e443968f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Dec 2020 13:37:40 +0100 Subject: [PATCH 006/106] implemented `ITrayAction` --- pype/modules/base.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pype/modules/base.py b/pype/modules/base.py index 3c2c2e7e21..074fd43429 100644 --- a/pype/modules/base.py +++ b/pype/modules/base.py @@ -124,6 +124,41 @@ class ITrayModule: pass +class ITrayAction(ITrayModule): + """Implementation of Tray action. + + Add action to tray menu which will trigger `on_action_trigger`. + It is expected to be used for showing tools. + + Methods `tray_start`, `tray_exit` and `connect_with_modules` are overriden + as it's not expected that action will use them. But it is possible if + necessary. + """ + + @property + @abstractmethod + def label(self): + """Service label showed in menu.""" + pass + + @abstractmethod + def on_action_trigger(self): + """What happens on actions click.""" + pass + + def tray_menu(self, tray_menu): + from Qt import QtWidgets + action = QtWidgets.QAction(self.label, tray_menu) + action.triggered.connect(self.on_action_trigger) + tray_menu.addAction(action) + + def tray_start(self): + return + + def tray_exit(self): + return + + class ITrayService(ITrayModule): # Module's property menu_action = None From 7b54264804a2cf8ae4278a7a1b0e43cc4ae58da1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Dec 2020 13:38:26 +0100 Subject: [PATCH 007/106] SettingsModule renamed to SettingsAction and inherit from ITrayAction instead of ITrayModule --- ...{settings_module.py => settings_action.py} | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) rename pype/modules/{settings_module.py => settings_action.py} (58%) diff --git a/pype/modules/settings_module.py b/pype/modules/settings_action.py similarity index 58% rename from pype/modules/settings_module.py rename to pype/modules/settings_action.py index 0651170148..0d56a6c5ae 100644 --- a/pype/modules/settings_module.py +++ b/pype/modules/settings_action.py @@ -1,11 +1,13 @@ -from . import PypeModule, ITrayModule +from . import PypeModule, ITrayAction -class SettingsModule(PypeModule, ITrayModule): +class SettingsAction(PypeModule, ITrayAction): + """Action to show Setttings tool.""" name = "settings" + label = "Settings" def initialize(self, _modules_settings): - # This module is always enabled + # This action is always enabled self.enabled = True # User role @@ -18,13 +20,28 @@ class SettingsModule(PypeModule, ITrayModule): def connect_with_modules(self, *_a, **_kw): return + def tray_init(self): + """Initialization in tray implementation of ITrayAction.""" + self.create_settings_window() + + def on_action_trigger(self): + """Implementation for action trigger of ITrayAction.""" + self.show_settings_window() + def create_settings_window(self): + """Initializa Settings Qt window.""" if self.settings_window: return from pype.tools.settings import MainWidget self.settings_window = MainWidget(self.user_role) def show_settings_window(self): + """Show settings tool window. + + Raises: + AssertionError: Window must be already created. Call + `create_settings_window` before callint this method. + """ if not self.settings_window: raise AssertionError("Window is not initialized.") @@ -33,21 +50,3 @@ class SettingsModule(PypeModule, ITrayModule): # Pull window to the front. self.settings_window.raise_() self.settings_window.activateWindow() - - def tray_init(self): - self.create_settings_window() - - def tray_menu(self, tray_menu): - """Add **change credentials** option to tray menu.""" - from Qt import QtWidgets - - # Actions - action = QtWidgets.QAction("Settings", tray_menu) - action.triggered.connect(self.show_settings_window) - tray_menu.addAction(action) - - def tray_start(self): - return - - def tray_exit(self): - return From dd5dea2c04476c9206e29c23060ff92c4a3f708c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Dec 2020 13:38:40 +0100 Subject: [PATCH 008/106] converted StandAlonePublishModule to StandAlonePublishAction --- pype/modules/standalonepublish/__init__.py | 5 ----- ..._module.py => standalonepublish_action.py} | 19 +++++-------------- 2 files changed, 5 insertions(+), 19 deletions(-) delete mode 100644 pype/modules/standalonepublish/__init__.py rename pype/modules/{standalonepublish/standalonepublish_module.py => standalonepublish_action.py} (70%) diff --git a/pype/modules/standalonepublish/__init__.py b/pype/modules/standalonepublish/__init__.py deleted file mode 100644 index 5c40deb6f0..0000000000 --- a/pype/modules/standalonepublish/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .standalonepublish_module import StandAlonePublishModule - -__all__ = ( - "StandAlonePublishModule", -) diff --git a/pype/modules/standalonepublish/standalonepublish_module.py b/pype/modules/standalonepublish_action.py similarity index 70% rename from pype/modules/standalonepublish/standalonepublish_module.py rename to pype/modules/standalonepublish_action.py index 5b0cfe14bf..0e7bcfd86e 100644 --- a/pype/modules/standalonepublish/standalonepublish_module.py +++ b/pype/modules/standalonepublish_action.py @@ -1,15 +1,15 @@ import os import sys import subprocess -import pype -from .. import PypeModule, ITrayModule +from . import PypeModule, ITrayAction -class StandAlonePublishModule(PypeModule, ITrayModule): +class StandAlonePublishAction(PypeModule, ITrayAction): menu_label = "Publish" name = "standalonepublish_tool" def initialize(self, modules_settings): + import pype self.enabled = modules_settings[self.name]["enabled"] self.publish_paths = [ os.path.join( @@ -20,17 +20,8 @@ class StandAlonePublishModule(PypeModule, ITrayModule): def tray_init(self): return - def tray_start(self): - return - - def tray_exit(self): - return - - def tray_menu(self, parent_menu): - from Qt import QtWidgets - run_action = QtWidgets.QAction(self.menu_label, parent_menu) - run_action.triggered.connect(self.run_standalone_publisher) - parent_menu.addAction(run_action) + def on_action_trigger(self): + self.run_standalone_publisher() def connect_with_modules(self, enabled_modules): """Collect publish paths from other modules.""" From 5db24e34ae9860a1518c4000319a507d97fc0564 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Dec 2020 13:39:17 +0100 Subject: [PATCH 009/106] extracted launcher from avalon module to launcher action --- pype/modules/avalon_apps/avalon_app.py | 33 +------------------- pype/modules/base.py | 1 + pype/modules/launcher_action.py | 43 ++++++++++++++++++++++++++ pype/tools/launcher/actions.py | 14 ++++++--- 4 files changed, 54 insertions(+), 37 deletions(-) create mode 100644 pype/modules/launcher_action.py diff --git a/pype/modules/avalon_apps/avalon_app.py b/pype/modules/avalon_apps/avalon_app.py index 683d804412..d00a306e9e 100644 --- a/pype/modules/avalon_apps/avalon_app.py +++ b/pype/modules/avalon_apps/avalon_app.py @@ -44,7 +44,6 @@ class AvalonModule(PypeModule, ITrayModule, IRestApi): ) # Tray attributes - self.app_launcher = None self.libraryloader = None self.rest_api_obj = None @@ -99,29 +98,8 @@ class AvalonModule(PypeModule, ITrayModule, IRestApi): exc_info=True ) - # Add launcher - try: - from pype.tools.launcher import LauncherWindow - self.app_launcher = LauncherWindow() - except Exception: - self.log.warning( - "Couldn't load Launch for tray.", - exc_info=True - ) - def connect_with_modules(self, _enabled_modules): - plugin_paths = self.manager.collect_plugin_paths()["actions"] - if plugin_paths: - env_paths_str = os.environ.get("AVALON_ACTIONS") or "" - env_paths = env_paths_str.split(os.pathsep) - env_paths.extend(plugin_paths) - os.environ["AVALON_ACTIONS"] = os.pathsep.join(env_paths) - - if self.tray_initialized: - from pype.tools.launcher import actions - # actions.register_default_actions() - actions.register_config_actions() - actions.register_environment_actions() + return def rest_api_initialization(self, rest_api_module): if self.tray_initialized: @@ -132,15 +110,12 @@ class AvalonModule(PypeModule, ITrayModule, IRestApi): def tray_menu(self, tray_menu): from Qt import QtWidgets # Actions - action_launcher = QtWidgets.QAction("Launcher", tray_menu) action_library_loader = QtWidgets.QAction( "Library loader", tray_menu ) - action_launcher.triggered.connect(self.show_launcher) action_library_loader.triggered.connect(self.show_library_loader) - tray_menu.addAction(action_launcher) tray_menu.addAction(action_library_loader) def tray_start(self, *_a, **_kw): @@ -149,12 +124,6 @@ class AvalonModule(PypeModule, ITrayModule, IRestApi): def tray_exit(self, *_a, **_kw): return - def show_launcher(self): - # if app_launcher don't exist create it/otherwise only show main window - self.app_launcher.show() - self.app_launcher.raise_() - self.app_launcher.activateWindow() - def show_library_loader(self): self.libraryloader.show() diff --git a/pype/modules/base.py b/pype/modules/base.py index 074fd43429..43ed570e19 100644 --- a/pype/modules/base.py +++ b/pype/modules/base.py @@ -422,6 +422,7 @@ class TrayModulesManager(ModulesManager): "user", "ftrack", "muster", + "launcher_tool", "avalon", "clockify", "standalonepublish_tool", diff --git a/pype/modules/launcher_action.py b/pype/modules/launcher_action.py new file mode 100644 index 0000000000..bc6ad507c4 --- /dev/null +++ b/pype/modules/launcher_action.py @@ -0,0 +1,43 @@ +from . import PypeModule, ITrayAction + + +class LauncherAction(PypeModule, ITrayAction): + label = "Launcher" + name = "launcher_tool" + + def initialize(self, _modules_settings): + # This module is always enabled + self.enabled = True + + # Tray attributes + self.window = None + + def tray_init(self): + self.create_window() + + def tray_start(self): + # Register actions + from pype.tools.launcher import actions + # actions.register_default_actions() + actions.register_config_actions() + actions_paths = self.manager.collect_plugin_paths()["actions"] + actions.register_actions_from_paths(actions_paths) + actions.register_environment_actions() + + def connect_with_modules(self, _enabled_modules): + return + + def create_window(self): + if self.window: + return + from pype.tools.launcher import LauncherWindow + self.window = LauncherWindow() + + def on_action_trigger(self): + self.show_launcher() + + def show_launcher(self): + if self.window: + self.window.show() + self.window.raise_() + self.window.activateWindow() diff --git a/pype/tools/launcher/actions.py b/pype/tools/launcher/actions.py index 6d0c94b676..db50c0c859 100644 --- a/pype/tools/launcher/actions.py +++ b/pype/tools/launcher/actions.py @@ -85,14 +85,11 @@ def register_config_actions(): config.register_launcher_actions() -def register_environment_actions(): - """Register actions from AVALON_ACTIONS for Launcher.""" - - paths = os.environ.get("AVALON_ACTIONS") +def register_actions_from_paths(paths): if not paths: return - for path in paths.split(os.pathsep): + for path in paths: api.register_plugin_path(api.Action, path) # Run "register" if found. @@ -110,6 +107,13 @@ def register_environment_actions(): ) +def register_environment_actions(): + """Register actions from AVALON_ACTIONS for Launcher.""" + + paths = os.environ.get("AVALON_ACTIONS") or "" + register_actions_from_paths(paths.split(os.pathsep)) + + class ApplicationAction(api.Action): """Pype's application launcher From 98a745c7135d44bce110efd61e21d84c492ba81a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Dec 2020 13:39:34 +0100 Subject: [PATCH 010/106] added readme --- pype/modules/README.md | 103 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 pype/modules/README.md diff --git a/pype/modules/README.md b/pype/modules/README.md new file mode 100644 index 0000000000..818375461f --- /dev/null +++ b/pype/modules/README.md @@ -0,0 +1,103 @@ +# Pype modules +Pype modules should contain separated logic of specific kind of implementation, like Ftrack connection and usage code or Deadline farm rendering. + +## Base class `PypeModule` +- abstract class as base for each module +- implementation should be module's api withou GUI parts +- may implement `get_global_environments` method which should return dictionary of environments that are globally appliable and value is the same for whole studio if launched at any workstation (except os specific paths) +- abstract parts: + - `name` attribute - name of a module + - `initialize` method - method for own initialization of a module (should not override `__init__`) + - `connect_with_modules` method - where module may look for it's interfaces implementations or check for other modules +- `__init__` should not be overriden and `initialize` should not do time consuming part but only prepare base data about module + - also keep in mind that they may be initialized in headless mode +- connection with other modules is made with help of interfaces + +# Interfaces +- interface is class that has defined abstract methods to implement and may contain preimplemented helper methods +- module that inherit from an interface must implement those abstract methods otherwise won't be initialized +- it is easy to find which module object inherited from which interfaces withh 100% chance they have implemented required methods + +## Global interfaces +- few interfaces are implemented for global usage + +### IPluginPaths +- module want to add directory path/s to avalon or publish plugins +- module must implement `get_plugin_paths` which must return dictionary with possible keys `"publish"`, `"load"`, `"create"` or `"actions"` + - each key may contain list or string with path to directory with plugins + +### ITrayModule +- module has more logic when used in tray + - it is possible that module can be used only in tray +- abstract methods + - `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_modules` + - `tray_menu` - add actions to tray widget's menu that represent the module + - `tray_start` - start of module's login in tray + - module is initialized and connected with other modules + - `tray_exit` - module's cleanup like stop and join threads etc. + - order of calling is based on implementation this order is how it works with `TrayModulesManager` + - it is recommended to import and use GUI implementaion only in these methods +- has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayModulesManager` to True after `tray_init` + - if module has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations + +### ITrayService +- inherit from `ITrayModule` and implement `tray_menu` method for you + - add action to submenu "Services" in tray widget menu with icon and label +- abstract atttribute `label` + - label shown in menu +- interface has preimplemented methods to change icon color + - `set_service_running` - green icon + - `set_service_failed` - red icon + - `set_service_idle` - orange icon + - these states must be set by module itself `set_service_running` is default state on initialization + +### ITrayAction +- inherit from `ITrayModule` and implement `tray_menu` method for you + - add action to tray widget menu with label +- abstract atttribute `label` + - label shown in menu +- abstract method `on_action_trigger` + - what should happen when action is triggered +- NOTE: It is good idea to implement logic in `on_action_trigger` to api method and trigger that methods on callbacks this gives ability to trigger that method outside tray + +## Modules interfaces +- modules may have defined their interfaces to be able recognize other modules that would want to use their features +- +### Example: +- Ftrack module has `IFtrackEventHandlerPaths` which helps to tell Ftrack module which of other modules want to add paths to server/user event handlers + - Clockify module use `IFtrackEventHandlerPaths` and return paths to clockify ftrack synchronizers + +- Clockify has more inharitance it's class definition looks like +``` +class ClockifyModule( + PypeModule, # Says it's Pype module so ModulesManager will try to initialize. + ITrayModule, # Says has special implementation when used in tray. + IPluginPaths, # Says has plugin paths that want to register (paths to clockify actions for launcher). + IFtrackEventHandlerPaths, # Says has Ftrack actions/events for user/server. + ITimersManager # Listen to other modules with timer and can trigger changes in other module timers through `TimerManager` module. +): +``` + +### ModulesManager +- collect module classes and tries to initialize them +- important attributes + - `modules` - list of available attributes + - `modules_by_id` - dictionary of modules mapped by their ids + - `modules_by_name` - dictionary of modules mapped by their names + - all these attributes contain all found modules even if are not enabled +- helper methods + - `collect_global_environments` to collect all global environments from enabled modules with calling `get_global_environments` on each of them + - `collect_plugin_paths` collect plugin paths from all enabled modules + - output is always dictionary with all keys and values as list + ``` + { + "publish": [], + "create": [], + "load": [], + "actions": [] + } + ``` + +### TrayModulesManager +- inherit from `ModulesManager` +- has specific implementations for Pype Tray tool and handle `ITrayModule` methods From 7153f131b6e8f2535e57ddaacce0c13d94fe5f70 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Dec 2020 13:39:40 +0100 Subject: [PATCH 011/106] fix imports --- pype/modules/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pype/modules/__init__.py b/pype/modules/__init__.py index 00303aafc6..49e158b55e 100644 --- a/pype/modules/__init__.py +++ b/pype/modules/__init__.py @@ -2,12 +2,13 @@ from .base import ( PypeModule, ITrayModule, + ITrayAction, ITrayService, IPluginPaths, ModulesManager, TrayModulesManager ) -from .settings_module import SettingsModule +from .settings_action import SettingsAction from .rest_api import ( RestApiModule, IRestApi @@ -25,6 +26,7 @@ from .timers_manager import ( ITimersManager ) from .avalon_apps import AvalonModule +from .launcher_action import LauncherAction from .ftrack import ( FtrackModule, IFtrackEventHandlerPaths @@ -32,19 +34,20 @@ from .ftrack import ( from .clockify import ClockifyModule from .logging import LoggingModule from .muster import MusterModule -from .standalonepublish import StandAlonePublishModule +from .standalonepublish_action import StandAlonePublishAction from .websocket_server import WebsocketModule __all__ = ( "PypeModule", "ITrayModule", + "ITrayAction", "ITrayService", "IPluginPaths", "ModulesManager", "TrayModulesManager", - "SettingsModule", + "SettingsAction", "UserModule", "IUserModule", @@ -59,6 +62,7 @@ __all__ = ( "IRestApi", "AvalonModule", + "LauncherAction", "FtrackModule", "IFtrackEventHandlerPaths", @@ -67,7 +71,7 @@ __all__ = ( "IdleManager", "LoggingModule", "MusterModule", - "StandAlonePublishModule", + "StandAlonePublishAction", "WebsocketModule" ) From 29cf951ecf6f0978d9a354536a978a21d5656005 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Dec 2020 17:43:45 +0100 Subject: [PATCH 012/106] fix standalone publish label --- pype/modules/standalonepublish_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/standalonepublish_action.py b/pype/modules/standalonepublish_action.py index 0e7bcfd86e..4bcb5b6018 100644 --- a/pype/modules/standalonepublish_action.py +++ b/pype/modules/standalonepublish_action.py @@ -5,7 +5,7 @@ from . import PypeModule, ITrayAction class StandAlonePublishAction(PypeModule, ITrayAction): - menu_label = "Publish" + label = "Publish" name = "standalonepublish_tool" def initialize(self, modules_settings): From d0e3caacf4ff0ac0e4576ddde0133c62a02e5269 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Dec 2020 17:56:44 +0100 Subject: [PATCH 013/106] moved registering of action to different spot because it cases huge issues when happens in `tray_start` --- pype/modules/launcher_action.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pype/modules/launcher_action.py b/pype/modules/launcher_action.py index bc6ad507c4..9c2120cf9a 100644 --- a/pype/modules/launcher_action.py +++ b/pype/modules/launcher_action.py @@ -16,17 +16,18 @@ class LauncherAction(PypeModule, ITrayAction): self.create_window() def tray_start(self): - # Register actions - from pype.tools.launcher import actions - # actions.register_default_actions() - actions.register_config_actions() - actions_paths = self.manager.collect_plugin_paths()["actions"] - actions.register_actions_from_paths(actions_paths) - actions.register_environment_actions() - - def connect_with_modules(self, _enabled_modules): return + def connect_with_modules(self, enabled_modules): + # Register actions + if self.tray_initialized: + from pype.tools.launcher import actions + # actions.register_default_actions() + actions.register_config_actions() + actions_paths = self.manager.collect_plugin_paths()["actions"] + actions.register_actions_from_paths(actions_paths) + actions.register_environment_actions() + def create_window(self): if self.window: return From 86ff90dad71e55f66ebc0d54b0c20a279c9f9080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 15 Dec 2020 18:58:52 +0100 Subject: [PATCH 014/106] handle referenced AOVs --- pype/hosts/maya/expected_files.py | 67 ++++++++++++------- pype/plugins/maya/create/create_render.py | 1 + pype/plugins/maya/publish/collect_render.py | 5 +- .../publish/validate_vray_referenced_aovs.py | 50 ++++++++++++++ 4 files changed, 98 insertions(+), 25 deletions(-) create mode 100644 pype/plugins/maya/publish/validate_vray_referenced_aovs.py diff --git a/pype/hosts/maya/expected_files.py b/pype/hosts/maya/expected_files.py index a2ddec1640..174876db4e 100644 --- a/pype/hosts/maya/expected_files.py +++ b/pype/hosts/maya/expected_files.py @@ -32,6 +32,9 @@ Attributes: ImagePrefixes (dict): Mapping between renderers and their respective image prefix atrribute names. +Todo: + Determine `multipart` from render instance. + """ import types @@ -94,6 +97,10 @@ class ExpectedFiles: multipart = False + def __init__(self, render_instance): + """Constructor.""" + self._render_instance = render_instance + def get(self, renderer, layer): """Get expected files for given renderer and render layer. @@ -114,15 +121,20 @@ class ExpectedFiles: renderSetup.instance().switchToLayerUsingLegacyName(layer) if renderer.lower() == "arnold": - return self._get_files(ExpectedFilesArnold(layer)) + return self._get_files(ExpectedFilesArnold(layer, + self._render_instance)) elif renderer.lower() == "vray": - return self._get_files(ExpectedFilesVray(layer)) + return self._get_files(ExpectedFilesVray( + layer, self._render_instance)) elif renderer.lower() == "redshift": - return self._get_files(ExpectedFilesRedshift(layer)) + return self._get_files(ExpectedFilesRedshift( + layer, self._render_instance)) elif renderer.lower() == "mentalray": - return self._get_files(ExpectedFilesMentalray(layer)) + return self._get_files(ExpectedFilesMentalray( + layer, self._render_instance)) elif renderer.lower() == "renderman": - return self._get_files(ExpectedFilesRenderman(layer)) + return self._get_files(ExpectedFilesRenderman( + layer, self._render_instance)) else: raise UnsupportedRendererException( "unsupported {}".format(renderer) @@ -149,9 +161,10 @@ class AExpectedFiles: layer = None multipart = False - def __init__(self, layer): + def __init__(self, layer, render_instance): """Constructor.""" self.layer = layer + self.render_instance = render_instance @abstractmethod def get_aovs(self): @@ -460,9 +473,9 @@ class ExpectedFilesArnold(AExpectedFiles): "maya": "", } - def __init__(self, layer): + def __init__(self, layer, render_instance): """Constructor.""" - super(ExpectedFilesArnold, self).__init__(layer) + super(ExpectedFilesArnold, self).__init__(layer, render_instance) self.renderer = "arnold" def get_aovs(self): @@ -531,9 +544,9 @@ class ExpectedFilesArnold(AExpectedFiles): class ExpectedFilesVray(AExpectedFiles): """Expected files for V-Ray renderer.""" - def __init__(self, layer): + def __init__(self, layer, render_instance): """Constructor.""" - super(ExpectedFilesVray, self).__init__(layer) + super(ExpectedFilesVray, self).__init__(layer, render_instance) self.renderer = "vray" def get_renderer_prefix(self): @@ -615,14 +628,22 @@ class ExpectedFilesVray(AExpectedFiles): default_ext = "exr" # filter all namespace prefixed AOVs - they are pulled in from - # references and are not rendered. - vr_aovs = [ - n - for n in cmds.ls( - type=["VRayRenderElement", "VRayRenderElementSet"] - ) - if len(n.split(":")) == 1 - ] + # references. Or leave them alone, based on preferences on render + # instance. + ref_aovs = self.render_instance.data.get( + "vrayUseReferencedAovs", False) or False + + if ref_aovs: + vr_aovs = cmds.ls( + type=["VRayRenderElement", "VRayRenderElementSet"]) + else: + vr_aovs = [ + n + for n in cmds.ls( + type=["VRayRenderElement", "VRayRenderElementSet"] + ) + if len(n.split(":")) == 1 + ] for aov in vr_aovs: enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov))) @@ -696,9 +717,9 @@ class ExpectedFilesRedshift(AExpectedFiles): ext_mapping = ["iff", "exr", "tif", "png", "tga", "jpg"] - def __init__(self, layer): + def __init__(self, layer, render_instance): """Construtor.""" - super(ExpectedFilesRedshift, self).__init__(layer) + super(ExpectedFilesRedshift, self).__init__(layer, render_instance) self.renderer = "redshift" def get_renderer_prefix(self): @@ -815,9 +836,9 @@ class ExpectedFilesRenderman(AExpectedFiles): This is very rudimentary and needs more love and testing. """ - def __init__(self, layer): + def __init__(self, layer, render_instance): """Constructor.""" - super(ExpectedFilesRenderman, self).__init__(layer) + super(ExpectedFilesRenderman, self).__init__(layer, render_instance) self.renderer = "renderman" def get_aovs(self): @@ -880,7 +901,7 @@ class ExpectedFilesRenderman(AExpectedFiles): class ExpectedFilesMentalray(AExpectedFiles): """Skeleton unimplemented class for Mentalray renderer.""" - def __init__(self, layer): + def __init__(self, layer, render_instance): """Constructor. Raises: diff --git a/pype/plugins/maya/create/create_render.py b/pype/plugins/maya/create/create_render.py index fa0e269126..5a4f8f9dcb 100644 --- a/pype/plugins/maya/create/create_render.py +++ b/pype/plugins/maya/create/create_render.py @@ -189,6 +189,7 @@ class CreateRender(avalon.maya.Creator): self.data["tilesX"] = 2 self.data["tilesY"] = 2 self.data["convertToScanline"] = False + self.data["vrayUseReferencedAovs"] = False # Disable for now as this feature is not working yet # self.data["assScene"] = False diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index 3dde3b1592..0853473120 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -149,7 +149,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # return all expected files for all cameras and aovs in given # frame range - ef = ExpectedFiles() + ef = ExpectedFiles(render_instance) exp_files = ef.get(renderer, layer_name) self.log.info("multipart: {}".format(ef.multipart)) assert exp_files, "no file names were generated, this is bug" @@ -248,7 +248,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "tilesX": render_instance.data.get("tilesX") or 2, "tilesY": render_instance.data.get("tilesY") or 2, "priority": render_instance.data.get("priority"), - "convertToScanline": render_instance.data.get("convertToScanline") or False # noqa: E501 + "convertToScanline": render_instance.data.get("convertToScanline") or False, # noqa: E501 + "vrayUseReferencedAovs": render_instance.data.get("vrayUseReferencedAovs") or False # noqa: E501 } if self.sync_workfile_version: diff --git a/pype/plugins/maya/publish/validate_vray_referenced_aovs.py b/pype/plugins/maya/publish/validate_vray_referenced_aovs.py new file mode 100644 index 0000000000..923cb06263 --- /dev/null +++ b/pype/plugins/maya/publish/validate_vray_referenced_aovs.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +"""Validate if there are AOVs pulled from references.""" +import pyblish.api +import pype.api + +from maya import cmds + +import pype.hosts.maya.action + + +class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin): + """Validate whether the V-Ray Render Elements (AOVs) include references. + + This will check if there are AOVs pulled from references. If + `Vray Use Referenced Aovs` is checked on render instance, u must add those + manually to Render Elements as Pype will expect them to be rendered. + + """ + + order = pyblish.api.ValidatorOrder + label = 'VRay Referenced AOVs' + hosts = ['maya'] + families = ['renderlayer'] + actions = [pype.hosts.maya.action.SelectInvalidAction] + + def process(self, instance): + """Plugin main entry point.""" + if instance.data.get("renderer") != "vray": + # If not V-Ray ignore.. + return + + if not instance.data.get("vrayUseReferencedAovs"): + self.get_invalid(instance) + + @classmethod + def get_invalid(cls, instance): + """Find referenced AOVs in scene.""" + # those aovs with namespace prefix are coming from references + ref_aovs = [ + n for n in + cmds.ls(type=["VRayRenderElement", "VRayRenderElementSet"]) + if len(n.split(":")) > 1 + ] + + if ref_aovs: + cls.log.warning( + "Scene contain referenced AOVs: {}".format(ref_aovs)) + + # Return the instance itself + return ref_aovs From 5e8aca606ca36eefe0cadb65309de4f8ed85a89f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 15 Dec 2020 19:09:05 +0100 Subject: [PATCH 015/106] better check for referenced AOVs --- pype/hosts/maya/expected_files.py | 12 ++++-------- .../maya/publish/validate_vray_referenced_aovs.py | 11 +++++------ 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/pype/hosts/maya/expected_files.py b/pype/hosts/maya/expected_files.py index 174876db4e..9dd10e573e 100644 --- a/pype/hosts/maya/expected_files.py +++ b/pype/hosts/maya/expected_files.py @@ -635,15 +635,11 @@ class ExpectedFilesVray(AExpectedFiles): if ref_aovs: vr_aovs = cmds.ls( - type=["VRayRenderElement", "VRayRenderElementSet"]) + type=["VRayRenderElement", "VRayRenderElementSet"]) or [] else: - vr_aovs = [ - n - for n in cmds.ls( - type=["VRayRenderElement", "VRayRenderElementSet"] - ) - if len(n.split(":")) == 1 - ] + vr_aovs = cmds.ls( + type=["VRayRenderElement", "VRayRenderElementSet"], + referencedNodes=False) or [] for aov in vr_aovs: enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov))) diff --git a/pype/plugins/maya/publish/validate_vray_referenced_aovs.py b/pype/plugins/maya/publish/validate_vray_referenced_aovs.py index 923cb06263..0c1a5f552a 100644 --- a/pype/plugins/maya/publish/validate_vray_referenced_aovs.py +++ b/pype/plugins/maya/publish/validate_vray_referenced_aovs.py @@ -35,12 +35,11 @@ class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): """Find referenced AOVs in scene.""" - # those aovs with namespace prefix are coming from references - ref_aovs = [ - n for n in - cmds.ls(type=["VRayRenderElement", "VRayRenderElementSet"]) - if len(n.split(":")) > 1 - ] + + if cmds.getAttr("vraySettings.relements_usereferenced") == 0: + ref_aovs = cmds.ls( + type=["VRayRenderElement", "VRayRenderElementSet"], + referencedNodes=True) or [] if ref_aovs: cls.log.warning( From bdd23020be6ff0f9029c1db96b743bd530dd0137 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Dec 2020 17:07:23 +0100 Subject: [PATCH 016/106] added asset types filtering to settings for task to version event --- .../schema_project_ftrack.json | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json index ea01400e94..569bcdd976 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json @@ -183,20 +183,31 @@ "label": "Sync status from Task to Version", "checkbox_key": "enabled", "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "dict-modifiable", - "key": "mapping", - "object_type": + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "dict-modifiable", + "key": "mapping", + "object_type": + { + "type": "list", + "object_type": "text" + } + }, + { + "type": "label", + "label": "Limit status changes to entered asset types. Limitation is ignored if nothing is entered." + }, { "type": "list", + "key": "asset_types_filter", + "label": "Asset types (short)", "object_type": "text" } - }] + ] }, { "type": "dict", From 011bba8ba88b79716dfbc5c8f0169672bede0d99 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Dec 2020 17:07:47 +0100 Subject: [PATCH 017/106] added `join_query_keys` to task to version event handler --- pype/modules/ftrack/events/event_task_to_version_status.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index e07be67b18..86b9d8b690 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -31,6 +31,11 @@ class TaskToVersionStatus(BaseEvent): self.asset_types_of_focus = modified_asset_types_of_focus return super(TaskToVersionStatus, self).register(*args, **kwargs) + # TODO remove `join_query_keys` as it should be in `BaseHandler` + @staticmethod + def join_query_keys(keys): + """Helper to join keys to query.""" + return ",".join(["\"{}\"".format(key) for key in keys]) def is_event_invalid(self, session, event): # Cache user id of currently running session From 76fd63238b16fed014dd4029655c349d2581ffff Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Dec 2020 17:08:16 +0100 Subject: [PATCH 018/106] do not override `register` and removed attribute `asset_types_of_focus` --- .../events/event_task_to_version_status.py | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index 86b9d8b690..caf0b74038 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -8,29 +8,6 @@ class TaskToVersionStatus(BaseEvent): # Attribute for caching session user id _cached_user_id = None - # Presets usage - asset_types_of_focus = [] - - def register(self, *args, **kwargs): - # Skip registration if attribute `asset_types_of_focus` is not set - modified_asset_types_of_focus = list() - if self.asset_types_of_focus: - if isinstance(self.asset_types_of_focus, str): - self.asset_types_of_focus = [self.asset_types_of_focus] - - for asset_type_name in self.asset_types_of_focus: - modified_asset_types_of_focus.append( - asset_type_name.lower() - ) - - if not modified_asset_types_of_focus: - raise Exception(( - "Event handler \"{}\" does not" - " have set presets for attribute \"{}\"" - ).format(self.__class__.__name__, "asset_types_of_focus")) - - self.asset_types_of_focus = modified_asset_types_of_focus - return super(TaskToVersionStatus, self).register(*args, **kwargs) # TODO remove `join_query_keys` as it should be in `BaseHandler` @staticmethod def join_query_keys(keys): From b13465d6b023b8f2f82c3f6555452c01a61a55a8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Dec 2020 17:08:37 +0100 Subject: [PATCH 019/106] filtering split entities info by project id --- .../events/event_task_to_version_status.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index caf0b74038..b728414bed 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -40,15 +40,19 @@ class TaskToVersionStatus(BaseEvent): return user_id == self._cached_user_id def filter_event_entities(self, event): - # Filter if event contain relevant data + """Filter if event contain relevant data. + + Event cares only about changes of `statusid` on `entity_type` "Task". + """ + entities_info = event["data"].get("entities") if not entities_info: return - filtered_entities = [] + filtered_entity_info = collections.defaultdict(list) for entity_info in entities_info: # Care only about tasks - if entity_info.get("entityType") != "task": + if entity_info.get("entity_type") != "Task": continue # Care only about changes of status @@ -60,9 +64,17 @@ class TaskToVersionStatus(BaseEvent): ): continue - filtered_entities.append(entity_info) + # Get project id from entity info + project_id = None + for parent_item in reversed(entity_info["parents"]): + if parent_item["entityType"] == "show": + project_id = parent_item["entityId"] + break - return filtered_entities + if project_id: + filtered_entity_info[project_id].append(entity_info) + + return filtered_entity_info def _get_ent_path(self, entity): return "/".join( From 60f9dcb9efbb2e995ca6b9516b505f4c721025de Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Dec 2020 17:09:28 +0100 Subject: [PATCH 020/106] `asset_version_statuses` renamed to `get_asset_version_statuses` and expect project entity in argument --- .../events/event_task_to_version_status.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index b728414bed..71c7c41ecb 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -174,16 +174,30 @@ class TaskToVersionStatus(BaseEvent): exc_info=True ) - def asset_version_statuses(self, entity): - project_entity = self.get_project_from_entity(entity) + def get_asset_version_statuses(self, project_entity): + """Status entities for AssetVersion from project's schema. + + Load statuses from project's schema and store them by id and name. + + Args: + project_entity (ftrack_api.Entity): Entity of ftrack's project. + + Returns: + tuple: 2 items are returned first are statuses by name + second are statuses by id. + """ project_schema = project_entity["project_schema"] # Get all available statuses for Task statuses = project_schema.get_statuses("AssetVersion") # map lowered status name with it's object - av_statuses_by_low_name = { - status["name"].lower(): status for status in statuses - } - return av_statuses_by_low_name + av_statuses_by_low_name = {} + av_statuses_by_id = {} + for status in statuses: + av_statuses_by_low_name[status["name"].lower()] = status + av_statuses_by_id[status["id"]] = status + + return av_statuses_by_low_name, av_statuses_by_id + def last_asset_version_by_task_id(self, asset_versions, task_ids): last_asset_version_by_task_id = collections.defaultdict(list) From 2bc2acc5185e3a2a2935266c8610ea1bd6083fef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Dec 2020 17:09:58 +0100 Subject: [PATCH 021/106] `last_asset_version_by_task_id` renamed to `find_last_asset_versions_for_task_ids` --- pype/modules/ftrack/events/event_task_to_version_status.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index 71c7c41ecb..79fd1e5fe2 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -198,8 +198,10 @@ class TaskToVersionStatus(BaseEvent): return av_statuses_by_low_name, av_statuses_by_id + def find_last_asset_versions_for_task_ids( + self, session, task_ids, asset_types_filter + ): - def last_asset_version_by_task_id(self, asset_versions, task_ids): last_asset_version_by_task_id = collections.defaultdict(list) last_version_by_task_id = {} poping_entity_ids = set(task_ids) From f67203074c6b28115fd09b0ef45e21701761aa18 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Dec 2020 17:11:31 +0100 Subject: [PATCH 022/106] `find_last_asset_versions_for_task_ids` do much more than did --- .../events/event_task_to_version_status.py | 89 +++++++++++++++---- 1 file changed, 73 insertions(+), 16 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index 79fd1e5fe2..7026655fad 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -201,31 +201,88 @@ class TaskToVersionStatus(BaseEvent): def find_last_asset_versions_for_task_ids( self, session, task_ids, asset_types_filter ): + """Find latest AssetVersion entities for task. - last_asset_version_by_task_id = collections.defaultdict(list) - last_version_by_task_id = {} - poping_entity_ids = set(task_ids) - for asset_version in asset_versions: - asset_type_name_low = ( - asset_version["asset"]["type"]["name"].lower() + Find first latest AssetVersion for task and all AssetVersions with + same version for the task. + + Args: + asset_versions (list): AssetVersion entities sorted by "version". + task_ids (list): Task ids. + asset_types_filter (list): Asset types short names that will be + used to filter AssetVersions. Filtering is skipped if entered + value is empty list. + """ + + # Allow event only on specific asset type names + asset_query_part = "" + if asset_types_filter: + # Query all AssetTypes + asset_types = session.query( + "select id, short from AssetType" + ).all() + # Store AssetTypes by id + asset_type_short_by_id = { + asset_type["id"]: asset_type["short"] + for asset_type in asset_types + } + + # Lower asset types from settings + # WARNING: not sure if is good idea to lower names as Ftrack may + # contain asset type with name "Scene" and "scene"! + asset_types_filter_low = set( + asset_types_name.lower() + for asset_types_name in asset_types_filter ) - if asset_type_name_low not in self.asset_types_of_focus: + asset_type_ids = [] + for type_id, short in asset_type_short_by_id.items(): + # TODO log if asset type name is not found + if short.lower() in asset_types_filter_low: + asset_type_ids.append(type_id) + + # TODO log that none of asset type names were found in ftrack + if asset_type_ids: + asset_query_part = " and asset.type_id in ({})".format( + self.join_query_keys(asset_type_ids) + ) + + # Query tasks' AssetVersions + asset_versions = session.query( + ( + "select status_id, version, task_id, asset_id" + " from AssetVersion where task_id in ({}){}}" + " order by version descending" + ).format(self.join_query_keys(task_ids), asset_query_part) + ).all() + + last_asset_versions_by_task_id = collections.defaultdict(list) + last_version_by_task_id = {} + not_finished_task_ids = set(task_ids) + for asset_version in asset_versions: + task_id = asset_version["task_id"] + # Check if task id is still in `not_finished_task_ids` + if task_id not in not_finished_task_ids: continue - task_id = asset_version["task_id"] + version = asset_version["version"] + + # Find last version in `last_version_by_task_id` last_version = last_version_by_task_id.get(task_id) if last_version is None: - last_version_by_task_id[task_id] = asset_version["version"] + # If task id does not have version set yet then it's first + # AssetVersion for this task + last_version_by_task_id[task_id] = version - elif last_version != asset_version["version"]: - poping_entity_ids.remove(task_id) + elif last_version > version: + # Skip processing if version is lower than last version + # and pop task id from `not_finished_task_ids` + not_finished_task_ids.remove(task_id) + continue - if not poping_entity_ids: - break + # Add AssetVersion entity to output dictionary + last_asset_versions_by_task_id[task_id].append(asset_version) - if task_id in poping_entity_ids: - last_asset_version_by_task_id[task_id].append(asset_version) - return last_asset_version_by_task_id + return last_asset_versions_by_task_id def register(session, plugins_presets): From 48aa0f215e811fa7ad32806d229b515a91c75712 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Dec 2020 17:11:56 +0100 Subject: [PATCH 023/106] processing is using project settings --- .../events/event_task_to_version_status.py | 158 +++++++++++++----- 1 file changed, 117 insertions(+), 41 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index 7026655fad..392adce690 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -90,86 +90,162 @@ class TaskToVersionStatus(BaseEvent): if not filtered_entity_infos: return - task_ids = [ - entity_info["entityId"] - for entity_info in filtered_entity_infos - ] - joined_ids = ",".join( - ["\"{}\"".format(entity_id) for entity_id in task_ids] - ) + for project_id, entities_info in filtered_entity_infos.items(): + self.process_by_project(session, event, project_id, entities_info) - # Query tasks' AssetVersions - asset_versions = session.query(( - "AssetVersion where task_id in ({}) order by version descending" - ).format(joined_ids)).all() - - last_asset_version_by_task_id = ( - self.last_asset_version_by_task_id(asset_versions, task_ids) - ) - if not last_asset_version_by_task_id: + def process_by_project(self, session, event, project_id, entities_info): + if not entities_info: return + project_entity = self.get_project_entity_from_event( + session, event, project_id + ) + project_settings = self.get_settings_for_project( + session, event, project_entity=project_entity + ) + + project_name = project_entity["full_name"] + event_settings = ( + project_settings["ftrack"]["events"][self.settings_key] + ) + _status_mapping = event_settings["mapping"] + if not event_settings["enabled"] or not _status_mapping: + self.log.debug("Project \"{}\" has disabled {}.".format( + project_name, self.__class__.__name__ + )) + return + + status_mapping = { + key.lower(): value + for key, value in _status_mapping.items() + } + + asset_types_filter = event_settings["asset_types_filter"] + + task_ids = [ + entity_info["entityId"] + for entity_info in entities_info + ] + + last_asset_versions_by_task_id = ( + self.find_last_asset_versions_for_task_ids( + session, task_ids, asset_types_filter + ) + ) + # Query Task entities for last asset versions - joined_filtered_ids = ",".join([ - "\"{}\"".format(entity_id) - for entity_id in last_asset_version_by_task_id.keys() - ]) + joined_filtered_ids = self.join_query_keys( + last_asset_versions_by_task_id.keys() + ) + if not joined_filtered_ids: + return + task_entities = session.query( - "Task where id in ({})".format(joined_filtered_ids) + "select status_id, link from Task where id in ({})".format( + joined_filtered_ids + ) ).all() if not task_entities: return + status_ids = set() + for task_entity in task_entities: + status_ids.add(task_entity["status_id"]) + + task_status_entities = session.query( + "select id, name from Status where id in ({})".format( + self.join_query_keys(status_ids) + ) + ).all() + task_status_name_by_id = { + status_entity["id"]: status_entity["name"] + for status_entity in task_status_entities + } + # Final process of changing statuses - av_statuses_by_low_name = self.asset_version_statuses(task_entities[0]) + av_statuses_by_low_name, av_statuses_by_id = ( + self.get_asset_version_statuses(project_entity) + ) + + asset_ids = set() + for asset_versions in last_asset_versions_by_task_id.values(): + for asset_version in asset_versions: + asset_ids.add(asset_version["asset_id"]) + + asset_entities = session.qeury( + "select name from Asset where id in ({}})".format( + self.join_query_keys(asset_ids) + ) + ).all() + asset_names_by_id = { + asset_entity["id"]: asset_entity["name"] + for asset_entity in asset_entities + } for task_entity in task_entities: task_id = task_entity["id"] + status_id = task_entity["status_id"] task_path = self._get_ent_path(task_entity) - task_status_name = task_entity["status"]["name"] + + task_status_name = task_status_name_by_id[status_id] task_status_name_low = task_status_name.lower() - last_asset_versions = last_asset_version_by_task_id[task_id] - for last_asset_version in last_asset_versions: - self.log.debug(( - "Trying to change status of last AssetVersion {}" - " for task \"{}\"" - ).format(last_asset_version["version"], task_path)) + new_asset_version_status = None + mapped_status_names = status_mapping.get(task_status_name_low) + for status_name in mapped_status_names: + _status = av_statuses_by_low_name.get(status_name.lower()) + if _status: + new_asset_version_status = _status + break + if not new_asset_version_status: new_asset_version_status = av_statuses_by_low_name.get( task_status_name_low ) - # Skip if tasks status is not available to AssetVersion - if not new_asset_version_status: - self.log.debug(( - "AssetVersion does not have matching status to \"{}\"" - ).format(task_status_name)) - continue + # Skip if tasks status is not available to AssetVersion + if not new_asset_version_status: + self.log.debug(( + "AssetVersion does not have matching status to \"{}\"" + ).format(task_status_name)) + continue + last_asset_versions = last_asset_versions_by_task_id[task_id] + for asset_version in last_asset_versions: + version = asset_version["version"] + self.log.debug(( + "Trying to change status of last AssetVersion {}" + " for task \"{}\"" + ).format(version, task_path)) + + asset_id = asset_version["asset_id"] + asset_type_name = asset_names_by_id[asset_id] av_ent_path = task_path + " Asset {} AssetVersion {}".format( - last_asset_version["asset"]["name"], - last_asset_version["version"] + asset_type_name, + version ) # Skip if current AssetVersion's status is same - current_status_name = last_asset_version["status"]["name"] + status_id = asset_version["status_id"] + current_status_name = av_statuses_by_id[status_id]["name"] if current_status_name.lower() == task_status_name_low: self.log.debug(( "AssetVersion already has set status \"{}\". \"{}\"" ).format(current_status_name, av_ent_path)) continue + new_status_id = new_asset_version_status["id"] + new_status_name = new_asset_version_status["name"] # Change the status try: - last_asset_version["status"] = new_asset_version_status + asset_version["status_id"] = new_status_id session.commit() self.log.info("[ {} ] Status updated to [ {} ]".format( - av_ent_path, new_asset_version_status["name"] + av_ent_path, new_status_name )) except Exception: session.rollback() self.log.warning( "[ {} ]Status couldn't be set to \"{}\"".format( - av_ent_path, new_asset_version_status["name"] + av_ent_path, new_status_name ), exc_info=True ) From ebb9e259097779f9e45ef85fe60e4fd2d646a390 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Dec 2020 17:12:06 +0100 Subject: [PATCH 024/106] added docstring and `settings_key` --- .../modules/ftrack/events/event_task_to_version_status.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index 392adce690..c714abafa5 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -5,6 +5,8 @@ from pype.modules.ftrack import BaseEvent class TaskToVersionStatus(BaseEvent): """Changes status of task's latest AssetVersions on its status change.""" + settings_key = "status_task_to_version" + # Attribute for caching session user id _cached_user_id = None @@ -15,6 +17,12 @@ class TaskToVersionStatus(BaseEvent): return ",".join(["\"{}\"".format(key) for key in keys]) def is_event_invalid(self, session, event): + """Skip task status changes for session user changes. + + It is expected that there may be another event handler that set + version status to task in that case skip all events caused by same + user as session has to avoid infinite loop of status changes. + """ # Cache user id of currently running session if self._cached_user_id is None: session_user_entity = session.query( From c516650de8d594cd4202b126754e83f8bb163fbd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Dec 2020 17:13:02 +0100 Subject: [PATCH 025/106] adde defaults for key "asset_types_filter" --- pype/settings/defaults/project_settings/ftrack.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/settings/defaults/project_settings/ftrack.json b/pype/settings/defaults/project_settings/ftrack.json index 5481574ef8..711e381986 100644 --- a/pype/settings/defaults/project_settings/ftrack.json +++ b/pype/settings/defaults/project_settings/ftrack.json @@ -66,7 +66,8 @@ "Approved": [ "Complete" ] - } + }, + "asset_types_filter": [] }, "status_version_to_task": { "enabled": true, From 6fb3cfafdd92b8c63187cd64912bd87f8ef19e54 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 16 Dec 2020 18:44:04 +0100 Subject: [PATCH 026/106] repair action for validator, skip unwanted referenced aovs --- pype/hosts/maya/expected_files.py | 21 +++--- .../publish/validate_vray_referenced_aovs.py | 73 +++++++++++++++---- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/pype/hosts/maya/expected_files.py b/pype/hosts/maya/expected_files.py index 9dd10e573e..07b3f94aa0 100644 --- a/pype/hosts/maya/expected_files.py +++ b/pype/hosts/maya/expected_files.py @@ -627,19 +627,20 @@ class ExpectedFilesVray(AExpectedFiles): if default_ext == "exr (multichannel)" or default_ext == "exr (deep)": default_ext = "exr" - # filter all namespace prefixed AOVs - they are pulled in from - # references. Or leave them alone, based on preferences on render - # instance. - ref_aovs = self.render_instance.data.get( + # handle aovs from references + use_ref_aovs = self.render_instance.data.get( "vrayUseReferencedAovs", False) or False - if ref_aovs: - vr_aovs = cmds.ls( - type=["VRayRenderElement", "VRayRenderElementSet"]) or [] - else: - vr_aovs = cmds.ls( + # this will have list of all aovs no matter if they are coming from + # reference or not. + vr_aovs = cmds.ls( + type=["VRayRenderElement", "VRayRenderElementSet"]) or [] + if not use_ref_aovs: + ref_aovs = cmds.ls( type=["VRayRenderElement", "VRayRenderElementSet"], - referencedNodes=False) or [] + referencedNodes=True) or [] + # get difference + vr_aovs = list(set(vr_aovs) - set(ref_aovs)) for aov in vr_aovs: enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov))) diff --git a/pype/plugins/maya/publish/validate_vray_referenced_aovs.py b/pype/plugins/maya/publish/validate_vray_referenced_aovs.py index 0c1a5f552a..67d5ed558c 100644 --- a/pype/plugins/maya/publish/validate_vray_referenced_aovs.py +++ b/pype/plugins/maya/publish/validate_vray_referenced_aovs.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- """Validate if there are AOVs pulled from references.""" import pyblish.api -import pype.api - +import types from maya import cmds import pype.hosts.maya.action @@ -21,7 +20,7 @@ class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin): label = 'VRay Referenced AOVs' hosts = ['maya'] families = ['renderlayer'] - actions = [pype.hosts.maya.action.SelectInvalidAction] + actions = [pype.api.RepairContextAction] def process(self, instance): """Plugin main entry point.""" @@ -29,21 +28,65 @@ class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin): # If not V-Ray ignore.. return + ref_aovs = cmds.ls( + type=["VRayRenderElement", "VRayRenderElementSet"], + referencedNodes=True) + ref_aovs_enabled = ValidateVrayReferencedAOVs.maya_is_true( + cmds.getAttr("vraySettings.relements_usereferenced")) + if not instance.data.get("vrayUseReferencedAovs"): - self.get_invalid(instance) + if ref_aovs_enabled and ref_aovs: + self.log.warning(( + "Referenced AOVs are enabled in Vray " + "Render Settings and are detected in scene, but " + "Pype render instance option for referenced AOVs is " + "disabled. Those AOVs will be rendered but not published " + "by Pype." + )) + self.log.warning(", ".join(ref_aovs)) + else: + if not ref_aovs: + self.log.warning(( + "Use of referenced AOVs enabled but there are none " + "in the scene." + )) + if not ref_aovs_enabled: + self.log.error(( + "'Use referenced' not enabled in Vray Render Settings." + )) + raise AssertionError("Invalid render settings") @classmethod - def get_invalid(cls, instance): - """Find referenced AOVs in scene.""" + def repair(cls, context): - if cmds.getAttr("vraySettings.relements_usereferenced") == 0: - ref_aovs = cmds.ls( - type=["VRayRenderElement", "VRayRenderElementSet"], - referencedNodes=True) or [] + vray_settings = cmds.ls(type="VRaySettingsNode") + if not vray_settings: + node = cmds.createNode("VRaySettingsNode") + else: + node = vray_settings[0] - if ref_aovs: - cls.log.warning( - "Scene contain referenced AOVs: {}".format(ref_aovs)) + cmds.setAttr("{}.relements_usereferenced".format(node), True) - # Return the instance itself - return ref_aovs + + + @staticmethod + def maya_is_true(attr_val): + """Whether a Maya attr evaluates to True. + + When querying an attribute value from an ambiguous object the + Maya API will return a list of values, which need to be properly + handled to evaluate properly. + + Args: + attr_val (mixed): Maya attribute to be evaluated as bool. + + Returns: + bool: cast Maya attribute to Pythons boolean value. + + """ + if isinstance(attr_val, types.BooleanType): + return attr_val + elif isinstance(attr_val, (types.ListType, types.GeneratorType)): + return any(attr_val) + else: + return bool(attr_val) From 183058a6c4e836b5601399e89e0dd98198a12d55 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 16 Dec 2020 18:46:41 +0100 Subject: [PATCH 027/106] shut the hound up --- pype/plugins/maya/publish/validate_vray_referenced_aovs.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pype/plugins/maya/publish/validate_vray_referenced_aovs.py b/pype/plugins/maya/publish/validate_vray_referenced_aovs.py index 67d5ed558c..120677021d 100644 --- a/pype/plugins/maya/publish/validate_vray_referenced_aovs.py +++ b/pype/plugins/maya/publish/validate_vray_referenced_aovs.py @@ -58,7 +58,7 @@ class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin): @classmethod def repair(cls, context): - + """Repair action.""" vray_settings = cmds.ls(type="VRaySettingsNode") if not vray_settings: node = cmds.createNode("VRaySettingsNode") @@ -67,8 +67,6 @@ class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin): cmds.setAttr("{}.relements_usereferenced".format(node), True) - - @staticmethod def maya_is_true(attr_val): """Whether a Maya attr evaluates to True. From f7ed24889252d9c17fea82a68c02659c0d97e254 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Dec 2020 10:46:08 +0100 Subject: [PATCH 028/106] fixed environment paths registering --- pype/tools/launcher/actions.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pype/tools/launcher/actions.py b/pype/tools/launcher/actions.py index db50c0c859..5d52b44acd 100644 --- a/pype/tools/launcher/actions.py +++ b/pype/tools/launcher/actions.py @@ -110,8 +110,13 @@ def register_actions_from_paths(paths): def register_environment_actions(): """Register actions from AVALON_ACTIONS for Launcher.""" - paths = os.environ.get("AVALON_ACTIONS") or "" - register_actions_from_paths(paths.split(os.pathsep)) + paths_str = os.environ.get("AVALON_ACTIONS") or "" + paths = [] + for path in paths_str.split(os.pathsep): + if path and os.path.exists(path): + paths.append(path) + + register_actions_from_paths(paths) class ApplicationAction(api.Action): From 2c70a160e0d936cb75973f517ae50ef44ad1b878 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Dec 2020 10:52:04 +0100 Subject: [PATCH 029/106] added try except in connect with modules --- pype/modules/base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pype/modules/base.py b/pype/modules/base.py index 43ed570e19..525320f1a7 100644 --- a/pype/modules/base.py +++ b/pype/modules/base.py @@ -322,7 +322,13 @@ class ModulesManager: enabled_modules = self.get_enabled_modules() self.log.debug("Has {} enabled modules.".format(len(enabled_modules))) for module in enabled_modules: - module.connect_with_modules(enabled_modules) + try: + module.connect_with_modules(enabled_modules) + except Exception: + self.log.error( + "BUG: Module failed on connection with other modules.", + exc_info=True + ) def get_enabled_modules(self): """Enabled modules initialized by the manager. From 7798f24fa772c65bf1b822fe06f1825aa981a31a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Dec 2020 10:58:46 +0100 Subject: [PATCH 030/106] better fix of registering paths --- pype/tools/launcher/actions.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pype/tools/launcher/actions.py b/pype/tools/launcher/actions.py index 5d52b44acd..ad1a2d524b 100644 --- a/pype/tools/launcher/actions.py +++ b/pype/tools/launcher/actions.py @@ -90,6 +90,19 @@ def register_actions_from_paths(paths): return for path in paths: + if not path: + continue + + if path.startswith("."): + print(( + "BUG: Relative paths are not allowed for security reasons. {}" + ).format(path)) + continue + + if not os.path.exists(path): + print("Path was not found: {}".format(path)) + continue + api.register_plugin_path(api.Action, path) # Run "register" if found. @@ -111,12 +124,7 @@ def register_environment_actions(): """Register actions from AVALON_ACTIONS for Launcher.""" paths_str = os.environ.get("AVALON_ACTIONS") or "" - paths = [] - for path in paths_str.split(os.pathsep): - if path and os.path.exists(path): - paths.append(path) - - register_actions_from_paths(paths) + register_actions_from_paths(paths_str.split(os.pathsep)) class ApplicationAction(api.Action): From 0874853f63e25846839cef27a93c4e7ed8d6f797 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Dec 2020 11:09:44 +0100 Subject: [PATCH 031/106] actions do not `Run "register" if found.` --- pype/tools/launcher/actions.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/pype/tools/launcher/actions.py b/pype/tools/launcher/actions.py index ad1a2d524b..aefa190768 100644 --- a/pype/tools/launcher/actions.py +++ b/pype/tools/launcher/actions.py @@ -105,20 +105,6 @@ def register_actions_from_paths(paths): api.register_plugin_path(api.Action, path) - # Run "register" if found. - for module in lib.modules_from_path(path): - if "register" not in dir(module): - continue - - try: - module.register() - except Exception as e: - print( - "Register method in {0} failed: {1}".format( - module, str(e) - ) - ) - def register_environment_actions(): """Register actions from AVALON_ACTIONS for Launcher.""" From 72f6d3b4afa7d88cb05344e2551f2555416440e9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Dec 2020 11:09:56 +0100 Subject: [PATCH 032/106] module importing has security checks now --- pype/lib/python_module_tools.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pype/lib/python_module_tools.py b/pype/lib/python_module_tools.py index 2ce2f60dca..b5400c9981 100644 --- a/pype/lib/python_module_tools.py +++ b/pype/lib/python_module_tools.py @@ -18,10 +18,20 @@ def modules_from_path(folder_path): Returns: List of modules. """ + modules = [] + # Just skip and return empty list if path is not set + if not folder_path: + return modules + + # Do not allow relative imports + if folder_path.startswith("."): + log.warning(( + "BUG: Relative paths are not allowed for security reasons. {}" + ).format(folder_path)) + return modules folder_path = os.path.normpath(folder_path) - modules = [] if not os.path.isdir(folder_path): log.warning("Not a directory path: {}".format(folder_path)) return modules From b5faff6bba4a83085640bbd3b5525b056d927fce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Dec 2020 11:18:17 +0100 Subject: [PATCH 033/106] do not run boot on pype.py import until it's in `__main__` --- pype.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype.py b/pype.py index 992e0c35ba..769e8c8f6f 100644 --- a/pype.py +++ b/pype.py @@ -260,4 +260,5 @@ def get_info() -> list: return formatted -boot() +if __name__ == "__main__": + boot() From 4fbd70328a96d7d46a1cd0e52ead0621312b352b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Dec 2020 11:38:01 +0100 Subject: [PATCH 034/106] moved gui schema changes back --- .../projects_schema/schema_project_ftrack.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json index 70f578822a..588fce74c8 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json @@ -191,10 +191,21 @@ { "type": "dict-modifiable", "key": "mapping", - "object_type": { + "object_type": + { "type": "list", "object_type": "text" } + }, + { + "type": "label", + "label": "Limit status changes to entered asset types. Limitation is ignored if nothing is entered." + }, + { + "type": "list", + "key": "asset_types_filter", + "label": "Asset types (short)", + "object_type": "text" } ] }, From 128a86b3b0a46ba602398b62f4e4fd82d24e1cc0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Dec 2020 11:49:40 +0100 Subject: [PATCH 035/106] separated skip logs --- .../modules/ftrack/events/event_task_to_version_status.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index c714abafa5..3ed7ba94f7 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -117,12 +117,18 @@ class TaskToVersionStatus(BaseEvent): project_settings["ftrack"]["events"][self.settings_key] ) _status_mapping = event_settings["mapping"] - if not event_settings["enabled"] or not _status_mapping: + if not event_settings["enabled"]: self.log.debug("Project \"{}\" has disabled {}.".format( project_name, self.__class__.__name__ )) return + if not _status_mapping: + self.log.debug(( + "Project \"{}\" does not have set status mapping for {}." + ).format(project_name, self.__class__.__name__)) + return + status_mapping = { key.lower(): value for key, value in _status_mapping.items() From c8d6d120f5e12dd7f20fca1325e0711b50daab01 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Dec 2020 11:50:00 +0100 Subject: [PATCH 036/106] fixed query templates --- pype/modules/ftrack/events/event_task_to_version_status.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index 3ed7ba94f7..ab3b85827a 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -186,8 +186,8 @@ class TaskToVersionStatus(BaseEvent): for asset_version in asset_versions: asset_ids.add(asset_version["asset_id"]) - asset_entities = session.qeury( - "select name from Asset where id in ({}})".format( + asset_entities = session.query( + "select name from Asset where id in ({})".format( self.join_query_keys(asset_ids) ) ).all() @@ -340,7 +340,7 @@ class TaskToVersionStatus(BaseEvent): asset_versions = session.query( ( "select status_id, version, task_id, asset_id" - " from AssetVersion where task_id in ({}){}}" + " from AssetVersion where task_id in ({}){}" " order by version descending" ).format(self.join_query_keys(task_ids), asset_query_part) ).all() From fc6bc150801f0037c64aa2a5f24ccc25bedd6777 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Dec 2020 11:50:24 +0100 Subject: [PATCH 037/106] fixed missing status name in mapping --- .../ftrack/events/event_task_to_version_status.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index ab3b85827a..46e15e1214 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -205,11 +205,12 @@ class TaskToVersionStatus(BaseEvent): new_asset_version_status = None mapped_status_names = status_mapping.get(task_status_name_low) - for status_name in mapped_status_names: - _status = av_statuses_by_low_name.get(status_name.lower()) - if _status: - new_asset_version_status = _status - break + if mapped_status_names: + for status_name in mapped_status_names: + _status = av_statuses_by_low_name.get(status_name.lower()) + if _status: + new_asset_version_status = _status + break if not new_asset_version_status: new_asset_version_status = av_statuses_by_low_name.get( From ee3e840e4a152f52538632995214ae3c6648c0ee Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Dec 2020 11:50:48 +0100 Subject: [PATCH 038/106] removed `join_query_keys` from event as it is in base already --- pype/modules/ftrack/events/event_task_to_version_status.py | 6 ------ pype/modules/ftrack/events/event_thumbnail_updates.py | 6 ------ .../modules/ftrack/events/event_version_to_task_statuses.py | 6 ------ 3 files changed, 18 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index 46e15e1214..db5939104b 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -10,12 +10,6 @@ class TaskToVersionStatus(BaseEvent): # Attribute for caching session user id _cached_user_id = None - # TODO remove `join_query_keys` as it should be in `BaseHandler` - @staticmethod - def join_query_keys(keys): - """Helper to join keys to query.""" - return ",".join(["\"{}\"".format(key) for key in keys]) - def is_event_invalid(self, session, event): """Skip task status changes for session user changes. diff --git a/pype/modules/ftrack/events/event_thumbnail_updates.py b/pype/modules/ftrack/events/event_thumbnail_updates.py index 9d816a79e5..0044c5e21c 100644 --- a/pype/modules/ftrack/events/event_thumbnail_updates.py +++ b/pype/modules/ftrack/events/event_thumbnail_updates.py @@ -5,12 +5,6 @@ from pype.modules.ftrack import BaseEvent class ThumbnailEvents(BaseEvent): settings_key = "thumbnail_updates" - # TODO remove `join_query_keys` as it should be in `BaseHandler` - @staticmethod - def join_query_keys(keys): - """Helper to join keys to query.""" - return ",".join(["\"{}\"".format(key) for key in keys]) - def launch(self, session, event): """Updates thumbnails of entities from new AssetVersion.""" filtered_entities = self.filter_entities(event) diff --git a/pype/modules/ftrack/events/event_version_to_task_statuses.py b/pype/modules/ftrack/events/event_version_to_task_statuses.py index ed47d2f8a9..d094c2a8fd 100644 --- a/pype/modules/ftrack/events/event_version_to_task_statuses.py +++ b/pype/modules/ftrack/events/event_version_to_task_statuses.py @@ -14,12 +14,6 @@ class VersionToTaskStatus(BaseEvent): for project_id, entities_info in filtered_entities_info.items(): self.process_by_project(session, event, project_id, entities_info) - # TODO remove `join_query_keys` as it should be in `BaseHandler` - @staticmethod - def join_query_keys(keys): - """Helper to join keys to query.""" - return ",".join(["\"{}\"".format(key) for key in keys]) - def filter_entity_info(self, event): filtered_entity_info = {} for entity_info in event["data"].get("entities", []): From 52eafcd6680a8f2fbec65d849244e82da1641578 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Dec 2020 11:56:46 +0100 Subject: [PATCH 039/106] skip processing if new status id is same as asset version already has --- pype/modules/ftrack/events/event_task_to_version_status.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index db5939104b..adc8508f0f 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -243,6 +243,10 @@ class TaskToVersionStatus(BaseEvent): new_status_id = new_asset_version_status["id"] new_status_name = new_asset_version_status["name"] + # Skip if status is already same + if asset_version["status_id"] == new_status_id: + continue + # Change the status try: asset_version["status_id"] = new_status_id From c2a06d6f1913e1358ba4c615a9a3820c8443c65f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Dec 2020 12:06:42 +0100 Subject: [PATCH 040/106] hound fixes --- .../events/event_task_to_version_status.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index adc8508f0f..7fc9464478 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -212,9 +212,8 @@ class TaskToVersionStatus(BaseEvent): ) # Skip if tasks status is not available to AssetVersion if not new_asset_version_status: - self.log.debug(( - "AssetVersion does not have matching status to \"{}\"" - ).format(task_status_name)) + msg = "AssetVersion does not have matching status to \"{}\"" + self.log.debug(msg.format(task_status_name)) continue last_asset_versions = last_asset_versions_by_task_id[task_id] @@ -336,13 +335,13 @@ class TaskToVersionStatus(BaseEvent): ) # Query tasks' AssetVersions - asset_versions = session.query( - ( - "select status_id, version, task_id, asset_id" - " from AssetVersion where task_id in ({}){}" - " order by version descending" - ).format(self.join_query_keys(task_ids), asset_query_part) - ).all() + av_query = ( + "select status_id, version, task_id, asset_id" + " from AssetVersion where task_id in ({}){}" + " order by version descending" + ).format(self.join_query_keys(task_ids), asset_query_part) + + asset_versions = session.query(av_query).all() last_asset_versions_by_task_id = collections.defaultdict(list) last_version_by_task_id = {} From 7b285dcba0f83a15cfc38c8694656d9347bd7979 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Dec 2020 12:08:58 +0100 Subject: [PATCH 041/106] hound fixes once more (changed indent from 3 to 4) --- .../events/event_task_to_version_status.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index 7fc9464478..b49fd01a91 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -212,8 +212,9 @@ class TaskToVersionStatus(BaseEvent): ) # Skip if tasks status is not available to AssetVersion if not new_asset_version_status: - msg = "AssetVersion does not have matching status to \"{}\"" - self.log.debug(msg.format(task_status_name)) + self.log.debug(( + "AssetVersion does not have matching status to \"{}\"" + ).format(task_status_name)) continue last_asset_versions = last_asset_versions_by_task_id[task_id] @@ -335,13 +336,11 @@ class TaskToVersionStatus(BaseEvent): ) # Query tasks' AssetVersions - av_query = ( - "select status_id, version, task_id, asset_id" - " from AssetVersion where task_id in ({}){}" - " order by version descending" - ).format(self.join_query_keys(task_ids), asset_query_part) - - asset_versions = session.query(av_query).all() + asset_versions = session.query(( + "select status_id, version, task_id, asset_id" + " from AssetVersion where task_id in ({}){}" + " order by version descending" + ).format(self.join_query_keys(task_ids), asset_query_part)).all() last_asset_versions_by_task_id = collections.defaultdict(list) last_version_by_task_id = {} From 07ef600cfcee18902d53caa7224ef9a78151a2dc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 10:34:40 +0100 Subject: [PATCH 042/106] tray `main` function moved to pype_tray.py and imported in tray's `__init__.py` --- pype/tools/tray/__init__.py | 5 +++++ pype/tools/tray/__main__.py | 16 +++++----------- pype/tools/tray/pype_tray.py | 11 +++++++++++ 3 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 pype/tools/tray/__init__.py diff --git a/pype/tools/tray/__init__.py b/pype/tools/tray/__init__.py new file mode 100644 index 0000000000..38c59d2a43 --- /dev/null +++ b/pype/tools/tray/__init__.py @@ -0,0 +1,5 @@ +from .pype_tray import main + +__all__ = ( + "main", +) diff --git a/pype/tools/tray/__main__.py b/pype/tools/tray/__main__.py index a997e4302b..830cf45d0e 100644 --- a/pype/tools/tray/__main__.py +++ b/pype/tools/tray/__main__.py @@ -1,13 +1,7 @@ -import os -import sys +try: + from . import pype_tray +except ImportError: + import pype_tray -from . import pype_tray -app = pype_tray.PypeTrayApplication() -if os.name == "nt": - import ctypes - ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( - u"pype_tray" - ) - -sys.exit(app.exec_()) +pype_tray.main() diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index c8c04d229a..edb3ffa251 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -255,3 +255,14 @@ class PypeTrayApplication(QtWidgets.QApplication): QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint ) return splash + + +def main(): + app = PypeTrayApplication() + if os.name == "nt": + import ctypes + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + u"pype_tray" + ) + + sys.exit(app.exec_()) From 2130fa4995cb34be713953a78b370002f2f4d65d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 10:35:36 +0100 Subject: [PATCH 043/106] launch_tray in pype_commands just import tray and run it's main --- pype/pype_commands.py | 37 ++----------------------------------- 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/pype/pype_commands.py b/pype/pype_commands.py index f504728ca1..527e729e08 100644 --- a/pype/pype_commands.py +++ b/pype/pype_commands.py @@ -12,42 +12,9 @@ class PypeCommands: """ @staticmethod def launch_tray(debug=False): - from pype.lib import PypeLogger as Logger - from pype.lib import execute - if debug: - execute([ - sys.executable, - "-m", - "pype.tools.tray" - ]) - return + from pype.tools import tray - detached_process = 0x00000008 # noqa: N806 - - args = [sys.executable, "-m", "pype.tools.tray"] - if sys.platform.startswith('linux'): - subprocess.Popen( - args, - universal_newlines=True, - bufsize=1, - env=os.environ, - stdout=None, - stderr=None, - preexec_fn=os.setpgrp - ) - - if sys.platform == 'win32': - args = ["pythonw", "-m", "pype.tools.tray"] - subprocess.Popen( - args, - universal_newlines=True, - bufsize=1, - cwd=None, - env=os.environ, - stdout=open(Logger.get_file_path(), 'w+'), - stderr=subprocess.STDOUT, - creationflags=detached_process - ) + tray.main() @staticmethod def launch_settings_gui(dev): From cc6c752533dbce38ba9ea6ea5d7d7dd1bbc69cc4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 10:36:24 +0100 Subject: [PATCH 044/106] remove handlers from root logger to avoid doubled logs --- pype/lib/log.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pype/lib/log.py b/pype/lib/log.py index f64d677ea8..28bf82f352 100644 --- a/pype/lib/log.py +++ b/pype/lib/log.py @@ -378,6 +378,11 @@ class PypeLogger: Terminal.echo(line) _mongo_logging = False + # Remove root's StreamHandler + for hdlr in tuple(logger.root.handlers): + if isinstance(hdlr, logging.StreamHandler): + logger.root.removeHandler(hdlr) + return logger From c0e6d24ed55b21e6d0efac278057318b92abcbb5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 10:49:32 +0100 Subject: [PATCH 045/106] less invasive way of not using root logger --- pype/lib/log.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pype/lib/log.py b/pype/lib/log.py index 28bf82f352..47f379d952 100644 --- a/pype/lib/log.py +++ b/pype/lib/log.py @@ -378,10 +378,8 @@ class PypeLogger: Terminal.echo(line) _mongo_logging = False - # Remove root's StreamHandler - for hdlr in tuple(logger.root.handlers): - if isinstance(hdlr, logging.StreamHandler): - logger.root.removeHandler(hdlr) + # Do not propagate logs to root logger + logger.propagate = False return logger From 31e879af0891fa0bb0766b82a6a43245e21d5aad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 10:51:15 +0100 Subject: [PATCH 046/106] added comment --- pype/tools/tray/pype_tray.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index edb3ffa251..1fec95906d 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -259,6 +259,7 @@ class PypeTrayApplication(QtWidgets.QApplication): def main(): app = PypeTrayApplication() + # TODO remove when pype.exe will have an icon if os.name == "nt": import ctypes ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( From be9c4d4841ba830d4068d56f2314cf489ccdaf68 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 18 Dec 2020 12:47:32 +0100 Subject: [PATCH 047/106] Revert "Merge remote-tracking branch 'origin/2.x/develop' into develop" This reverts commit b38585243cba039d39571b8393d7be535cae8cc2. --- pype/hosts/maya/expected_files.py | 70 +++++------ pype/lib/__init__.py | 10 +- pype/lib/plugin_tools.py | 114 ------------------ pype/plugins/global/publish/extract_burnin.py | 29 +---- pype/plugins/global/publish/extract_jpeg.py | 35 +----- pype/plugins/global/publish/extract_review.py | 58 ++------- pype/plugins/maya/create/create_render.py | 1 - pype/plugins/maya/publish/collect_render.py | 5 +- .../publish/validate_vray_referenced_aovs.py | 90 -------------- 9 files changed, 49 insertions(+), 363 deletions(-) delete mode 100644 pype/plugins/maya/publish/validate_vray_referenced_aovs.py diff --git a/pype/hosts/maya/expected_files.py b/pype/hosts/maya/expected_files.py index d39e5fa204..52c8893e4b 100644 --- a/pype/hosts/maya/expected_files.py +++ b/pype/hosts/maya/expected_files.py @@ -32,9 +32,6 @@ Attributes: ImagePrefixes (dict): Mapping between renderers and their respective image prefix atrribute names. -Todo: - Determine `multipart` from render instance. - """ import types @@ -97,10 +94,6 @@ class ExpectedFiles: multipart = False - def __init__(self, render_instance): - """Constructor.""" - self._render_instance = render_instance - def get(self, renderer, layer): """Get expected files for given renderer and render layer. @@ -121,20 +114,15 @@ class ExpectedFiles: renderSetup.instance().switchToLayerUsingLegacyName(layer) if renderer.lower() == "arnold": - return self._get_files(ExpectedFilesArnold(layer, - self._render_instance)) + return self._get_files(ExpectedFilesArnold(layer)) elif renderer.lower() == "vray": - return self._get_files(ExpectedFilesVray( - layer, self._render_instance)) + return self._get_files(ExpectedFilesVray(layer)) elif renderer.lower() == "redshift": - return self._get_files(ExpectedFilesRedshift( - layer, self._render_instance)) + return self._get_files(ExpectedFilesRedshift(layer)) elif renderer.lower() == "mentalray": - return self._get_files(ExpectedFilesMentalray( - layer, self._render_instance)) + return self._get_files(ExpectedFilesMentalray(layer)) elif renderer.lower() == "renderman": - return self._get_files(ExpectedFilesRenderman( - layer, self._render_instance)) + return self._get_files(ExpectedFilesRenderman(layer)) else: raise UnsupportedRendererException( "unsupported {}".format(renderer) @@ -161,10 +149,9 @@ class AExpectedFiles: layer = None multipart = False - def __init__(self, layer, render_instance): + def __init__(self, layer): """Constructor.""" self.layer = layer - self.render_instance = render_instance @abstractmethod def get_aovs(self): @@ -473,9 +460,9 @@ class ExpectedFilesArnold(AExpectedFiles): "maya": "", } - def __init__(self, layer, render_instance): + def __init__(self, layer): """Constructor.""" - super(ExpectedFilesArnold, self).__init__(layer, render_instance) + super(ExpectedFilesArnold, self).__init__(layer) self.renderer = "arnold" def get_aovs(self): @@ -544,9 +531,9 @@ class ExpectedFilesArnold(AExpectedFiles): class ExpectedFilesVray(AExpectedFiles): """Expected files for V-Ray renderer.""" - def __init__(self, layer, render_instance): + def __init__(self, layer): """Constructor.""" - super(ExpectedFilesVray, self).__init__(layer, render_instance) + super(ExpectedFilesVray, self).__init__(layer) self.renderer = "vray" def get_renderer_prefix(self): @@ -627,25 +614,24 @@ class ExpectedFilesVray(AExpectedFiles): if default_ext == "exr (multichannel)" or default_ext == "exr (deep)": default_ext = "exr" - # add beauty as default enabled_aovs.append( (u"beauty", default_ext) ) - # handle aovs from references - use_ref_aovs = self.render_instance.data.get( - "vrayUseReferencedAovs", False) or False + if not self.maya_is_true( + cmds.getAttr("vraySettings.relements_enableall") + ): + return enabled_aovs - # this will have list of all aovs no matter if they are coming from - # reference or not. - vr_aovs = cmds.ls( - type=["VRayRenderElement", "VRayRenderElementSet"]) or [] - if not use_ref_aovs: - ref_aovs = cmds.ls( - type=["VRayRenderElement", "VRayRenderElementSet"], - referencedNodes=True) or [] - # get difference - vr_aovs = list(set(vr_aovs) - set(ref_aovs)) + # filter all namespace prefixed AOVs - they are pulled in from + # references and are not rendered. + vr_aovs = [ + n + for n in cmds.ls( + type=["VRayRenderElement", "VRayRenderElementSet"] + ) + if len(n.split(":")) == 1 + ] for aov in vr_aovs: enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov))) @@ -717,9 +703,9 @@ class ExpectedFilesRedshift(AExpectedFiles): ext_mapping = ["iff", "exr", "tif", "png", "tga", "jpg"] - def __init__(self, layer, render_instance): + def __init__(self, layer): """Construtor.""" - super(ExpectedFilesRedshift, self).__init__(layer, render_instance) + super(ExpectedFilesRedshift, self).__init__(layer) self.renderer = "redshift" def get_renderer_prefix(self): @@ -836,9 +822,9 @@ class ExpectedFilesRenderman(AExpectedFiles): This is very rudimentary and needs more love and testing. """ - def __init__(self, layer, render_instance): + def __init__(self, layer): """Constructor.""" - super(ExpectedFilesRenderman, self).__init__(layer, render_instance) + super(ExpectedFilesRenderman, self).__init__(layer) self.renderer = "renderman" def get_aovs(self): @@ -901,7 +887,7 @@ class ExpectedFilesRenderman(AExpectedFiles): class ExpectedFilesMentalray(AExpectedFiles): """Skeleton unimplemented class for Mentalray renderer.""" - def __init__(self, layer, render_instance): + def __init__(self, layer): """Constructor. Raises: diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 09cc998b7c..9444ef5195 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -56,11 +56,7 @@ from .plugin_tools import ( filter_pyblish_plugins, source_hash, get_unique_layer_name, - get_background_layers, - oiio_supported, - decompress, - get_decompress_dir, - should_decompress + get_background_layers ) from .user_settings import ( @@ -112,10 +108,6 @@ __all__ = [ "source_hash", "get_unique_layer_name", "get_background_layers", - "oiio_supported", - "decompress", - "get_decompress_dir", - "should_decompress", "version_up", "get_version_from_path", diff --git a/pype/lib/plugin_tools.py b/pype/lib/plugin_tools.py index c9441dbd88..13d311d96c 100644 --- a/pype/lib/plugin_tools.py +++ b/pype/lib/plugin_tools.py @@ -5,8 +5,6 @@ import inspect import logging import re import json -import pype.api -import tempfile from pype.settings import get_project_settings @@ -136,115 +134,3 @@ def get_background_layers(file_url): layer.get("filename")). replace("\\", "/")) return layers - - -def oiio_supported(): - """ - Checks if oiiotool is configured for this platform. - - Expects full path to executable. - - 'should_decompress' will throw exception if configured, - but not present or not working. - Returns: - (bool) - """ - oiio_path = os.getenv("PYPE_OIIO_PATH", "") - if not oiio_path or not os.path.exists(oiio_path): - log.debug("OIIOTool is not configured or not present at {}". - format(oiio_path)) - return False - - return True - - -def decompress(target_dir, file_url, - input_frame_start=None, input_frame_end=None, log=None): - """ - Decompresses DWAA 'file_url' .exr to 'target_dir'. - - Creates uncompressed files in 'target_dir', they need to be cleaned. - - File url could be for single file or for a sequence, in that case - %0Xd will be as a placeholder for frame number AND input_frame* will - be filled. - In that case single oiio command with '--frames' will be triggered for - all frames, this should be faster then looping and running sequentially - - Args: - target_dir (str): extended from stagingDir - file_url (str): full urls to source file (with or without %0Xd) - input_frame_start (int) (optional): first frame - input_frame_end (int) (optional): last frame - log (Logger) (optional): pype logger - """ - is_sequence = input_frame_start is not None and \ - input_frame_end is not None and \ - (int(input_frame_end) > int(input_frame_start)) - - oiio_cmd = [] - oiio_cmd.append(os.getenv("PYPE_OIIO_PATH")) - - oiio_cmd.append("--compression none") - - base_file_name = os.path.basename(file_url) - oiio_cmd.append(file_url) - - if is_sequence: - oiio_cmd.append("--frames {}-{}".format(input_frame_start, - input_frame_end)) - - oiio_cmd.append("-o") - oiio_cmd.append(os.path.join(target_dir, base_file_name)) - - subprocess_exr = " ".join(oiio_cmd) - - if not log: - log = logging.getLogger(__name__) - - log.debug("Decompressing {}".format(subprocess_exr)) - pype.api.subprocess( - subprocess_exr, shell=True, logger=log - ) - - -def get_decompress_dir(): - """ - Creates temporary folder for decompressing. - Its local, in case of farm it is 'local' to the farm machine. - - Should be much faster, needs to be cleaned up later. - """ - return os.path.normpath( - tempfile.mkdtemp(prefix="pyblish_tmp_") - ) - - -def should_decompress(file_url): - """ - Tests that 'file_url' is compressed with DWAA. - - Uses 'oiio_supported' to check that OIIO tool is available for this - platform. - - Shouldn't throw exception as oiiotool is guarded by check function. - Currently implemented this way as there is no support for Mac and Linux - In the future, it should be more strict and throws exception on - misconfiguration. - - Args: - file_url (str): path to rendered file (in sequence it would be - first file, if that compressed it is expected that whole seq - will be too) - Returns: - (bool): 'file_url' is DWAA compressed and should be decompressed - and we can decompress (oiiotool supported) - """ - if oiio_supported(): - output = pype.api.subprocess([ - os.getenv("PYPE_OIIO_PATH"), - "--info", "-v", file_url]) - return "compression: \"dwaa\"" in output or \ - "compression: \"dwab\"" in output - - return False diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py index d29af63483..501162b6a6 100644 --- a/pype/plugins/global/publish/extract_burnin.py +++ b/pype/plugins/global/publish/extract_burnin.py @@ -6,9 +6,6 @@ import tempfile import pype.api import pyblish -from pype.lib import should_decompress, \ - get_decompress_dir, decompress -import shutil class ExtractBurnin(pype.api.Extractor): @@ -31,8 +28,7 @@ class ExtractBurnin(pype.api.Extractor): "premiere", "standalonepublisher", "harmony", - "fusion", - "aftereffects" + "fusion" ] optional = True @@ -208,26 +204,6 @@ class ExtractBurnin(pype.api.Extractor): # Prepare paths and files for process. self.input_output_paths(new_repre, temp_data, filename_suffix) - decompressed_dir = '' - full_input_path = temp_data["full_input_path"] - do_decompress = should_decompress(full_input_path) - if do_decompress: - decompressed_dir = get_decompress_dir() - - decompress( - decompressed_dir, - full_input_path, - temp_data["frame_start"], - temp_data["frame_end"], - self.log - ) - - # input path changed, 'decompressed' added - input_file = os.path.basename(full_input_path) - temp_data["full_input_path"] = os.path.join( - decompressed_dir, - input_file) - # Data for burnin script script_data = { "input": temp_data["full_input_path"], @@ -287,9 +263,6 @@ class ExtractBurnin(pype.api.Extractor): os.remove(filepath) self.log.debug("Removed: \"{}\"".format(filepath)) - if do_decompress and os.path.exists(decompressed_dir): - shutil.rmtree(decompressed_dir) - def prepare_basic_data(self, instance): """Pick data from instance for processing and for burnin strings. diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index af90d4366d..551e57796a 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -3,9 +3,6 @@ import os import pyblish.api import pype.api import pype.lib -from pype.lib import should_decompress, \ - get_decompress_dir, decompress -import shutil class ExtractJpegEXR(pyblish.api.InstancePlugin): @@ -25,8 +22,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): if 'crypto' in instance.data['subset']: return - do_decompress = False - # ffmpeg doesn't support multipart exrs, use oiiotool if available + # ffmpeg doesn't support multipart exrs if instance.data.get("multipartExr") is True: return @@ -40,6 +36,10 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): # filter out mov and img sequences representations_new = representations[:] + if instance.data.get("multipartExr"): + # ffmpeg doesn't support multipart exrs + return + for repre in representations: tags = repre.get("tags", []) self.log.debug(repre) @@ -60,19 +60,6 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): full_input_path = os.path.join(stagingdir, input_file) self.log.info("input {}".format(full_input_path)) - decompressed_dir = '' - do_decompress = should_decompress(full_input_path) - if do_decompress: - decompressed_dir = get_decompress_dir() - - decompress( - decompressed_dir, - full_input_path) - # input path changed, 'decompressed' added - full_input_path = os.path.join( - decompressed_dir, - input_file) - filename = os.path.splitext(input_file)[0] if not filename.endswith('.'): filename += "." @@ -106,14 +93,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): # run subprocess self.log.debug("{}".format(subprocess_jpeg)) - try: # temporary until oiiotool is supported cross platform - pype.api.subprocess(subprocess_jpeg, shell=True) - except RuntimeError as exp: - if "Compression" in str(exp): - self.log.debug("Unsupported compression on input files. " + - "Skipping!!!") - return - raise + pype.api.subprocess(subprocess_jpeg, shell=True) if "representations" not in instance.data: instance.data["representations"] = [] @@ -131,7 +111,4 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): self.log.debug("Adding: {}".format(representation)) representations_new.append(representation) - if do_decompress and os.path.exists(decompressed_dir): - shutil.rmtree(decompressed_dir) - instance.data["representations"] = representations_new diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 37fe83bf10..aa8d8accb5 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -6,8 +6,6 @@ import pyblish.api import clique import pype.api import pype.lib -from pype.lib import should_decompress, \ - get_decompress_dir, decompress class ExtractReview(pyblish.api.InstancePlugin): @@ -16,7 +14,7 @@ class ExtractReview(pyblish.api.InstancePlugin): Compulsory attribute of representation is tags list with "review", otherwise the representation is ignored. - All new representations are created and encoded by ffmpeg following + All new represetnations are created and encoded by ffmpeg following presets found in `pype-config/presets/plugins/global/ publish.json:ExtractReview:outputs`. """ @@ -190,17 +188,9 @@ class ExtractReview(pyblish.api.InstancePlugin): temp_data = self.prepare_temp_data(instance, repre, output_def) - try: # temporary until oiiotool is supported cross platform - ffmpeg_args = self._ffmpeg_arguments( - output_def, instance, new_repre, temp_data - ) - except ZeroDivisionError: - if 'exr' in temp_data["origin_repre"]["ext"]: - self.log.debug("Unsupported compression on input " + - "files. Skipping!!!") - return - raise - + ffmpeg_args = self._ffmpeg_arguments( + output_def, instance, new_repre, temp_data + ) subprcs_cmd = " ".join(ffmpeg_args) # run subprocess @@ -328,9 +318,9 @@ class ExtractReview(pyblish.api.InstancePlugin): Args: output_def (dict): Currently processed output definition. instance (Instance): Currently processed instance. - new_repre (dict): Representation representing output of this + new_repre (dict): Reprensetation representing output of this process. - temp_data (dict): Base data for successful process. + temp_data (dict): Base data for successfull process. """ # Get FFmpeg arguments from profile presets @@ -341,35 +331,9 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_video_filters = out_def_ffmpeg_args.get("video_filters") or [] ffmpeg_audio_filters = out_def_ffmpeg_args.get("audio_filters") or [] - if isinstance(new_repre['files'], list): - input_files_urls = [os.path.join(new_repre["stagingDir"], f) for f - in new_repre['files']] - test_path = input_files_urls[0] - else: - test_path = os.path.join( - new_repre["stagingDir"], new_repre['files']) - do_decompress = should_decompress(test_path) - - if do_decompress: - # change stagingDir, decompress first - # calculate all paths with modified directory, used on too many - # places - # will be purged by cleanup.py automatically - orig_staging_dir = new_repre["stagingDir"] - new_repre["stagingDir"] = get_decompress_dir() - # Prepare input and output filepaths self.input_output_paths(new_repre, output_def, temp_data) - if do_decompress: - input_file = temp_data["full_input_path"].\ - replace(new_repre["stagingDir"], orig_staging_dir) - - decompress(new_repre["stagingDir"], input_file, - temp_data["frame_start"], - temp_data["frame_end"], - self.log) - # Set output frames len to 1 when ouput is single image if ( temp_data["output_ext_is_image"] @@ -966,7 +930,7 @@ class ExtractReview(pyblish.api.InstancePlugin): return regexes def validate_value_by_regexes(self, value, in_list): - """Validates in any regex from list match entered value. + """Validates in any regexe from list match entered value. Args: in_list (list): List with regexes. @@ -991,9 +955,9 @@ class ExtractReview(pyblish.api.InstancePlugin): def profile_exclusion(self, matching_profiles): """Find out most matching profile byt host, task and family match. - Profiles are selectively filtered. Each profile should have + Profiles are selectivelly filtered. Each profile should have "__value__" key with list of booleans. Each boolean represents - existence of filter for specific key (host, tasks, family). + existence of filter for specific key (host, taks, family). Profiles are looped in sequence. In each sequence are split into true_list and false_list. For next sequence loop are used profiles in true_list if there are any profiles else false_list is used. @@ -1072,7 +1036,7 @@ class ExtractReview(pyblish.api.InstancePlugin): highest_profile_points = -1 # Each profile get 1 point for each matching filter. Profile with most - # points is returned. For cases when more than one profile will match + # points is returnd. For cases when more than one profile will match # are also stored ordered lists of matching values. for profile in self.profiles: profile_points = 0 @@ -1684,7 +1648,7 @@ class ExtractReview(pyblish.api.InstancePlugin): def add_video_filter_args(self, args, inserting_arg): """ - Fixing video filter arguments to be one long string + Fixing video filter argumets to be one long string Args: args (list): list of string arguments diff --git a/pype/plugins/maya/create/create_render.py b/pype/plugins/maya/create/create_render.py index b718079b43..bdd237a54e 100644 --- a/pype/plugins/maya/create/create_render.py +++ b/pype/plugins/maya/create/create_render.py @@ -193,7 +193,6 @@ class CreateRender(avalon.maya.Creator): self.data["tilesX"] = 2 self.data["tilesY"] = 2 self.data["convertToScanline"] = False - self.data["vrayUseReferencedAovs"] = False # Disable for now as this feature is not working yet # self.data["assScene"] = False diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index 0853473120..3dde3b1592 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -149,7 +149,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # return all expected files for all cameras and aovs in given # frame range - ef = ExpectedFiles(render_instance) + ef = ExpectedFiles() exp_files = ef.get(renderer, layer_name) self.log.info("multipart: {}".format(ef.multipart)) assert exp_files, "no file names were generated, this is bug" @@ -248,8 +248,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "tilesX": render_instance.data.get("tilesX") or 2, "tilesY": render_instance.data.get("tilesY") or 2, "priority": render_instance.data.get("priority"), - "convertToScanline": render_instance.data.get("convertToScanline") or False, # noqa: E501 - "vrayUseReferencedAovs": render_instance.data.get("vrayUseReferencedAovs") or False # noqa: E501 + "convertToScanline": render_instance.data.get("convertToScanline") or False # noqa: E501 } if self.sync_workfile_version: diff --git a/pype/plugins/maya/publish/validate_vray_referenced_aovs.py b/pype/plugins/maya/publish/validate_vray_referenced_aovs.py deleted file mode 100644 index 120677021d..0000000000 --- a/pype/plugins/maya/publish/validate_vray_referenced_aovs.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- coding: utf-8 -*- -"""Validate if there are AOVs pulled from references.""" -import pyblish.api -import types -from maya import cmds - -import pype.hosts.maya.action - - -class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin): - """Validate whether the V-Ray Render Elements (AOVs) include references. - - This will check if there are AOVs pulled from references. If - `Vray Use Referenced Aovs` is checked on render instance, u must add those - manually to Render Elements as Pype will expect them to be rendered. - - """ - - order = pyblish.api.ValidatorOrder - label = 'VRay Referenced AOVs' - hosts = ['maya'] - families = ['renderlayer'] - actions = [pype.api.RepairContextAction] - - def process(self, instance): - """Plugin main entry point.""" - if instance.data.get("renderer") != "vray": - # If not V-Ray ignore.. - return - - ref_aovs = cmds.ls( - type=["VRayRenderElement", "VRayRenderElementSet"], - referencedNodes=True) - ref_aovs_enabled = ValidateVrayReferencedAOVs.maya_is_true( - cmds.getAttr("vraySettings.relements_usereferenced")) - - if not instance.data.get("vrayUseReferencedAovs"): - if ref_aovs_enabled and ref_aovs: - self.log.warning(( - "Referenced AOVs are enabled in Vray " - "Render Settings and are detected in scene, but " - "Pype render instance option for referenced AOVs is " - "disabled. Those AOVs will be rendered but not published " - "by Pype." - )) - self.log.warning(", ".join(ref_aovs)) - else: - if not ref_aovs: - self.log.warning(( - "Use of referenced AOVs enabled but there are none " - "in the scene." - )) - if not ref_aovs_enabled: - self.log.error(( - "'Use referenced' not enabled in Vray Render Settings." - )) - raise AssertionError("Invalid render settings") - - @classmethod - def repair(cls, context): - """Repair action.""" - vray_settings = cmds.ls(type="VRaySettingsNode") - if not vray_settings: - node = cmds.createNode("VRaySettingsNode") - else: - node = vray_settings[0] - - cmds.setAttr("{}.relements_usereferenced".format(node), True) - - @staticmethod - def maya_is_true(attr_val): - """Whether a Maya attr evaluates to True. - - When querying an attribute value from an ambiguous object the - Maya API will return a list of values, which need to be properly - handled to evaluate properly. - - Args: - attr_val (mixed): Maya attribute to be evaluated as bool. - - Returns: - bool: cast Maya attribute to Pythons boolean value. - - """ - if isinstance(attr_val, types.BooleanType): - return attr_val - elif isinstance(attr_val, (types.ListType, types.GeneratorType)): - return any(attr_val) - else: - return bool(attr_val) From c644453b76e36abebbdabee7d259af799323004d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 12:52:27 +0100 Subject: [PATCH 048/106] added object type filtering to settings --- pype/settings/defaults/project_settings/ftrack.json | 6 +++++- .../projects_schema/schema_project_ftrack.json | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pype/settings/defaults/project_settings/ftrack.json b/pype/settings/defaults/project_settings/ftrack.json index 4d617b9f09..b03328115b 100644 --- a/pype/settings/defaults/project_settings/ftrack.json +++ b/pype/settings/defaults/project_settings/ftrack.json @@ -45,6 +45,10 @@ }, "status_task_to_parent": { "enabled": true, + "parent_object_types": [ + "Shot", + "Asset Build" + ], "parent_status_match_all_task_statuses": { "Completed": [ "Approved", @@ -190,4 +194,4 @@ "ftrack_custom_attributes": {} } } -} +} \ No newline at end of file diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json index 70f578822a..a0cb6c9255 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json @@ -157,6 +157,16 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "label", + "label": "List of parent object types where this is triggered (\"Shot\", \"Asset Build\", etc.). Skipped if list is empty." + }, + { + "type": "list", + "object_type": "text", + "key": "parent_object_types", + "label": "Object types" + }, { "key": "parent_status_match_all_task_statuses", "type": "dict-modifiable", From a8e83d350a45833f38948570dea10244a4eb2020 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 18 Dec 2020 12:56:54 +0100 Subject: [PATCH 049/106] Fix - AE - added explicit cast to int Deadline requires integers only in frames range. On some environments rounded floats are pushed in, this casts them to int explicitly. --- .../aftereffects/publish/submit_aftereffects_deadline.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pype/plugins/aftereffects/publish/submit_aftereffects_deadline.py b/pype/plugins/aftereffects/publish/submit_aftereffects_deadline.py index 5e5c00dec1..fcb97e1281 100644 --- a/pype/plugins/aftereffects/publish/submit_aftereffects_deadline.py +++ b/pype/plugins/aftereffects/publish/submit_aftereffects_deadline.py @@ -43,8 +43,10 @@ class AfterEffectsSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline dln_job_info.UserName = context.data.get( "deadlineUser", getpass.getuser()) if self._instance.data["frameEnd"] > self._instance.data["frameStart"]: - frame_range = "{}-{}".format(self._instance.data["frameStart"], - self._instance.data["frameEnd"]) + # Deadline requires integers in frame range + frame_range = "{}-{}".format( + int(round(self._instance.data["frameStart"])), + int(round(self._instance.data["frameEnd"]))) dln_job_info.Frames = frame_range dln_job_info.ChunkSize = self.chunk_size From 50d2d9db71f09b7c6afc446f302d2a430035b4cc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 15:32:30 +0100 Subject: [PATCH 050/106] get rid of register override and class attributes --- .../events/event_task_to_parent_status.py | 59 ------------------- 1 file changed, 59 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_parent_status.py b/pype/modules/ftrack/events/event_task_to_parent_status.py index f14c52e3a6..921aa42d4d 100644 --- a/pype/modules/ftrack/events/event_task_to_parent_status.py +++ b/pype/modules/ftrack/events/event_task_to_parent_status.py @@ -3,66 +3,7 @@ from pype.modules.ftrack import BaseEvent class TaskStatusToParent(BaseEvent): - # Parent types where we care about changing of status - parent_types = ["shot", "asset build"] - # All parent's tasks must have status name in `task_statuses` key to apply - # status name in `new_status` - parent_status_match_all_task_statuses = [ - { - "new_status": "approved", - "task_statuses": [ - "approved", "omitted" - ] - } - ] - - # Task's status was changed to something in `task_statuses` to apply - # `new_status` on it's parent - # - this is done only if `parent_status_match_all_task_statuses` filtering - # didn't found matching status - parent_status_by_task_status = [ - { - "new_status": "in progress", - "task_statuses": [ - "in progress" - ] - } - ] - - def register(self, *args, **kwargs): - result = super(TaskStatusToParent, self).register(*args, **kwargs) - # Clean up presetable attributes - _new_all_match = [] - if self.parent_status_match_all_task_statuses: - for item in self.parent_status_match_all_task_statuses: - _new_all_match.append({ - "new_status": item["new_status"].lower(), - "task_statuses": [ - status_name.lower() - for status_name in item["task_statuses"] - ] - }) - self.parent_status_match_all_task_statuses = _new_all_match - - _new_single_match = [] - if self.parent_status_by_task_status: - for item in self.parent_status_by_task_status: - _new_single_match.append({ - "new_status": item["new_status"].lower(), - "task_statuses": [ - status_name.lower() - for status_name in item["task_statuses"] - ] - }) - self.parent_status_by_task_status = _new_single_match - - self.parent_types = [ - parent_type.lower() - for parent_type in self.parent_types - ] - - return result def filter_entities_info(self, session, event): # Filter if event contain relevant data From 3031224736c9ed1ae1eab2b52f817c537ee6f235 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 15:33:26 +0100 Subject: [PATCH 051/106] task to parent status are converted to use settings --- .../events/event_task_to_parent_status.py | 392 +++++++++++------- 1 file changed, 251 insertions(+), 141 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_parent_status.py b/pype/modules/ftrack/events/event_task_to_parent_status.py index 921aa42d4d..4498456d03 100644 --- a/pype/modules/ftrack/events/event_task_to_parent_status.py +++ b/pype/modules/ftrack/events/event_task_to_parent_status.py @@ -3,49 +3,47 @@ from pype.modules.ftrack import BaseEvent class TaskStatusToParent(BaseEvent): + settings_key = "status_task_to_parent" - - def filter_entities_info(self, session, event): + def filter_entities_info(self, event): # Filter if event contain relevant data entities_info = event["data"].get("entities") if not entities_info: return - filtered_entities = [] + filtered_entity_info = collections.defaultdict(list) + status_ids = set() for entity_info in entities_info: # Care only about tasks if entity_info.get("entityType") != "task": continue # Care only about changes of status - changes = entity_info.get("changes") or {} - statusid_changes = changes.get("statusid") or {} + changes = entity_info.get("changes") + if not changes: + continue + statusid_changes = changes.get("statusid") + if not statusid_changes: + continue + + new_status_id = entity_info["changes"]["statusid"]["new"] if ( - statusid_changes.get("new") is None - or statusid_changes.get("old") is None + statusid_changes.get("old") is None + or new_status_id is None ): continue - filtered_entities.append(entity_info) + project_id = None + for parent_item in reversed(entity_info["parents"]): + if parent_item["entityType"] == "show": + project_id = parent_item["entityId"] + break - if not filtered_entities: - return + if project_id: + filtered_entity_info[project_id].append(entity_info) + status_ids.add(new_status_id) - status_ids = [ - entity_info["changes"]["statusid"]["new"] - for entity_info in filtered_entities - ] - statuses_by_id = self.get_statuses_by_id( - session, status_ids=status_ids - ) - - # Care only about tasks having status with state `Done` - output = [] - for entity_info in filtered_entities: - status_id = entity_info["changes"]["statusid"]["new"] - entity_info["status_entity"] = statuses_by_id[status_id] - output.append(entity_info) - return output + return filtered_entity_info def get_parents_by_id(self, session, entities_info, object_types): task_type_id = None @@ -91,146 +89,216 @@ class TaskStatusToParent(BaseEvent): for entity in task_entities } - def get_statuses_by_id(self, session, task_entities=None, status_ids=None): - if task_entities is None and status_ids is None: - return {} + def launch(self, session, event): + '''Propagates status from version to task when changed''' - if status_ids is None: - status_ids = [] - for task_entity in task_entities: - status_ids.append(task_entity["status_id"]) + filtered_entities_info = self.filter_entities_info(event) + if not filtered_entities_info: + return - if not status_ids: - return {} + for project_id, entities_info in filtered_entities_info.items(): + self.process_by_project(session, event, project_id, entities_info) - status_entities = session.query( - "Status where id in ({})".format(", ".join(status_ids)) + def process_by_project(self, session, event, project_id, entities_info): + # Get project entity + project_entity = self.get_project_entity_from_event( + session, event, project_id + ) + # Load settings + project_settings = self.get_settings_for_project( + session, event, project_entity=project_entity + ) + + # Prepare loaded settings and check if can be processed + project_name = project_entity["full_name"] + event_settings = ( + project_settings["ftrack"]["events"][self.settings_key] + ) + result = self.prepare_settings(event_settings, project_name) + if not result: + return + + # Unpack the result + parent_object_types, all_match, single_match = result + + # Prepare valid object type ids for object types from settings + object_types = session.query("select id, name from ObjectType").all() + object_type_id_by_low_name = { + object_type["name"].lower(): object_type["id"] + for object_type in object_types + } + + valid_object_type_ids = set() + for object_type_name in parent_object_types: + if object_type_name in object_type_id_by_low_name: + valid_object_type_ids.add( + object_type_id_by_low_name[object_type_name] + ) + else: + self.log.warning( + "Unknown object type \"{}\" set on project \"{}\".".format( + object_type_name, project_name + ) + ) + + if not valid_object_type_ids: + return + + # Prepare parent ids + parent_ids = set() + for entity_info in entities_info: + parent_id = entity_info["parentId"] + if parent_id: + parent_ids.add(parent_id) + + # Query parent ids by object type ids and parent ids + parent_entities = session.query( + ( + "select id, status_id, object_type_id, link from TypedContext" + " where id in ({}) and object_type_id in ({})" + ).format( + self.join_query_keys(parent_ids), + self.join_query_keys(valid_object_type_ids) + ) + ).all() + # Skip if none of parents match the filtering + if not parent_entities: + return + + obj_ids = set() + for entity in parent_entities: + obj_ids.add(entity["object_type_id"]) + + object_type_name_by_id = { + object_type["id"]: object_type["name"] + for object_type in object_types + } + + project_schema = project_entity["project_schema"] + available_statuses_by_obj_id = {} + for obj_id in obj_ids: + obj_name = object_type_name_by_id[obj_id] + statuses = project_schema.get_statuses(obj_name) + statuses_by_low_name = { + status["name"].lower(): status + for status in statuses + } + valid = False + for name in all_match.keys(): + if name in statuses_by_low_name: + valid = True + break + + if not valid: + for name in single_match.keys(): + if name in statuses_by_low_name: + valid = True + break + if valid: + available_statuses_by_obj_id[obj_id] = statuses_by_low_name + + valid_parent_ids = set() + status_ids = set() + valid_parent_entities = [] + for entity in parent_entities: + if entity["object_type_id"] not in available_statuses_by_obj_id: + continue + + valid_parent_entities.append(entity) + valid_parent_ids.add(entity["id"]) + status_ids.add(entity["status_id"]) + + task_entities = session.query( + ( + "select id, parent_id, status_id from TypedContext" + " where parent_id in ({}) and object_type_id is \"{}\"" + ).format( + self.join_query_keys(valid_parent_ids), + object_type_id_by_low_name["task"] + ) ).all() - return { + # This should not happen but it is safer + if not task_entities: + return + + task_entities_by_parent_id = collections.defaultdict(list) + for task_entity in task_entities: + status_ids.add(task_entity["status_id"]) + parent_id = task_entity["parent_id"] + task_entities_by_parent_id[parent_id].append(task_entity) + + status_entities = session.query(( + "select id, name from Status where id in ({})" + ).format(self.join_query_keys(status_ids))).all() + + statuses_by_id = { entity["id"]: entity for entity in status_entities } - def launch(self, session, event): - '''Propagates status from version to task when changed''' - - entities_info = self.filter_entities_info(session, event) - if not entities_info: - return - - object_types = session.query("select id, name from ObjectType").all() - parents_by_id = self.get_parents_by_id( - session, entities_info, object_types - ) - if not parents_by_id: - return - tasks_by_id = self.get_tasks_by_id( - session, tuple(parents_by_id.keys()) - ) - - # Just collect them in one variable - entities_by_id = {} - for entity_id, entity in parents_by_id.items(): - entities_by_id[entity_id] = entity - for entity_id, entity in tasks_by_id.items(): - entities_by_id[entity_id] = entity - - # Map task entities by their parents - tasks_by_parent_id = collections.defaultdict(list) - for task_entity in tasks_by_id.values(): - tasks_by_parent_id[task_entity["parent_id"]].append(task_entity) - - # Found status entities for all queried entities - statuses_by_id = self.get_statuses_by_id( - session, - entities_by_id.values() - ) - # New status determination logic new_statuses_by_parent_id = self.new_status_by_all_task_statuses( - parents_by_id.keys(), tasks_by_parent_id, statuses_by_id + task_entities_by_parent_id, statuses_by_id, all_match ) + task_entities_by_id = { + task_entity["id"]: task_entity + for task_entity in task_entities + } # Check if there are remaining any parents that does not have # determined new status yet remainder_tasks_by_parent_id = collections.defaultdict(list) for entity_info in entities_info: + entity_id = entity_info["entityId"] + if entity_id not in task_entities_by_id: + continue parent_id = entity_info["parentId"] if ( # Skip if already has determined new status parent_id in new_statuses_by_parent_id # Skip if parent is not in parent mapping # - if was not found or parent type is not interesting - or parent_id not in parents_by_id + or parent_id not in task_entities_by_parent_id ): continue remainder_tasks_by_parent_id[parent_id].append( - entities_by_id[entity_info["entityId"]] + task_entities_by_id[entity_id] ) # Try to find new status for remained parents new_statuses_by_parent_id.update( self.new_status_by_remainders( remainder_tasks_by_parent_id, - statuses_by_id + statuses_by_id, + single_match ) ) - # Make sure new_status is set to valid value - for parent_id in tuple(new_statuses_by_parent_id.keys()): - new_status_name = new_statuses_by_parent_id[parent_id] - if not new_status_name: - new_statuses_by_parent_id.pop(parent_id) - # If there are not new statuses then just skip if not new_statuses_by_parent_id: return - # Get project schema from any available entity - _entity = None - for _ent in entities_by_id.values(): - _entity = _ent - break - - project_entity = self.get_project_from_entity(_entity) - project_schema = project_entity["project_schema"] - - # Map type names by lowere type names - types_mapping = { - _type.lower(): _type - for _type in session.types + parent_entities_by_id = { + parent_entity["id"]: parent_entity + for parent_entity in valid_parent_entities } - # Map object type id by lowered and modified object type name - object_type_mapping = {} - for object_type in object_types: - mapping_name = object_type["name"].lower().replace(" ", "") - object_type_mapping[object_type["id"]] = mapping_name - - statuses_by_obj_id = {} for parent_id, new_status_name in new_statuses_by_parent_id.items(): if not new_status_name: continue - parent_entity = entities_by_id[parent_id] - obj_id = parent_entity["object_type_id"] - # Find statuses for entity type by object type name - # in project's schema and cache them - if obj_id not in statuses_by_obj_id: - mapping_name = object_type_mapping[obj_id] - mapped_name = types_mapping.get(mapping_name) - statuses = project_schema.get_statuses(mapped_name) - statuses_by_obj_id[obj_id] = { - status["name"].lower(): status - for status in statuses - } - - statuses_by_name = statuses_by_obj_id[obj_id] - new_status = statuses_by_name.get(new_status_name) + parent_entity = parent_entities_by_id[parent_id] ent_path = "/".join( [ent["name"] for ent in parent_entity["link"]] ) + + obj_id = parent_entity["object_type_id"] + statuses_by_low_name = available_statuses_by_obj_id.get(obj_id) + if not statuses_by_low_name: + continue + + new_status = statuses_by_low_name.get(new_status_name) if not new_status: self.log.warning(( "\"{}\" Couldn't change status to \"{}\"." @@ -240,18 +308,18 @@ class TaskStatusToParent(BaseEvent): )) continue - current_status_name = parent_entity["status"]["name"] + current_status = parent_entity["status"] # Do nothing if status is already set - if new_status["name"] == current_status_name: + if new_status["id"] == current_status["id"]: self.log.debug( "\"{}\" Status \"{}\" already set.".format( - ent_path, current_status_name + ent_path, current_status["name"] ) ) continue try: - parent_entity["status"] = new_status + parent_entity["status_id"] = new_status["id"] session.commit() self.log.info( "\"{}\" changed status to \"{}\"".format( @@ -267,8 +335,56 @@ class TaskStatusToParent(BaseEvent): exc_info=True ) + def prepare_settings(self, event_settings, project_name): + if not event_settings["enabled"]: + self.log.debug("Project \"{}\" has disabled {}.".format( + project_name, self.__class__.__name__ + )) + return + + _parent_object_types = event_settings["parent_object_types"] + if not _parent_object_types: + self.log.debug(( + "Project \"{}\" does not have set" + " parent object types filtering." + ).format(project_name)) + return + + _all_match = ( + event_settings["parent_status_match_all_task_statuses"] + ) + _single_match = ( + event_settings["parent_status_by_task_status"] + ) + + if not _all_match and not _single_match: + self.log.debug(( + "Project \"{}\" does not have set" + " parent status mappings." + ).format(project_name)) + return + + parent_object_types = [ + item.lower() + for item in _parent_object_types + ] + all_match = {} + for new_status_name, task_status_names in _all_match.items(): + all_match[new_status_name.lower] = [ + status_name.lower() + for status_name in task_status_names + ] + + single_match = {} + for new_status_name, task_status_names in _single_match.items(): + single_match[new_status_name.lower] = [ + status_name.lower() + for status_name in task_status_names + ] + return parent_object_types, all_match, single_match + def new_status_by_all_task_statuses( - self, parent_ids, tasks_by_parent_id, statuses_by_id + self, tasks_by_parent_id, statuses_by_id, all_match ): """All statuses of parent entity must match specific status names. @@ -276,23 +392,23 @@ class TaskStatusToParent(BaseEvent): determined. """ output = {} - for parent_id in parent_ids: + for parent_id, task_entities in tasks_by_parent_id.items(): task_statuses_lowered = set() - for task_entity in tasks_by_parent_id[parent_id]: + for task_entity in task_entities: task_status = statuses_by_id[task_entity["status_id"]] low_status_name = task_status["name"].lower() task_statuses_lowered.add(low_status_name) new_status = None - for item in self.parent_status_match_all_task_statuses: + for _new_status, task_statuses in all_match: valid_item = True for status_name_low in task_statuses_lowered: - if status_name_low not in item["task_statuses"]: + if status_name_low not in task_statuses: valid_item = False break if valid_item: - new_status = item["new_status"] + new_status = _new_status break if new_status is not None: @@ -301,7 +417,7 @@ class TaskStatusToParent(BaseEvent): return output def new_status_by_remainders( - self, remainder_tasks_by_parent_id, statuses_by_id + self, remainder_tasks_by_parent_id, statuses_by_id, single_match ): """By new task status can be determined new status of parent.""" output = {} @@ -312,27 +428,21 @@ class TaskStatusToParent(BaseEvent): if not task_entities: continue + # TODO re-implement status orders # For cases there are multiple tasks in changes # - task status which match any new status item by order in the # list `parent_status_by_task_status` is preffered - best_order = len(self.parent_status_by_task_status) - best_order_status = None + new_status = None for task_entity in task_entities: task_status = statuses_by_id[task_entity["status_id"]] low_status_name = task_status["name"].lower() - for order, item in enumerate( - self.parent_status_by_task_status - ): - if order >= best_order: + for _new_status, task_statuses in single_match.items(): + if low_status_name in task_statuses: + new_status = _new_status break - if low_status_name in item["task_statuses"]: - best_order = order - best_order_status = item["new_status"] - break - - if best_order_status: - output[parent_id] = best_order_status + if new_status: + output[parent_id] = new_status return output From 0e4eb39c6cb810b3580fe9d389496653f644f76c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 15:34:34 +0100 Subject: [PATCH 052/106] added initialization part to Terminal class --- pype/lib/terminal.py | 148 ++++++++++++++++++++++++++++--------------- 1 file changed, 97 insertions(+), 51 deletions(-) diff --git a/pype/lib/terminal.py b/pype/lib/terminal.py index afaca8241a..cdf102a499 100644 --- a/pype/lib/terminal.py +++ b/pype/lib/terminal.py @@ -11,12 +11,6 @@ # ..---===[[ PyP3 Setup ]]===---... # import re -import os -import sys -import blessed - - -term = blessed.Terminal() class Terminal: @@ -28,48 +22,97 @@ class Terminal: Using **PYPE_LOG_NO_COLORS** environment variable. """ - # shortcuts for colorama codes + # Is Terminal initialized + _initialized = False + # Use colorized output + use_colors = True + # Output message replacements mapping - set on initialization + _sdict = {} - _SB = term.bold - _RST = "" - _LR = term.tomato2 - _LG = term.aquamarine3 - _LB = term.turquoise2 - _LM = term.slateblue2 - _LY = term.gold - _R = term.red - _G = term.green - _B = term.blue - _C = term.cyan - _Y = term.yellow - _W = term.white + @staticmethod + def _initialize(): + """Initialize Terminal class as object. - # dictionary replacing string sequences with colorized one - _sdict = { + First check if colorized output is disabled by environment variable + `PYPE_LOG_NO_COLORS` value. By default is colorized output turned on. - r">>> ": _SB + _LG + r">>> " + _RST, - r"!!!(?!\sCRI|\sERR)": _SB + _R + r"!!! " + _RST, - r"\-\-\- ": _SB + _C + r"--- " + _RST, - r"\*\*\*(?!\sWRN)": _SB + _LY + r"***" + _RST, - r"\*\*\* WRN": _SB + _LY + r"*** WRN" + _RST, - r" \- ": _SB + _LY + r" - " + _RST, - r"\[ ": _SB + _LG + r"[ " + _RST, - r"\]": _SB + _LG + r"]" + _RST, - r"{": _LG + r"{", - r"}": r"}" + _RST, - r"\(": _LY + r"(", - r"\)": r")" + _RST, - r"^\.\.\. ": _SB + _LR + r"... " + _RST, - r"!!! ERR: ": - _SB + _LR + r"!!! ERR: " + _RST, - r"!!! CRI: ": - _SB + _R + r"!!! CRI: " + _RST, - r"(?i)failed": _SB + _LR + "FAILED" + _RST, - r"(?i)error": _SB + _LR + "ERROR" + _RST - } + Then tries to import python module that do the colors magic and create + it's terminal object. Colorized output is not used if import of python + module or terminal object creation fails. + """ + from . import env_value_to_bool + use_colors = env_value_to_bool( + "PYPE_LOG_NO_COLORS", default=Terminal.use_colors + ) + if not use_colors: + Terminal.use_colors = use_colors + return - def __init__(self): - pass + try: + # Try to import `blessed` module and create `Terminal` object + import blessed + term = blessed.Terminal() + + except Exception: + # Do not use colors if crashed + Terminal.use_colors = False + Terminal.echo( + "Module `blessed` failed on import or terminal creation." + " Pype terminal won't use colors." + ) + return + + # shortcuts for blessed codes + _SB = term.bold + _RST = "" + _LR = term.tomato2 + _LG = term.aquamarine3 + _LB = term.turquoise2 + _LM = term.slateblue2 + _LY = term.gold + _R = term.red + _G = term.green + _B = term.blue + _C = term.cyan + _Y = term.yellow + _W = term.white + + # dictionary replacing string sequences with colorized one + Terminal._sdict = { + r">>> ": _SB + _LG + r">>> " + _RST, + r"!!!(?!\sCRI|\sERR)": _SB + _R + r"!!! " + _RST, + r"\-\-\- ": _SB + _C + r"--- " + _RST, + r"\*\*\*(?!\sWRN)": _SB + _LY + r"***" + _RST, + r"\*\*\* WRN": _SB + _LY + r"*** WRN" + _RST, + r" \- ": _SB + _LY + r" - " + _RST, + r"\[ ": _SB + _LG + r"[ " + _RST, + r"\]": _SB + _LG + r"]" + _RST, + r"{": _LG + r"{", + r"}": r"}" + _RST, + r"\(": _LY + r"(", + r"\)": r")" + _RST, + r"^\.\.\. ": _SB + _LR + r"... " + _RST, + r"!!! ERR: ": + _SB + _LR + r"!!! ERR: " + _RST, + r"!!! CRI: ": + _SB + _R + r"!!! CRI: " + _RST, + r"(?i)failed": _SB + _LR + "FAILED" + _RST, + r"(?i)error": _SB + _LR + "ERROR" + _RST + } + + Terminal._SB = _SB + Terminal._RST = _RST + Terminal._LR = _LR + Terminal._LG = _LG + Terminal._LB = _LB + Terminal._LM = _LM + Terminal._LY = _LY + Terminal._R = _R + Terminal._G = _G + Terminal._B = _B + Terminal._C = _C + Terminal._Y = _Y + Terminal._W = _W @staticmethod def _multiple_replace(text, adict): @@ -123,12 +166,15 @@ class Terminal: """ T = Terminal - # if we dont want colors, just print raw message - if os.environ.get('PYPE_LOG_NO_COLORS'): - return message - else: - message = re.sub(r'\[(.*)\]', '[ ' + T._SB + T._W + - r'\1' + T._RST + ' ]', message) - message = T._multiple_replace(message + T._RST, T._sdict) + if not T._initialized: + T._initialize() + # if we dont want colors, just print raw message + if not T.use_colors: return message + + message = re.sub(r'\[(.*)\]', '[ ' + T._SB + T._W + + r'\1' + T._RST + ' ]', message) + message = T._multiple_replace(message + T._RST, T._sdict) + + return message From d012f54a1fd21040b52bcb19a05ceeb72f3eebc9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 15:43:00 +0100 Subject: [PATCH 053/106] set `_initialized` to True on `_initialize` call --- pype/lib/terminal.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/lib/terminal.py b/pype/lib/terminal.py index cdf102a499..043869130a 100644 --- a/pype/lib/terminal.py +++ b/pype/lib/terminal.py @@ -40,6 +40,9 @@ class Terminal: it's terminal object. Colorized output is not used if import of python module or terminal object creation fails. """ + # Mark that Terminal's initialization was already triggered + Terminal._initialized = True + from . import env_value_to_bool use_colors = env_value_to_bool( "PYPE_LOG_NO_COLORS", default=Terminal.use_colors From 637fba0357ba018bb5e7edf9b2294f46cab6a796 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 18 Dec 2020 14:54:16 +0000 Subject: [PATCH 054/106] Optionally hide image planes from reviews. --- pype/plugins/maya/create/create_review.py | 2 ++ pype/plugins/maya/publish/extract_playblast.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/pype/plugins/maya/create/create_review.py b/pype/plugins/maya/create/create_review.py index 97731d7950..bfeab33f5b 100644 --- a/pype/plugins/maya/create/create_review.py +++ b/pype/plugins/maya/create/create_review.py @@ -13,6 +13,7 @@ class CreateReview(avalon.maya.Creator): defaults = ['Main'] keepImages = False isolate = False + imagePlane = True def __init__(self, *args, **kwargs): super(CreateReview, self).__init__(*args, **kwargs) @@ -25,5 +26,6 @@ class CreateReview(avalon.maya.Creator): data["isolate"] = self.isolate data["keepImages"] = self.keepImages + data["imagePlane"] = self.imagePlane self.data = data diff --git a/pype/plugins/maya/publish/extract_playblast.py b/pype/plugins/maya/publish/extract_playblast.py index 647d1f4503..25e47cd48b 100644 --- a/pype/plugins/maya/publish/extract_playblast.py +++ b/pype/plugins/maya/publish/extract_playblast.py @@ -81,6 +81,13 @@ class ExtractPlayblast(pype.api.Extractor): if instance.data.get("isolate"): preset["isolate"] = instance.data["setMembers"] + # Show/Hide image planes on request. + image_plane = instance.data.get("imagePlane", True) + if "viewport_options" in preset: + preset["viewport_options"]["imagePlane"] = image_plane + else: + preset["viewport_options"] = {"imagePlane": image_plane} + with maintained_time(): filename = preset.get("filename", "%TEMP%") From cfcb9f49bd2ed738fb8cd46557879ab948fbc0f2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 16:03:47 +0100 Subject: [PATCH 055/106] removed unused methods --- .../events/event_task_to_parent_status.py | 64 +++---------------- 1 file changed, 10 insertions(+), 54 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_parent_status.py b/pype/modules/ftrack/events/event_task_to_parent_status.py index 4498456d03..a0b7c12408 100644 --- a/pype/modules/ftrack/events/event_task_to_parent_status.py +++ b/pype/modules/ftrack/events/event_task_to_parent_status.py @@ -5,6 +5,16 @@ from pype.modules.ftrack import BaseEvent class TaskStatusToParent(BaseEvent): settings_key = "status_task_to_parent" + def launch(self, session, event): + """Propagates status from task to parent when changed.""" + + filtered_entities_info = self.filter_entities_info(event) + if not filtered_entities_info: + return + + for project_id, entities_info in filtered_entities_info.items(): + self.process_by_project(session, event, project_id, entities_info) + def filter_entities_info(self, event): # Filter if event contain relevant data entities_info = event["data"].get("entities") @@ -45,60 +55,6 @@ class TaskStatusToParent(BaseEvent): return filtered_entity_info - def get_parents_by_id(self, session, entities_info, object_types): - task_type_id = None - valid_object_type_ids = [] - for object_type in object_types: - object_name_low = object_type["name"].lower() - if object_name_low == "task": - task_type_id = object_type["id"] - - if object_name_low in self.parent_types: - valid_object_type_ids.append(object_type["id"]) - - parent_ids = [ - "\"{}\"".format(entity_info["parentId"]) - for entity_info in entities_info - if entity_info["objectTypeId"] == task_type_id - ] - if not parent_ids: - return {} - - parent_entities = session.query(( - "TypedContext where id in ({}) and object_type_id in ({})" - ).format( - ", ".join(parent_ids), ", ".join(valid_object_type_ids)) - ).all() - - return { - entity["id"]: entity - for entity in parent_entities - } - - def get_tasks_by_id(self, session, parent_ids): - joined_parent_ids = ",".join([ - "\"{}\"".format(parent_id) - for parent_id in parent_ids - ]) - task_entities = session.query( - "Task where parent_id in ({})".format(joined_parent_ids) - ).all() - - return { - entity["id"]: entity - for entity in task_entities - } - - def launch(self, session, event): - '''Propagates status from version to task when changed''' - - filtered_entities_info = self.filter_entities_info(event) - if not filtered_entities_info: - return - - for project_id, entities_info in filtered_entities_info.items(): - self.process_by_project(session, event, project_id, entities_info) - def process_by_project(self, session, event, project_id, entities_info): # Get project entity project_entity = self.get_project_entity_from_event( From ab97f5df3c5be0292ce73ea2b197036cd6548d15 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 16:03:57 +0100 Subject: [PATCH 056/106] minor fixes --- .../modules/ftrack/events/event_task_to_parent_status.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_parent_status.py b/pype/modules/ftrack/events/event_task_to_parent_status.py index a0b7c12408..159c8ffd1c 100644 --- a/pype/modules/ftrack/events/event_task_to_parent_status.py +++ b/pype/modules/ftrack/events/event_task_to_parent_status.py @@ -164,6 +164,9 @@ class TaskStatusToParent(BaseEvent): valid_parent_ids.add(entity["id"]) status_ids.add(entity["status_id"]) + if not valid_parent_ids: + return + task_entities = session.query( ( "select id, parent_id, status_id from TypedContext" @@ -326,14 +329,14 @@ class TaskStatusToParent(BaseEvent): ] all_match = {} for new_status_name, task_status_names in _all_match.items(): - all_match[new_status_name.lower] = [ + all_match[new_status_name.lower()] = [ status_name.lower() for status_name in task_status_names ] single_match = {} for new_status_name, task_status_names in _single_match.items(): - single_match[new_status_name.lower] = [ + single_match[new_status_name.lower()] = [ status_name.lower() for status_name in task_status_names ] @@ -356,7 +359,7 @@ class TaskStatusToParent(BaseEvent): task_statuses_lowered.add(low_status_name) new_status = None - for _new_status, task_statuses in all_match: + for _new_status, task_statuses in all_match.items(): valid_item = True for status_name_low in task_statuses_lowered: if status_name_low not in task_statuses: From 2e6b3729067b3a3d37949f1ac9a878b650291ca1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 16:45:07 +0100 Subject: [PATCH 057/106] `parent_status_by_task_status` is list of dictionaries --- .../defaults/project_settings/ftrack.json | 21 +++++++++++-------- .../schema_project_ftrack.json | 21 ++++++++++++++++--- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/pype/settings/defaults/project_settings/ftrack.json b/pype/settings/defaults/project_settings/ftrack.json index b03328115b..7bc935e3c5 100644 --- a/pype/settings/defaults/project_settings/ftrack.json +++ b/pype/settings/defaults/project_settings/ftrack.json @@ -55,14 +55,17 @@ "Omitted" ] }, - "parent_status_by_task_status": { - "In Progress": [ - "in progress", - "change requested", - "retake", - "pending review" - ] - } + "parent_status_by_task_status": [ + { + "new_status": "In Progress", + "task_statuses": [ + "in progress", + "change requested", + "retake", + "pending review" + ] + } + ] }, "status_task_to_version": { "enabled": true, @@ -194,4 +197,4 @@ "ftrack_custom_attributes": {} } } -} \ No newline at end of file +} diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json index a0cb6c9255..c94ed40511 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json @@ -177,12 +177,27 @@ } }, { + "type": "list", "key": "parent_status_by_task_status", - "type": "dict-modifiable", "label": "Change parent status if a single task matches", "object_type": { - "type": "list", - "object_type": "text" + "type": "dict", + "children": [ + { + "type": "text", + "label": "New parent status", + "key": "new_status" + }, + { + "type": "separator" + }, + { + "type": "list", + "label": "Task status", + "key": "task_statuses", + "object_type": "text" + } + ] } } ] From 6b7933895f338b5ba5b4bb78d54c29d53303e5b1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 16:45:37 +0100 Subject: [PATCH 058/106] reimplemented best order status --- .../events/event_task_to_parent_status.py | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_parent_status.py b/pype/modules/ftrack/events/event_task_to_parent_status.py index 159c8ffd1c..b84994aeb6 100644 --- a/pype/modules/ftrack/events/event_task_to_parent_status.py +++ b/pype/modules/ftrack/events/event_task_to_parent_status.py @@ -334,12 +334,15 @@ class TaskStatusToParent(BaseEvent): for status_name in task_status_names ] - single_match = {} - for new_status_name, task_status_names in _single_match.items(): - single_match[new_status_name.lower()] = [ - status_name.lower() - for status_name in task_status_names - ] + single_match = [] + for item in _single_match.items(): + single_match.append({ + "new_status": item["new_status"].lower(), + "task_statuses": [ + status_name.lower() + for status_name in item["task_status_names"] + ] + }) return parent_object_types, all_match, single_match def new_status_by_all_task_statuses( @@ -387,21 +390,25 @@ class TaskStatusToParent(BaseEvent): if not task_entities: continue - # TODO re-implement status orders # For cases there are multiple tasks in changes # - task status which match any new status item by order in the # list `parent_status_by_task_status` is preffered - new_status = None + best_order = len(self.parent_status_by_task_status) + best_order_status = None for task_entity in task_entities: task_status = statuses_by_id[task_entity["status_id"]] low_status_name = task_status["name"].lower() - for _new_status, task_statuses in single_match.items(): - if low_status_name in task_statuses: - new_status = _new_status + for order, item in enumerate(single_match): + if order >= best_order: break - if new_status: - output[parent_id] = new_status + if low_status_name in item["task_statuses"]: + best_order = order + best_order_status = item["new_status"] + break + + if best_order_status: + output[parent_id] = best_order_status return output From 21b2f3e01898f791c63f33b8acc40a243718b973 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 16:52:13 +0100 Subject: [PATCH 059/106] single_match as list fix --- .../ftrack/events/event_task_to_parent_status.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_parent_status.py b/pype/modules/ftrack/events/event_task_to_parent_status.py index b84994aeb6..afe00e2d0c 100644 --- a/pype/modules/ftrack/events/event_task_to_parent_status.py +++ b/pype/modules/ftrack/events/event_task_to_parent_status.py @@ -67,10 +67,7 @@ class TaskStatusToParent(BaseEvent): # Prepare loaded settings and check if can be processed project_name = project_entity["full_name"] - event_settings = ( - project_settings["ftrack"]["events"][self.settings_key] - ) - result = self.prepare_settings(event_settings, project_name) + result = self.prepare_settings(project_settings, project_name) if not result: return @@ -146,8 +143,8 @@ class TaskStatusToParent(BaseEvent): break if not valid: - for name in single_match.keys(): - if name in statuses_by_low_name: + for item in single_match: + if item["new_status"] in statuses_by_low_name: valid = True break if valid: @@ -294,7 +291,11 @@ class TaskStatusToParent(BaseEvent): exc_info=True ) - def prepare_settings(self, event_settings, project_name): + def prepare_settings(self, project_settings, project_name): + event_settings = ( + project_settings["ftrack"]["events"][self.settings_key] + ) + if not event_settings["enabled"]: self.log.debug("Project \"{}\" has disabled {}.".format( project_name, self.__class__.__name__ From 8706f011971aacc015b58bf48f49d686bd07666c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 17:09:43 +0100 Subject: [PATCH 060/106] main function of settings gui moved to `__init__.py` --- pype/tools/settings/__init__.py | 26 +++++++++++++++++++++++++- pype/tools/settings/__main__.py | 26 +++++--------------------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/pype/tools/settings/__init__.py b/pype/tools/settings/__init__.py index 88f33ac188..89abd262e8 100644 --- a/pype/tools/settings/__init__.py +++ b/pype/tools/settings/__init__.py @@ -1,7 +1,31 @@ +import sys +from Qt import QtWidgets, QtGui + from .settings import style, MainWidget +def main(user_role=None): + if user_role is None: + user_role = "artist" + else: + user_role_low = user_role.lower() + allowed_roles = ("developer", "manager", "artist") + if user_role_low not in allowed_roles: + raise ValueError("Invalid user role \"{}\". Expected {}".format( + user_role, ", ".join(allowed_roles) + )) + + app = QtWidgets.QApplication(sys.argv) + app.setWindowIcon(QtGui.QIcon(style.app_icon_path())) + + widget = MainWidget(user_role) + widget.show() + + sys.exit(app.exec_()) + + __all__ = ( "style", - "MainWidget" + "MainWidget", + "main" ) diff --git a/pype/tools/settings/__main__.py b/pype/tools/settings/__main__.py index 7e9f80a52c..cf49035c23 100644 --- a/pype/tools/settings/__main__.py +++ b/pype/tools/settings/__main__.py @@ -1,23 +1,7 @@ -import sys - -import settings -from Qt import QtWidgets, QtGui +try: + from . import main +except ImportError: + from settings import main -if __name__ == "__main__": - app = QtWidgets.QApplication(sys.argv) - app.setWindowIcon(QtGui.QIcon(settings.style.app_icon_path())) - - _develop = "-d" in sys.argv or "--develop" in sys.argv - _user = "-m" in sys.argv or "--manager" in sys.argv - if _develop: - user_role = "developer" - elif _user: - user_role = "manager" - else: - user_role = "artist" - - widget = settings.MainWidget(user_role) - widget.show() - - sys.exit(app.exec_()) +main() From 1f59c6be824f15c879a3bbb122c99bedf6b3ae79 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 17:10:33 +0100 Subject: [PATCH 061/106] trigger settings.main in pype commands instead of creating new subprocess --- pype/pype_commands.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pype/pype_commands.py b/pype/pype_commands.py index f504728ca1..afb2848fa7 100644 --- a/pype/pype_commands.py +++ b/pype/pype_commands.py @@ -51,13 +51,11 @@ class PypeCommands: @staticmethod def launch_settings_gui(dev): - from pype.lib import execute + from pype.tools import settings - args = [sys.executable, "-m", "pype.tools.settings"] - if dev: - args.append("--develop") - return_code = execute(args) - return return_code + # TODO change argument options to allow enum of user roles + user_role = "developer" + settings.main(user_role) def launch_eventservercli(self, args): from pype.modules import ftrack From 8ffbe212f31e426e750626e8fe027366f23af6f3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 18:38:40 +0100 Subject: [PATCH 062/106] single task status match fixes --- .../ftrack/events/event_task_to_parent_status.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_parent_status.py b/pype/modules/ftrack/events/event_task_to_parent_status.py index afe00e2d0c..42d3ca0877 100644 --- a/pype/modules/ftrack/events/event_task_to_parent_status.py +++ b/pype/modules/ftrack/events/event_task_to_parent_status.py @@ -329,19 +329,19 @@ class TaskStatusToParent(BaseEvent): for item in _parent_object_types ] all_match = {} - for new_status_name, task_status_names in _all_match.items(): + for new_status_name, task_statuses in _all_match.items(): all_match[new_status_name.lower()] = [ status_name.lower() - for status_name in task_status_names + for status_name in task_statuses ] single_match = [] - for item in _single_match.items(): + for item in _single_match: single_match.append({ "new_status": item["new_status"].lower(), "task_statuses": [ status_name.lower() - for status_name in item["task_status_names"] + for status_name in item["task_statuses"] ] }) return parent_object_types, all_match, single_match @@ -393,8 +393,8 @@ class TaskStatusToParent(BaseEvent): # For cases there are multiple tasks in changes # - task status which match any new status item by order in the - # list `parent_status_by_task_status` is preffered - best_order = len(self.parent_status_by_task_status) + # list `single_match` is preffered + best_order = len(single_match) best_order_status = None for task_entity in task_entities: task_status = statuses_by_id[task_entity["status_id"]] From d615c07729466f3633d9df6b8bf7bf510ecbee9b Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 18 Dec 2020 18:57:17 +0100 Subject: [PATCH 063/106] change defaults for ftrack attributes --- .../defaults/project_settings/ftrack.json | 18 ++++-------------- .../defaults/project_settings/maya.json | 2 +- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/pype/settings/defaults/project_settings/ftrack.json b/pype/settings/defaults/project_settings/ftrack.json index d12f6c92e5..74f283570a 100644 --- a/pype/settings/defaults/project_settings/ftrack.json +++ b/pype/settings/defaults/project_settings/ftrack.json @@ -62,23 +62,13 @@ }, "status_task_to_version": { "enabled": true, - "mapping": { - "Approved": [ - "Complete" - ] - }, + "mapping": {}, "asset_types_filter": [] }, "status_version_to_task": { "enabled": true, - "mapping": { - "Approved": [ - "Complete" - ] - }, - "asset_types_to_skip": [ - "scene" - ] + "mapping": {}, + "asset_types_to_skip": [] }, "first_version_status": { "enabled": true, @@ -191,4 +181,4 @@ "ftrack_custom_attributes": {} } } -} +} \ No newline at end of file diff --git a/pype/settings/defaults/project_settings/maya.json b/pype/settings/defaults/project_settings/maya.json index b8c0dffa26..2307fd8b82 100644 --- a/pype/settings/defaults/project_settings/maya.json +++ b/pype/settings/defaults/project_settings/maya.json @@ -113,7 +113,7 @@ "sync_workfile_version": false }, "ValidateCameraAttributes": { - "enabled": true, + "enabled": false, "optional": true }, "ValidateModelName": { From 22d783282e4b34c71596c39eb97e12b978e13748 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 19:13:46 +0100 Subject: [PATCH 064/106] fix entity type mapping --- .../ftrack/events/event_task_to_parent_status.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_parent_status.py b/pype/modules/ftrack/events/event_task_to_parent_status.py index 42d3ca0877..4620f84395 100644 --- a/pype/modules/ftrack/events/event_task_to_parent_status.py +++ b/pype/modules/ftrack/events/event_task_to_parent_status.py @@ -122,10 +122,16 @@ class TaskStatusToParent(BaseEvent): for entity in parent_entities: obj_ids.add(entity["object_type_id"]) - object_type_name_by_id = { - object_type["id"]: object_type["name"] - for object_type in object_types + types_mapping = { + _type.lower(): _type + for _type in session.types } + # Map object type id by lowered and modified object type name + object_type_name_by_id = {} + for object_type in object_types: + mapping_name = object_type["name"].lower().replace(" ", "") + obj_id = object_type["id"] + object_type_name_by_id[obj_id] = types_mapping[mapping_name] project_schema = project_entity["project_schema"] available_statuses_by_obj_id = {} From d0b827ccbb42ea2ffc99b764ba3e0e609097c184 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 19:41:04 +0100 Subject: [PATCH 065/106] added label check on is_group item --- pype/tools/settings/settings/widgets/item_types.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 4428d51cef..15cd969d18 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -196,6 +196,12 @@ class SettingObject: self.key = self.schema_data["key"] + self.label = self.schema_data.get("label") + if not self.label and self._is_group: + raise ValueError( + "Item is set as `is_group` but has empty `label`." + ) + @property def user_role(self): """Tool is running with any user role. From 3564be679b3731cabf360d0f4e5dbcad0e017aef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 19:41:26 +0100 Subject: [PATCH 066/106] added `use_label_wrap` attribute to `list` item --- .../settings/settings/widgets/item_types.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 15cd969d18..52518ab98e 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1582,6 +1582,25 @@ class ListWidget(QtWidgets.QWidget, InputObject): self.initial_attributes(schema_data, parent, as_widget) + self.use_label_wrap = schema_data.get("use_label_wrap") or False + # Used only if `use_label_wrap` is set to True + self.collapsible = schema_data.get("collapsible") or True + self.collapsed = schema_data.get("collapsed") or False + + self.expand_in_grid = bool(self.use_label_wrap) + + if self.as_widget and self.use_label_wrap: + raise ValueError( + "`ListWidget` can't have set `use_label_wrap` to True and" + " be used as widget at the same time." + ) + + if self.use_label_wrap and not self.label: + raise ValueError( + "`ListWidget` can't have set `use_label_wrap` to True and" + " not have set \"label\" key at the same time." + ) + self.input_fields = [] object_type = schema_data["object_type"] From c32a9766f771063efd08b3c83a93c745e581aa1f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 19:41:57 +0100 Subject: [PATCH 067/106] remove backwards compatibility --- pype/tools/settings/settings/widgets/item_types.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 52518ab98e..40211669a5 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1610,14 +1610,6 @@ class ListWidget(QtWidgets.QWidget, InputObject): self.item_schema = { "type": object_type } - # Backwards compatibility - input_modifiers = schema_data.get("input_modifiers") or {} - if input_modifiers: - self.log.warning(( - "Used deprecated key `input_modifiers` to define item." - " Rather use `object_type` as dictionary with modifiers." - )) - self.item_schema.update(input_modifiers) def create_ui(self, label_widget=None): layout = QtWidgets.QHBoxLayout(self) From e05ef9b431b6843711490563b799545114fc1af8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 19:42:33 +0100 Subject: [PATCH 068/106] create_ui in `list` item wraps item with collapsible label --- .../settings/settings/widgets/item_types.py | 64 +++++++++++++------ 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 40211669a5..5c377cb9ec 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1612,34 +1612,62 @@ class ListWidget(QtWidgets.QWidget, InputObject): } def create_ui(self, label_widget=None): - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 5) - layout.setSpacing(5) + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) - if not self.as_widget and not label_widget: - label = self.schema_data.get("label") - if label: - label_widget = QtWidgets.QLabel(label, self) - layout.addWidget(label_widget, alignment=QtCore.Qt.AlignTop) - elif self._is_group: - raise KeyError(( - "Schema item must contain \"label\" if `is_group` is True" - " to be able visualize changes and show actions." - )) + body_widget = None + if self.as_widget: + pass + + elif self.use_label_wrap: + body_widget = ExpandingWidget(self.label, self) + main_layout.addWidget(body_widget) + + label_widget = body_widget.label_widget + + elif not label_widget: + if self.label: + label_widget = QtWidgets.QLabel(self.label, self) + main_layout.addWidget( + label_widget, alignment=QtCore.Qt.AlignTop + ) self.label_widget = label_widget - inputs_widget = QtWidgets.QWidget(self) - inputs_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - layout.addWidget(inputs_widget) + self.body_widget = body_widget + if body_widget is None: + content_parent_widget = self + else: + content_parent_widget = body_widget + + content_state = "" + + inputs_widget = QtWidgets.QWidget(content_parent_widget) + inputs_widget.setObjectName("ContentWidget") + inputs_widget.setProperty("content_state", content_state) inputs_layout = QtWidgets.QVBoxLayout(inputs_widget) - inputs_layout.setContentsMargins(0, 0, 0, 0) - inputs_layout.setSpacing(3) + inputs_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 5) + if body_widget is None: + main_layout.addWidget(inputs_widget) + else: + body_widget.set_content_widget(inputs_widget) + + self.body_widget = body_widget self.inputs_widget = inputs_widget self.inputs_layout = inputs_layout + if body_widget: + if not self.collapsible: + body_widget.hide_toolbox(hide_content=False) + + elif self.collapsed: + body_widget.toggle_content() + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.add_row(is_empty=True) def count(self): From f068a863375a4bda1b2f9e06996380e6beb59572 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 19:42:53 +0100 Subject: [PATCH 069/106] update_style works with label wrapper in list item --- .../settings/settings/widgets/item_types.py | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 5c377cb9ec..30ff128c2d 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1840,18 +1840,44 @@ class ListWidget(QtWidgets.QWidget, InputObject): return True return False - def update_style(self): + def update_style(self, is_overriden=None): if not self.label_widget: return - state = self._style_state() + child_invalid = self.child_invalid + if self.body_widget: + child_state = self.style_state( + self.child_has_studio_override, + child_invalid, + self.child_overriden, + self.child_modified + ) + if child_state: + child_state = "child-{}".format(child_state) + + if child_state != self._child_state: + self.body_widget.side_line_widget.setProperty( + "state", child_state + ) + self.body_widget.side_line_widget.style().polish( + self.body_widget.side_line_widget + ) + self._child_state = child_state + + state = self.style_state( + self.had_studio_override, + child_invalid, + self.is_overriden, + self.is_modified + ) if self._state == state: return - self._state = state self.label_widget.setProperty("state", state) self.label_widget.style().polish(self.label_widget) + self._state = state + def item_value(self): output = [] for item in self.input_fields: From fc6426ef631faf2b38b88ce5fff866ce4755e143 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 19:44:04 +0100 Subject: [PATCH 070/106] key `last_workfile_on_startup` has `use_label_wrap` now instead of `collapsible-wrap` as parent item --- .../schemas/schema_global_tools.json | 61 +++++++++---------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_global_tools.json b/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_global_tools.json index f221e87aa9..d89477edd1 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_global_tools.json +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_global_tools.json @@ -29,41 +29,36 @@ "label": "Workfiles", "children": [ { - "type": "collapsible-wrap", + "type": "list", + "key": "last_workfile_on_startup", "label": "Open last workfiles on launch", - "children": [ - { - "type": "list", - "key": "last_workfile_on_startup", - "label": "", - "is_group": true, - "object_type": { - "type": "dict", - "children": [ - { - "key": "hosts", - "label": "Hosts", - "type": "list", - "object_type": "text" - }, - { - "key": "tasks", - "label": "Tasks", - "type": "list", - "object_type": "text" - }, - { - "type": "splitter" - }, - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - } - ] + "is_group": true, + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "hosts", + "label": "Hosts", + "type": "list", + "object_type": "text" + }, + { + "key": "tasks", + "label": "Tasks", + "type": "list", + "object_type": "text" + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" } - } - ] + ] + } }, { "type": "dict-modifiable", From 9a6b710b74e52b1226d002f14ed259357c1f6c93 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 19:44:13 +0100 Subject: [PATCH 071/106] added label to Roots --- .../settings/gui_schemas/projects_schema/schema_main.json | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_main.json b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_main.json index 5724e50cdc..73266a9e79 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_main.json +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_main.json @@ -9,6 +9,7 @@ { "type": "anatomy_roots", "key": "roots", + "label": "Roots", "is_file": true }, { From d90991fd39d65dac11b9b9016ba079d3bcbf5a77 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 19:48:08 +0100 Subject: [PATCH 072/106] empty labels are removed --- .../projects_schema/schema_project_global.json | 10 +++++----- .../schemas/schema_anatomy_imageio.json | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_global.json b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_global.json index d6c413fea3..ab9b56115d 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_global.json +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_global.json @@ -18,11 +18,11 @@ "type": "collapsible-wrap", "label": "Project Folder Structure", "children": [ - { - "type": "raw-json", - "key": "project_folder_structure", - "label": "" - }] + { + "type": "raw-json", + "key": "project_folder_structure" + } + ] }, { diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_anatomy_imageio.json b/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_anatomy_imageio.json index 4f75e1171a..0032e3de06 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_anatomy_imageio.json +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/schemas/schema_anatomy_imageio.json @@ -92,7 +92,6 @@ { "type": "list", "key": "inputs", - "label": "", "object_type": { "type": "dict", "children": [ @@ -330,7 +329,6 @@ { "type": "list", "key": "inputs", - "label": "", "object_type": { "type": "dict", "children": [ From 3c23598b3e0a2fd72dde023f133626ceae012564 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 19:48:24 +0100 Subject: [PATCH 073/106] raw json does not need to fill label --- pype/tools/settings/settings/widgets/item_types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 30ff128c2d..c5112473e6 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1329,9 +1329,9 @@ class RawJsonWidget(QtWidgets.QWidget, InputObject): self.setFocusProxy(self.input_field) if not self.as_widget and not label_widget: - label = self.schema_data["label"] - label_widget = QtWidgets.QLabel(label) - layout.addWidget(label_widget, 0, alignment=QtCore.Qt.AlignTop) + if self.label: + label_widget = QtWidgets.QLabel(self.label) + layout.addWidget(label_widget, 0, alignment=QtCore.Qt.AlignTop) self.label_widget = label_widget layout.addWidget(self.input_field, 1, alignment=QtCore.Qt.AlignTop) From 9b5c6a006234b2c58202446fb7c4314bc08023b0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 19:54:38 +0100 Subject: [PATCH 074/106] added readme info about `use_label_wrap` --- pype/tools/settings/settings/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/tools/settings/settings/README.md b/pype/tools/settings/settings/README.md index 53f21aad06..643043b6c8 100644 --- a/pype/tools/settings/settings/README.md +++ b/pype/tools/settings/settings/README.md @@ -269,13 +269,17 @@ ``` ## Inputs for setting value using Pure inputs -- these inputs also have required `"key"` and `"label"` +- these inputs also have required `"key"` +- attribute `"label"` is required in few conditions + - when item is marked `as_group` or when `use_label_wrap` - they use Pure inputs "as widgets" ### list - output is list - items can be added and removed - items in list must be the same type +- to wrap item in collapsible widget with label on top set `use_label_wrap` to `True` + - when this is used `collapsible` and `collapsed` can be set (same as `dict` item does) - type of items is defined with key `"object_type"` - there are 2 possible ways how to set the type: 1.) dictionary with item modifiers (`number` input has `minimum`, `maximum` and `decimals`) in that case item type must be set as value of `"type"` (example below) From e0da4dd9f7825869e6f770aab778f3476dd59b29 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Dec 2020 19:56:25 +0100 Subject: [PATCH 075/106] add `use_label_wrap` to ftrack's `parent_status_by_task_status` key --- .../gui_schemas/projects_schema/schema_project_ftrack.json | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json index f5e3dcc47a..1554989c55 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/schema_project_ftrack.json @@ -180,6 +180,7 @@ "type": "list", "key": "parent_status_by_task_status", "label": "Change parent status if a single task matches", + "use_label_wrap": true, "object_type": { "type": "dict", "children": [ From 70034aee60baf780570a6535d3c078f7f76c6bac Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Dec 2020 11:10:06 +0100 Subject: [PATCH 076/106] `plugins_presets` was removed as is not used anymore --- .../ftrack/actions/action_applications.py | 8 ++--- .../actions/action_batch_task_creation.py | 4 +-- .../action_clean_hierarchical_attributes.py | 4 +-- .../actions/action_client_review_sort.py | 4 +-- .../ftrack/actions/action_component_open.py | 4 +-- .../actions/action_create_cust_attrs.py | 4 +-- .../ftrack/actions/action_create_folders.py | 4 +-- .../action_create_project_structure.py | 4 +-- .../ftrack/actions/action_delete_asset.py | 4 +-- .../actions/action_delete_old_versions.py | 4 +-- .../modules/ftrack/actions/action_delivery.py | 4 +-- pype/modules/ftrack/actions/action_djvview.py | 9 +++-- .../ftrack/actions/action_job_killer.py | 4 +-- .../ftrack/actions/action_multiple_notes.py | 4 +-- .../ftrack/actions/action_prepare_project.py | 4 +-- pype/modules/ftrack/actions/action_rv.py | 13 +++---- pype/modules/ftrack/actions/action_seed.py | 4 +-- .../action_store_thumbnails_to_avalon.py | 4 +-- .../ftrack/actions/action_sync_to_avalon.py | 4 +-- pype/modules/ftrack/actions/action_test.py | 4 +-- .../actions/action_thumbnail_to_childern.py | 4 +-- .../actions/action_thumbnail_to_parent.py | 4 +-- .../ftrack/actions/action_where_run_ask.py | 4 +-- .../ftrack/actions/action_where_run_show.py | 4 +-- .../action_push_frame_values_to_task.py | 4 +-- .../ftrack/events/action_sync_to_avalon.py | 4 +-- .../events/event_del_avalon_id_from_new.py | 4 +-- .../events/event_first_version_status.py | 4 +-- .../ftrack/events/event_next_task_update.py | 4 +-- .../events/event_push_frame_values_to_task.py | 4 +-- .../ftrack/events/event_radio_buttons.py | 4 +-- .../ftrack/events/event_sync_to_avalon.py | 8 ++--- .../events/event_task_to_parent_status.py | 4 +-- .../events/event_task_to_version_status.py | 4 +-- pype/modules/ftrack/events/event_test.py | 8 ++--- .../ftrack/events/event_thumbnail_updates.py | 4 +-- .../ftrack/events/event_user_assigment.py | 4 +-- .../events/event_version_to_task_statuses.py | 4 +-- .../ftrack/ftrack_server/ftrack_server.py | 13 +------ .../ftrack/lib/ftrack_action_handler.py | 4 +-- .../modules/ftrack/lib/ftrack_base_handler.py | 34 ++++++------------- .../ftrack/lib/ftrack_event_handler.py | 4 --- 42 files changed, 97 insertions(+), 136 deletions(-) diff --git a/pype/modules/ftrack/actions/action_applications.py b/pype/modules/ftrack/actions/action_applications.py index cf047a658d..5b6657793a 100644 --- a/pype/modules/ftrack/actions/action_applications.py +++ b/pype/modules/ftrack/actions/action_applications.py @@ -28,8 +28,8 @@ class AppplicationsAction(BaseAction): identifier = "pype_app.{}.".format(str(uuid4())) icon_url = os.environ.get("PYPE_STATICS_SERVER") - def __init__(self, session, plugins_presets=None): - super().__init__(session, plugins_presets) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.application_manager = ApplicationManager() self.dbcon = AvalonMongoDB() @@ -210,6 +210,6 @@ class AppplicationsAction(BaseAction): } -def register(session, plugins_presets=None): +def register(session): """Register action. Called when used as an event plugin.""" - AppplicationsAction(session, plugins_presets).register() + AppplicationsAction(session).register() diff --git a/pype/modules/ftrack/actions/action_batch_task_creation.py b/pype/modules/ftrack/actions/action_batch_task_creation.py index ef370d55eb..477971773d 100644 --- a/pype/modules/ftrack/actions/action_batch_task_creation.py +++ b/pype/modules/ftrack/actions/action_batch_task_creation.py @@ -158,7 +158,7 @@ class BatchTasksAction(BaseAction): } -def register(session, plugins_presets=None): +def register(session): '''Register action. Called when used as an event plugin.''' - BatchTasksAction(session, plugins_presets).register() + BatchTasksAction(session).register() diff --git a/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py b/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py index e81e587f0a..dc3a638192 100644 --- a/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py +++ b/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py @@ -98,7 +98,7 @@ class CleanHierarchicalAttrsAction(BaseAction): return True -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - CleanHierarchicalAttrsAction(session, plugins_presets).register() + CleanHierarchicalAttrsAction(session).register() diff --git a/pype/modules/ftrack/actions/action_client_review_sort.py b/pype/modules/ftrack/actions/action_client_review_sort.py index 72387fe695..1c5c429cf2 100644 --- a/pype/modules/ftrack/actions/action_client_review_sort.py +++ b/pype/modules/ftrack/actions/action_client_review_sort.py @@ -84,7 +84,7 @@ class ClientReviewSort(BaseAction): } -def register(session, plugins_presets={}): +def register(session): '''Register action. Called when used as an event plugin.''' - ClientReviewSort(session, plugins_presets).register() + ClientReviewSort(session).register() diff --git a/pype/modules/ftrack/actions/action_component_open.py b/pype/modules/ftrack/actions/action_component_open.py index 5fe8fe831b..2928f54b15 100644 --- a/pype/modules/ftrack/actions/action_component_open.py +++ b/pype/modules/ftrack/actions/action_component_open.py @@ -60,7 +60,7 @@ class ComponentOpen(BaseAction): } -def register(session, plugins_presets={}): +def register(session): '''Register action. Called when used as an event plugin.''' - ComponentOpen(session, plugins_presets).register() + ComponentOpen(session).register() diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index a63c77c198..9d6c16b556 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -814,7 +814,7 @@ class CustomAttributes(BaseAction): } -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - CustomAttributes(session, plugins_presets).register() + CustomAttributes(session).register() diff --git a/pype/modules/ftrack/actions/action_create_folders.py b/pype/modules/ftrack/actions/action_create_folders.py index a131a0e35b..d70232ae8f 100644 --- a/pype/modules/ftrack/actions/action_create_folders.py +++ b/pype/modules/ftrack/actions/action_create_folders.py @@ -243,6 +243,6 @@ class CreateFolders(BaseAction): return os.path.normpath(filled_template.split("{")[0]) -def register(session, plugins_presets={}): +def register(session): """Register plugin. Called when used as an plugin.""" - CreateFolders(session, plugins_presets).register() + CreateFolders(session).register() diff --git a/pype/modules/ftrack/actions/action_create_project_structure.py b/pype/modules/ftrack/actions/action_create_project_structure.py index 0815f82a69..64b4ba6727 100644 --- a/pype/modules/ftrack/actions/action_create_project_structure.py +++ b/pype/modules/ftrack/actions/action_create_project_structure.py @@ -238,5 +238,5 @@ class CreateProjectFolders(BaseAction): os.makedirs(path.format(project_root=project_root)) -def register(session, plugins_presets={}): - CreateProjectFolders(session, plugins_presets).register() +def register(session): + CreateProjectFolders(session).register() diff --git a/pype/modules/ftrack/actions/action_delete_asset.py b/pype/modules/ftrack/actions/action_delete_asset.py index 7d2dac3320..4720273c81 100644 --- a/pype/modules/ftrack/actions/action_delete_asset.py +++ b/pype/modules/ftrack/actions/action_delete_asset.py @@ -662,7 +662,7 @@ class DeleteAssetSubset(BaseAction): } -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - DeleteAssetSubset(session, plugins_presets).register() + DeleteAssetSubset(session).register() diff --git a/pype/modules/ftrack/actions/action_delete_old_versions.py b/pype/modules/ftrack/actions/action_delete_old_versions.py index b55f091fdc..31d15da9e5 100644 --- a/pype/modules/ftrack/actions/action_delete_old_versions.py +++ b/pype/modules/ftrack/actions/action_delete_old_versions.py @@ -577,7 +577,7 @@ class DeleteOldVersions(BaseAction): return (os.path.normpath(path), sequence_path) -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - DeleteOldVersions(session, plugins_presets).register() + DeleteOldVersions(session).register() diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index 0f63f7f7ea..853fe64ec7 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -692,7 +692,7 @@ class Delivery(BaseAction): } -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - Delivery(session, plugins_presets).register() + Delivery(session).register() diff --git a/pype/modules/ftrack/actions/action_djvview.py b/pype/modules/ftrack/actions/action_djvview.py index 6f667c0604..6036f9a35b 100644 --- a/pype/modules/ftrack/actions/action_djvview.py +++ b/pype/modules/ftrack/actions/action_djvview.py @@ -20,9 +20,8 @@ class DJVViewAction(BaseAction): "sgi", "rgba", "rgb", "bw", "tga", "tiff", "tif", "img" ] - def __init__(self, session, plugins_presets): - '''Expects a ftrack_api.Session instance''' - super().__init__(session, plugins_presets) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.djv_path = self.find_djv_path() @@ -208,7 +207,7 @@ class DJVViewAction(BaseAction): return True -def register(session, plugins_presets={}): +def register(session): """Register hooks.""" - DJVViewAction(session, plugins_presets).register() + DJVViewAction(session).register() diff --git a/pype/modules/ftrack/actions/action_job_killer.py b/pype/modules/ftrack/actions/action_job_killer.py index ff23da2a54..cb193b88ce 100644 --- a/pype/modules/ftrack/actions/action_job_killer.py +++ b/pype/modules/ftrack/actions/action_job_killer.py @@ -112,7 +112,7 @@ class JobKiller(BaseAction): } -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - JobKiller(session, plugins_presets).register() + JobKiller(session).register() diff --git a/pype/modules/ftrack/actions/action_multiple_notes.py b/pype/modules/ftrack/actions/action_multiple_notes.py index c1a5cc6ce0..d88a91dd92 100644 --- a/pype/modules/ftrack/actions/action_multiple_notes.py +++ b/pype/modules/ftrack/actions/action_multiple_notes.py @@ -104,7 +104,7 @@ class MultipleNotes(BaseAction): return True -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - MultipleNotes(session, plugins_presets).register() + MultipleNotes(session).register() diff --git a/pype/modules/ftrack/actions/action_prepare_project.py b/pype/modules/ftrack/actions/action_prepare_project.py index 970bb3d86b..98493f65c7 100644 --- a/pype/modules/ftrack/actions/action_prepare_project.py +++ b/pype/modules/ftrack/actions/action_prepare_project.py @@ -454,6 +454,6 @@ class PrepareProject(BaseAction): self.log.debug("*** Creating project specifig configs Finished ***") -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - PrepareProject(session, plugins_presets).register() + PrepareProject(session).register() diff --git a/pype/modules/ftrack/actions/action_rv.py b/pype/modules/ftrack/actions/action_rv.py index eeb5672047..1c5ccfaed0 100644 --- a/pype/modules/ftrack/actions/action_rv.py +++ b/pype/modules/ftrack/actions/action_rv.py @@ -19,13 +19,8 @@ class RVAction(BaseAction): allowed_types = ["img", "mov", "exr", "mp4"] - def __init__(self, session, plugins_presets): - """ Constructor - - :param session: ftrack Session - :type session: :class:`ftrack_api.Session` - """ - super().__init__(session, plugins_presets) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) # QUESTION load RV application data from AppplicationManager? rv_path = None @@ -317,7 +312,7 @@ class RVAction(BaseAction): return paths -def register(session, plugins_presets={}): +def register(session): """Register hooks.""" - RVAction(session, plugins_presets).register() + RVAction(session).register() diff --git a/pype/modules/ftrack/actions/action_seed.py b/pype/modules/ftrack/actions/action_seed.py index d6288a03aa..2610a25024 100644 --- a/pype/modules/ftrack/actions/action_seed.py +++ b/pype/modules/ftrack/actions/action_seed.py @@ -428,7 +428,7 @@ class SeedDebugProject(BaseAction): return True -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - SeedDebugProject(session, plugins_presets).register() + SeedDebugProject(session).register() diff --git a/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py b/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py index e6f98d23cd..6df8271381 100644 --- a/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py +++ b/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py @@ -457,5 +457,5 @@ class StoreThumbnailsToAvalon(BaseAction): return output -def register(session, plugins_presets={}): - StoreThumbnailsToAvalon(session, plugins_presets).register() +def register(session): + StoreThumbnailsToAvalon(session).register() diff --git a/pype/modules/ftrack/actions/action_sync_to_avalon.py b/pype/modules/ftrack/actions/action_sync_to_avalon.py index dfe1f2c464..6077511092 100644 --- a/pype/modules/ftrack/actions/action_sync_to_avalon.py +++ b/pype/modules/ftrack/actions/action_sync_to_avalon.py @@ -187,7 +187,7 @@ class SyncToAvalonLocal(BaseAction): pass -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - SyncToAvalonLocal(session, plugins_presets).register() + SyncToAvalonLocal(session).register() diff --git a/pype/modules/ftrack/actions/action_test.py b/pype/modules/ftrack/actions/action_test.py index e4936274b3..c12906e340 100644 --- a/pype/modules/ftrack/actions/action_test.py +++ b/pype/modules/ftrack/actions/action_test.py @@ -22,5 +22,5 @@ class TestAction(BaseAction): return True -def register(session, plugins_presets={}): - TestAction(session, plugins_presets).register() +def register(session): + TestAction(session).register() diff --git a/pype/modules/ftrack/actions/action_thumbnail_to_childern.py b/pype/modules/ftrack/actions/action_thumbnail_to_childern.py index 3c6af10b43..604688d221 100644 --- a/pype/modules/ftrack/actions/action_thumbnail_to_childern.py +++ b/pype/modules/ftrack/actions/action_thumbnail_to_childern.py @@ -59,7 +59,7 @@ class ThumbToChildren(BaseAction): } -def register(session, plugins_presets={}): +def register(session): '''Register action. Called when used as an event plugin.''' - ThumbToChildren(session, plugins_presets).register() + ThumbToChildren(session).register() diff --git a/pype/modules/ftrack/actions/action_thumbnail_to_parent.py b/pype/modules/ftrack/actions/action_thumbnail_to_parent.py index fb473f9aa5..5734ea6abc 100644 --- a/pype/modules/ftrack/actions/action_thumbnail_to_parent.py +++ b/pype/modules/ftrack/actions/action_thumbnail_to_parent.py @@ -85,7 +85,7 @@ class ThumbToParent(BaseAction): } -def register(session, plugins_presets={}): +def register(session): '''Register action. Called when used as an event plugin.''' - ThumbToParent(session, plugins_presets).register() + ThumbToParent(session).register() diff --git a/pype/modules/ftrack/actions/action_where_run_ask.py b/pype/modules/ftrack/actions/action_where_run_ask.py index 42640fb506..64957208da 100644 --- a/pype/modules/ftrack/actions/action_where_run_ask.py +++ b/pype/modules/ftrack/actions/action_where_run_ask.py @@ -27,7 +27,7 @@ class ActionAskWhereIRun(BaseAction): return True -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - ActionAskWhereIRun(session, plugins_presets).register() + ActionAskWhereIRun(session).register() diff --git a/pype/modules/ftrack/actions/action_where_run_show.py b/pype/modules/ftrack/actions/action_where_run_show.py index a084547a45..f872d17d27 100644 --- a/pype/modules/ftrack/actions/action_where_run_show.py +++ b/pype/modules/ftrack/actions/action_where_run_show.py @@ -76,7 +76,7 @@ class ActionShowWhereIRun(BaseAction): return True -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - ActionShowWhereIRun(session, plugins_presets).register() + ActionShowWhereIRun(session).register() diff --git a/pype/modules/ftrack/events/action_push_frame_values_to_task.py b/pype/modules/ftrack/events/action_push_frame_values_to_task.py index de61728a62..87d9d5afe9 100644 --- a/pype/modules/ftrack/events/action_push_frame_values_to_task.py +++ b/pype/modules/ftrack/events/action_push_frame_values_to_task.py @@ -430,5 +430,5 @@ class PushHierValuesToNonHier(ServerAction): session.commit() -def register(session, plugins_presets={}): - PushHierValuesToNonHier(session, plugins_presets).register() +def register(session): + PushHierValuesToNonHier(session).register() diff --git a/pype/modules/ftrack/events/action_sync_to_avalon.py b/pype/modules/ftrack/events/action_sync_to_avalon.py index 80b5939d84..486b977f04 100644 --- a/pype/modules/ftrack/events/action_sync_to_avalon.py +++ b/pype/modules/ftrack/events/action_sync_to_avalon.py @@ -182,6 +182,6 @@ class SyncToAvalonServer(ServerAction): pass -def register(session, plugins_presets={}): +def register(session): '''Register plugin. Called when used as an plugin.''' - SyncToAvalonServer(session, plugins_presets).register() + SyncToAvalonServer(session).register() diff --git a/pype/modules/ftrack/events/event_del_avalon_id_from_new.py b/pype/modules/ftrack/events/event_del_avalon_id_from_new.py index ee82c9589d..21e581e76a 100644 --- a/pype/modules/ftrack/events/event_del_avalon_id_from_new.py +++ b/pype/modules/ftrack/events/event_del_avalon_id_from_new.py @@ -47,6 +47,6 @@ class DelAvalonIdFromNew(BaseEvent): continue -def register(session, plugins_presets): +def register(session): '''Register plugin. Called when used as an plugin.''' - DelAvalonIdFromNew(session, plugins_presets).register() + DelAvalonIdFromNew(session).register() diff --git a/pype/modules/ftrack/events/event_first_version_status.py b/pype/modules/ftrack/events/event_first_version_status.py index 8754d092ab..cfca047c09 100644 --- a/pype/modules/ftrack/events/event_first_version_status.py +++ b/pype/modules/ftrack/events/event_first_version_status.py @@ -182,7 +182,7 @@ class FirstVersionStatus(BaseEvent): return filtered_ents -def register(session, plugins_presets): +def register(session): '''Register plugin. Called when used as an plugin.''' - FirstVersionStatus(session, plugins_presets).register() + FirstVersionStatus(session).register() diff --git a/pype/modules/ftrack/events/event_next_task_update.py b/pype/modules/ftrack/events/event_next_task_update.py index deb789f981..025bac0d07 100644 --- a/pype/modules/ftrack/events/event_next_task_update.py +++ b/pype/modules/ftrack/events/event_next_task_update.py @@ -225,5 +225,5 @@ class NextTaskUpdate(BaseEvent): ) -def register(session, plugins_presets): - NextTaskUpdate(session, plugins_presets).register() +def register(session): + NextTaskUpdate(session).register() diff --git a/pype/modules/ftrack/events/event_push_frame_values_to_task.py b/pype/modules/ftrack/events/event_push_frame_values_to_task.py index 00457c8bfc..061002c13f 100644 --- a/pype/modules/ftrack/events/event_push_frame_values_to_task.py +++ b/pype/modules/ftrack/events/event_push_frame_values_to_task.py @@ -364,5 +364,5 @@ class PushFrameValuesToTaskEvent(BaseEvent): return output, hiearchical -def register(session, plugins_presets): - PushFrameValuesToTaskEvent(session, plugins_presets).register() +def register(session): + PushFrameValuesToTaskEvent(session).register() diff --git a/pype/modules/ftrack/events/event_radio_buttons.py b/pype/modules/ftrack/events/event_radio_buttons.py index b2ab4e75ec..90811e5f45 100644 --- a/pype/modules/ftrack/events/event_radio_buttons.py +++ b/pype/modules/ftrack/events/event_radio_buttons.py @@ -34,7 +34,7 @@ class RadioButtons(BaseEvent): session.commit() -def register(session, plugins_presets): +def register(session): '''Register plugin. Called when used as an plugin.''' - RadioButtons(session, plugins_presets).register() + RadioButtons(session).register() diff --git a/pype/modules/ftrack/events/event_sync_to_avalon.py b/pype/modules/ftrack/events/event_sync_to_avalon.py index 2a69a559bd..0209dfd53a 100644 --- a/pype/modules/ftrack/events/event_sync_to_avalon.py +++ b/pype/modules/ftrack/events/event_sync_to_avalon.py @@ -53,7 +53,7 @@ class SyncToAvalonEvent(BaseEvent): created_entities = [] report_splitter = {"type": "label", "value": "---"} - def __init__(self, session, plugins_presets={}): + def __init__(self, session): '''Expects a ftrack_api.Session instance''' # Debug settings # - time expiration in seconds @@ -67,7 +67,7 @@ class SyncToAvalonEvent(BaseEvent): self.dbcon = AvalonMongoDB() # Set processing session to not use global self.set_process_session(session) - super().__init__(session, plugins_presets) + super().__init__(session) def debug_logs(self): """This is debug method for printing small debugs messages. """ @@ -2513,6 +2513,6 @@ class SyncToAvalonEvent(BaseEvent): return mongo_id_configuration_id -def register(session, plugins_presets): +def register(session): '''Register plugin. Called when used as an plugin.''' - SyncToAvalonEvent(session, plugins_presets).register() + SyncToAvalonEvent(session).register() diff --git a/pype/modules/ftrack/events/event_task_to_parent_status.py b/pype/modules/ftrack/events/event_task_to_parent_status.py index 4620f84395..2bb7be1a26 100644 --- a/pype/modules/ftrack/events/event_task_to_parent_status.py +++ b/pype/modules/ftrack/events/event_task_to_parent_status.py @@ -419,5 +419,5 @@ class TaskStatusToParent(BaseEvent): return output -def register(session, plugins_presets): - TaskStatusToParent(session, plugins_presets).register() +def register(session): + TaskStatusToParent(session).register() diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index b49fd01a91..8d226424c3 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -372,5 +372,5 @@ class TaskToVersionStatus(BaseEvent): return last_asset_versions_by_task_id -def register(session, plugins_presets): - TaskToVersionStatus(session, plugins_presets).register() +def register(session): + TaskToVersionStatus(session).register() diff --git a/pype/modules/ftrack/events/event_test.py b/pype/modules/ftrack/events/event_test.py index 0a86bd1754..c07f8b8d16 100644 --- a/pype/modules/ftrack/events/event_test.py +++ b/pype/modules/ftrack/events/event_test.py @@ -1,7 +1,3 @@ -import os -import sys -import re -import ftrack_api from pype.modules.ftrack import BaseEvent @@ -20,7 +16,7 @@ class TestEvent(BaseEvent): return True -def register(session, plugins_presets): +def register(session): '''Register plugin. Called when used as an plugin.''' - TestEvent(session, plugins_presets).register() + TestEvent(session).register() diff --git a/pype/modules/ftrack/events/event_thumbnail_updates.py b/pype/modules/ftrack/events/event_thumbnail_updates.py index 0044c5e21c..09d992b8c4 100644 --- a/pype/modules/ftrack/events/event_thumbnail_updates.py +++ b/pype/modules/ftrack/events/event_thumbnail_updates.py @@ -151,5 +151,5 @@ class ThumbnailEvents(BaseEvent): return filtered_entities_info -def register(session, plugins_presets): - ThumbnailEvents(session, plugins_presets).register() +def register(session): + ThumbnailEvents(session).register() diff --git a/pype/modules/ftrack/events/event_user_assigment.py b/pype/modules/ftrack/events/event_user_assigment.py index 9b0dfe84d1..59880fabe5 100644 --- a/pype/modules/ftrack/events/event_user_assigment.py +++ b/pype/modules/ftrack/events/event_user_assigment.py @@ -250,9 +250,9 @@ class UserAssigmentEvent(BaseEvent): return True -def register(session, plugins_presets): +def register(session): """ Register plugin. Called when used as an plugin. """ - UserAssigmentEvent(session, plugins_presets).register() + UserAssigmentEvent(session).register() diff --git a/pype/modules/ftrack/events/event_version_to_task_statuses.py b/pype/modules/ftrack/events/event_version_to_task_statuses.py index d094c2a8fd..03f873f2cd 100644 --- a/pype/modules/ftrack/events/event_version_to_task_statuses.py +++ b/pype/modules/ftrack/events/event_version_to_task_statuses.py @@ -241,7 +241,7 @@ class VersionToTaskStatus(BaseEvent): return output -def register(session, plugins_presets): +def register(session): '''Register plugin. Called when used as an plugin.''' - VersionToTaskStatus(session, plugins_presets).register() + VersionToTaskStatus(session).register() diff --git a/pype/modules/ftrack/ftrack_server/ftrack_server.py b/pype/modules/ftrack/ftrack_server/ftrack_server.py index 93c7cd3a67..3e0c752596 100644 --- a/pype/modules/ftrack/ftrack_server/ftrack_server.py +++ b/pype/modules/ftrack/ftrack_server/ftrack_server.py @@ -108,21 +108,10 @@ class FtrackServer: " in registered paths: \"{}\"" ).format("| ".join(paths))) - # TODO replace with settings or get rid of passing the dictionary - plugins_presets = {} - - function_counter = 0 for function_dict in register_functions_dict: register = function_dict["register"] try: - if len(inspect.signature(register).parameters) == 1: - register(self.session) - else: - register(self.session, plugins_presets=plugins_presets) - - if function_counter % 7 == 0: - time.sleep(0.1) - function_counter += 1 + register(self.session) except Exception as exc: msg = '"{}" - register was not successful ({})'.format( function_dict['name'], str(exc) diff --git a/pype/modules/ftrack/lib/ftrack_action_handler.py b/pype/modules/ftrack/lib/ftrack_action_handler.py index a550d9e7d3..e04ed6b404 100644 --- a/pype/modules/ftrack/lib/ftrack_action_handler.py +++ b/pype/modules/ftrack/lib/ftrack_action_handler.py @@ -29,7 +29,7 @@ class BaseAction(BaseHandler): icon = None type = 'Action' - def __init__(self, session, plugins_presets={}): + def __init__(self, session): '''Expects a ftrack_api.Session instance''' if self.label is None: raise ValueError('Action missing label.') @@ -37,7 +37,7 @@ class BaseAction(BaseHandler): if self.identifier is None: raise ValueError('Action missing identifier.') - super().__init__(session, plugins_presets) + super().__init__(session) def register(self): ''' diff --git a/pype/modules/ftrack/lib/ftrack_base_handler.py b/pype/modules/ftrack/lib/ftrack_base_handler.py index 72b6272b76..022c4f0829 100644 --- a/pype/modules/ftrack/lib/ftrack_base_handler.py +++ b/pype/modules/ftrack/lib/ftrack_base_handler.py @@ -44,7 +44,7 @@ class BaseHandler(object): """Helper to join keys to query.""" return ",".join(["\"{}\"".format(key) for key in keys]) - def __init__(self, session, plugins_presets=None): + def __init__(self, session): '''Expects a ftrack_api.Session instance''' self.log = Logger().get_logger(self.__class__.__name__) if not( @@ -65,31 +65,19 @@ class BaseHandler(object): # Using decorator self.register = self.register_decorator(self.register) self.launch = self.launch_log(self.launch) - if plugins_presets is None: - plugins_presets = {} - self.plugins_presets = plugins_presets # Decorator def register_decorator(self, func): @functools.wraps(func) def wrapper_register(*args, **kwargs): - - presets_data = self.plugins_presets.get(self.__class__.__name__) - if presets_data: - for key, value in presets_data.items(): - if not hasattr(self, key): - continue - setattr(self, key, value) - if self.ignore_me: return - label = self.__class__.__name__ - if hasattr(self, 'label'): - if self.variant is None: - label = self.label - else: - label = '{} {}'.format(self.label, self.variant) + label = getattr(self, "label", self.__class__.__name__) + variant = getattr(self, "variant", None) + if variant: + label = "{} {}".format(label, variant) + try: self._preregister() @@ -126,12 +114,10 @@ class BaseHandler(object): def launch_log(self, func): @functools.wraps(func) def wrapper_launch(*args, **kwargs): - label = self.__class__.__name__ - if hasattr(self, 'label'): - label = self.label - if hasattr(self, 'variant'): - if self.variant is not None: - label = '{} {}'.format(self.label, self.variant) + label = getattr(self, "label", self.__class__.__name__) + variant = getattr(self, "variant", None) + if variant: + label = "{} {}".format(label, variant) self.log.info(('{} "{}": Launched').format(self.type, label)) try: diff --git a/pype/modules/ftrack/lib/ftrack_event_handler.py b/pype/modules/ftrack/lib/ftrack_event_handler.py index 770b942844..53b78ccc17 100644 --- a/pype/modules/ftrack/lib/ftrack_event_handler.py +++ b/pype/modules/ftrack/lib/ftrack_event_handler.py @@ -15,10 +15,6 @@ class BaseEvent(BaseHandler): type = 'Event' - def __init__(self, session, plugins_presets={}): - '''Expects a ftrack_api.Session instance''' - super().__init__(session, plugins_presets) - # Decorator def launch_log(self, func): @functools.wraps(func) From 3635fe042364eec0486ff465e6066af22ea475be Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Dec 2020 11:24:25 +0100 Subject: [PATCH 077/106] added threading lock to Terminal to wait until is initialized --- pype/lib/terminal.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pype/lib/terminal.py b/pype/lib/terminal.py index 043869130a..ffd3c5c76a 100644 --- a/pype/lib/terminal.py +++ b/pype/lib/terminal.py @@ -11,6 +11,8 @@ # ..---===[[ PyP3 Setup ]]===---... # import re +import time +import threading class Terminal: @@ -24,6 +26,7 @@ class Terminal: # Is Terminal initialized _initialized = False + _init_lock = threading.Lock() # Use colorized output use_colors = True # Output message replacements mapping - set on initialization @@ -40,15 +43,14 @@ class Terminal: it's terminal object. Colorized output is not used if import of python module or terminal object creation fails. """ - # Mark that Terminal's initialization was already triggered - Terminal._initialized = True - from . import env_value_to_bool + from pype.lib import env_value_to_bool use_colors = env_value_to_bool( "PYPE_LOG_NO_COLORS", default=Terminal.use_colors ) if not use_colors: Terminal.use_colors = use_colors + Terminal._initialized = True return try: @@ -63,6 +65,7 @@ class Terminal: "Module `blessed` failed on import or terminal creation." " Pype terminal won't use colors." ) + Terminal._initialized = True return # shortcuts for blessed codes @@ -117,6 +120,8 @@ class Terminal: Terminal._Y = _Y Terminal._W = _W + Terminal._initialized = True + @staticmethod def _multiple_replace(text, adict): """Replace multiple tokens defined in dict. @@ -170,7 +175,12 @@ class Terminal: """ T = Terminal if not T._initialized: - T._initialize() + if T._init_lock.locked(): + while T._init_lock.locked(): + time.sleep(0.1) + else: + with T._init_lock: + T._initialize() # if we dont want colors, just print raw message if not T.use_colors: From b25a70245618a2ed3d7a27962678aac2e90640e8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Dec 2020 15:55:31 +0100 Subject: [PATCH 078/106] added few comments --- pype/lib/terminal.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pype/lib/terminal.py b/pype/lib/terminal.py index ffd3c5c76a..a47d58ec3b 100644 --- a/pype/lib/terminal.py +++ b/pype/lib/terminal.py @@ -26,6 +26,7 @@ class Terminal: # Is Terminal initialized _initialized = False + # Thread lock for initialization to avoid race conditions _init_lock = threading.Lock() # Use colorized output use_colors = True @@ -42,6 +43,8 @@ class Terminal: Then tries to import python module that do the colors magic and create it's terminal object. Colorized output is not used if import of python module or terminal object creation fails. + + Set `_initialized` attribute to `True` when is done. """ from pype.lib import env_value_to_bool @@ -174,13 +177,18 @@ class Terminal: """ T = Terminal + # Initialize if not yet initialized and use thread lock to avoid race + # condition issues if not T._initialized: - if T._init_lock.locked(): - while T._init_lock.locked(): - time.sleep(0.1) - else: + # Check if lock is already locked to be sure `_initialize` is not + # executed multiple times + if not T._init_lock.locked(): with T._init_lock: T._initialize() + else: + # If lock is locked wait until is finished + while T._init_lock.locked(): + time.sleep(0.1) # if we dont want colors, just print raw message if not T.use_colors: From f368385920a3c41a1342011b6f828830e5b968a3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Dec 2020 17:35:10 +0100 Subject: [PATCH 079/106] removed roles from registerings --- .../modules/ftrack/lib/ftrack_base_handler.py | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/pype/modules/ftrack/lib/ftrack_base_handler.py b/pype/modules/ftrack/lib/ftrack_base_handler.py index 022c4f0829..8fbb54440a 100644 --- a/pype/modules/ftrack/lib/ftrack_base_handler.py +++ b/pype/modules/ftrack/lib/ftrack_base_handler.py @@ -37,7 +37,6 @@ class BaseHandler(object): type = 'No-type' ignore_me = False preactions = [] - role_list = [] @staticmethod def join_query_keys(keys): @@ -142,28 +141,7 @@ class BaseHandler(object): def reset_session(self): self.session.reset() - def _register_role_check(self): - if not self.role_list or not isinstance(self.role_list, (list, tuple)): - return - - user_entity = self.session.query( - "User where username is \"{}\"".format(self.session.api_user) - ).one() - available = False - lowercase_rolelist = [ - role_name.lower() - for role_name in self.role_list - ] - for role in user_entity["user_security_roles"]: - if role["security_role"]["name"].lower() in lowercase_rolelist: - available = True - break - if available is False: - raise MissingPermision - def _preregister(self): - self._register_role_check() - # Custom validations result = self.preregister() if result is None: @@ -621,11 +599,19 @@ class BaseHandler(object): project_entity (ftrack_api.Entity): Project entity. Must be entered if project_id is not. """ + if not project_entity: project_entity = self.get_project_entity_from_event( session, event, project_id ) + if not project_entity: + raise AssertionError(( + "Invalid arguments entered. Project entity or project id" + "must be entered." + )) + + project_id = project_entity["id"] project_name = project_entity["full_name"] project_settings_by_id = event["data"].get("project_settings") From 1bf55c56868249d5c43edacab2fec7f222396e59 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Dec 2020 17:37:17 +0100 Subject: [PATCH 080/106] removed server action role check --- .../ftrack/lib/ftrack_action_handler.py | 72 ++----------------- 1 file changed, 7 insertions(+), 65 deletions(-) diff --git a/pype/modules/ftrack/lib/ftrack_action_handler.py b/pype/modules/ftrack/lib/ftrack_action_handler.py index e04ed6b404..6d45748b23 100644 --- a/pype/modules/ftrack/lib/ftrack_action_handler.py +++ b/pype/modules/ftrack/lib/ftrack_action_handler.py @@ -67,6 +67,9 @@ class BaseAction(BaseHandler): def _discover(self, event): entities = self._translate_event(event) + if not entities: + return + accepts = self.discover(self.session, entities, event) if not accepts: return @@ -146,21 +149,18 @@ class BaseAction(BaseHandler): def _launch(self, event): entities = self._translate_event(event) + if not entities: + return preactions_launched = self._handle_preactions(self.session, event) if preactions_launched is False: return - interface = self._interface( - self.session, entities, event - ) - + interface = self._interface(self.session, entities, event) if interface: return interface - response = self.launch( - self.session, entities, event - ) + response = self.launch(self.session, entities, event) return self._handle_result(response) @@ -204,64 +204,6 @@ class ServerAction(BaseAction): For the same reason register is modified to not filter topics by username. """ - def __init__(self, *args, **kwargs): - if not self.role_list: - self.role_list = set() - else: - self.role_list = set( - role_name.lower() - for role_name in self.role_list - ) - super(ServerAction, self).__init__(*args, **kwargs) - - def _register_role_check(self): - # Skip register role check. - return - - def _discover(self, event): - """Check user discover availability.""" - if not self._check_user_discover(event): - return - return super(ServerAction, self)._discover(event) - - def _check_user_discover(self, event): - """Should be action discovered by user trying to show actions.""" - if not self.role_list: - return True - - user_entity = self._get_user_entity(event) - if not user_entity: - return False - - for role in user_entity["user_security_roles"]: - lowered_role = role["security_role"]["name"].lower() - if lowered_role in self.role_list: - return True - return False - - def _get_user_entity(self, event): - """Query user entity from event.""" - not_set = object() - - # Check if user is already stored in event data - user_entity = event["data"].get("user_entity", not_set) - if user_entity is not_set: - # Query user entity from event - user_info = event.get("source", {}).get("user", {}) - user_id = user_info.get("id") - username = user_info.get("username") - if user_id: - user_entity = self.session.query( - "User where id is {}".format(user_id) - ).first() - if not user_entity and username: - user_entity = self.session.query( - "User where username is {}".format(username) - ).first() - event["data"]["user_entity"] = user_entity - - return user_entity - def register(self): """Register subcription to Ftrack event hub.""" self.session.event_hub.subscribe( From 5517fe09cc92ee4417baba44579bacdfe432728a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Dec 2020 17:39:27 +0100 Subject: [PATCH 081/106] implemented helper functions to get user roles from event --- .../ftrack/lib/ftrack_action_handler.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/pype/modules/ftrack/lib/ftrack_action_handler.py b/pype/modules/ftrack/lib/ftrack_action_handler.py index 6d45748b23..1dcc5ca647 100644 --- a/pype/modules/ftrack/lib/ftrack_action_handler.py +++ b/pype/modules/ftrack/lib/ftrack_action_handler.py @@ -88,6 +88,44 @@ class BaseAction(BaseHandler): }] } + @classmethod + def get_user_entity_from_event(cls, session, event): + """Query user entity from event.""" + not_set = object() + + # Check if user is already stored in event data + user_entity = event["data"].get("user_entity", not_set) + if user_entity is not_set: + # Query user entity from event + user_info = event.get("source", {}).get("user", {}) + user_id = user_info.get("id") + username = user_info.get("username") + if user_id: + user_entity = session.query( + "User where id is {}".format(user_id) + ).first() + if not user_entity and username: + user_entity = session.query( + "User where username is {}".format(username) + ).first() + event["data"]["user_entity"] = user_entity + + return user_entity + + @classmethod + def get_user_roles_from_event(cls, session, event): + """Query user entity from event.""" + not_set = object() + + user_roles = event["data"].get("user_roles", not_set) + if user_roles is not_set: + user_roles = [] + user_entity = cls.get_user_entity_from_event(session, event) + for role in user_entity["user_security_roles"]: + user_roles.append(role["security_role"]["name"].lower()) + event["data"]["user_roles"] = user_roles + return user_roles + def discover(self, session, entities, event): '''Return true if we can handle the selected entities. From 8ed00fbdef6fe9af869708a1c67f2e9c321183c1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Dec 2020 17:47:16 +0100 Subject: [PATCH 082/106] get_project_entity_from_event moved to event handler --- .../modules/ftrack/lib/ftrack_base_handler.py | 29 ------------------ .../ftrack/lib/ftrack_event_handler.py | 30 +++++++++++++++++++ 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/pype/modules/ftrack/lib/ftrack_base_handler.py b/pype/modules/ftrack/lib/ftrack_base_handler.py index 8fbb54440a..517bcfd3c8 100644 --- a/pype/modules/ftrack/lib/ftrack_base_handler.py +++ b/pype/modules/ftrack/lib/ftrack_base_handler.py @@ -553,35 +553,6 @@ class BaseHandler(object): "Project where id is {}".format(project_data["id"]) ).one() - def get_project_entity_from_event(self, session, event, project_id): - """Load or query and fill project entity from/to event data. - - Project data are stored by ftrack id because in most cases it is - easier to access project id than project name. - - Args: - session (ftrack_api.Session): Current session. - event (ftrack_api.Event): Processed event by session. - project_id (str): Ftrack project id. - """ - if not project_id: - raise ValueError( - "Entered `project_id` is not valid. {} ({})".format( - str(project_id), str(type(project_id)) - ) - ) - # Try to get project entity from event - project_entities = event["data"].get("project_entities") - if not project_entities: - project_entities = {} - event["data"]["project_entities"] = project_entities - - project_entity = project_entities.get(project_id) - if not project_entity: - # Get project entity from task and store to event - project_entity = session.get("Project", project_id) - event["data"]["project_entities"][project_id] = project_entity - return project_entity def get_settings_for_project( self, session, event, project_id=None, project_entity=None diff --git a/pype/modules/ftrack/lib/ftrack_event_handler.py b/pype/modules/ftrack/lib/ftrack_event_handler.py index 53b78ccc17..73cebc4d34 100644 --- a/pype/modules/ftrack/lib/ftrack_event_handler.py +++ b/pype/modules/ftrack/lib/ftrack_event_handler.py @@ -46,3 +46,33 @@ class BaseEvent(BaseHandler): session, ignore=['socialfeed', 'socialnotification'] ) + + def get_project_entity_from_event(self, session, event, project_id): + """Load or query and fill project entity from/to event data. + + Project data are stored by ftrack id because in most cases it is + easier to access project id than project name. + + Args: + session (ftrack_api.Session): Current session. + event (ftrack_api.Event): Processed event by session. + project_id (str): Ftrack project id. + """ + if not project_id: + raise ValueError( + "Entered `project_id` is not valid. {} ({})".format( + str(project_id), str(type(project_id)) + ) + ) + # Try to get project entity from event + project_entities = event["data"].get("project_entities") + if not project_entities: + project_entities = {} + event["data"]["project_entities"] = project_entities + + project_entity = project_entities.get(project_id) + if not project_entity: + # Get project entity from task and store to event + project_entity = session.get("Project", project_id) + event["data"]["project_entities"][project_id] = project_entity + return project_entity From ff80d5df6de3544bba83f050b4a16efc841f5562 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Dec 2020 17:47:38 +0100 Subject: [PATCH 083/106] `get_settings_for_project` expect only project_entity --- pype/modules/ftrack/lib/ftrack_base_handler.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/pype/modules/ftrack/lib/ftrack_base_handler.py b/pype/modules/ftrack/lib/ftrack_base_handler.py index 517bcfd3c8..ebcc3b3809 100644 --- a/pype/modules/ftrack/lib/ftrack_base_handler.py +++ b/pype/modules/ftrack/lib/ftrack_base_handler.py @@ -563,19 +563,9 @@ class BaseHandler(object): easier to access project id than project name. Args: - session (ftrack_api.Session): Current session. event (ftrack_api.Event): Processed event by session. - project_id (str): Ftrack project id. Must be entered if - project_entity is not. - project_entity (ftrack_api.Entity): Project entity. Must be entered - if project_id is not. + project_entity (ftrack_api.Entity): Project entity. """ - - if not project_entity: - project_entity = self.get_project_entity_from_event( - session, event, project_id - ) - if not project_entity: raise AssertionError(( "Invalid arguments entered. Project entity or project id" From 3eb4674ac977355d2161738a58476061b59c108b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Dec 2020 17:48:21 +0100 Subject: [PATCH 084/106] renamed `get_settings_for_project` to `get_project_settings_from_event` --- pype/modules/ftrack/events/event_task_to_version_status.py | 4 ++-- pype/modules/ftrack/events/event_thumbnail_updates.py | 4 ++-- pype/modules/ftrack/events/event_version_to_task_statuses.py | 4 ++-- pype/modules/ftrack/lib/ftrack_base_handler.py | 5 +---- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index 8d226424c3..fa2cb043bd 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -102,8 +102,8 @@ class TaskToVersionStatus(BaseEvent): project_entity = self.get_project_entity_from_event( session, event, project_id ) - project_settings = self.get_settings_for_project( - session, event, project_entity=project_entity + project_settings = self.get_project_settings_from_event( + event, project_entity ) project_name = project_entity["full_name"] diff --git a/pype/modules/ftrack/events/event_thumbnail_updates.py b/pype/modules/ftrack/events/event_thumbnail_updates.py index 09d992b8c4..cda43b05a9 100644 --- a/pype/modules/ftrack/events/event_thumbnail_updates.py +++ b/pype/modules/ftrack/events/event_thumbnail_updates.py @@ -22,8 +22,8 @@ class ThumbnailEvents(BaseEvent): project_entity = self.get_project_entity_from_event( session, event, project_id ) - project_settings = self.get_settings_for_project( - session, event, project_entity=project_entity + project_settings = self.get_project_settings_from_event( + event, project_entity ) project_name = project_entity["full_name"] diff --git a/pype/modules/ftrack/events/event_version_to_task_statuses.py b/pype/modules/ftrack/events/event_version_to_task_statuses.py index 03f873f2cd..6debd4aac4 100644 --- a/pype/modules/ftrack/events/event_version_to_task_statuses.py +++ b/pype/modules/ftrack/events/event_version_to_task_statuses.py @@ -51,8 +51,8 @@ class VersionToTaskStatus(BaseEvent): project_entity = self.get_project_entity_from_event( session, event, project_id ) - project_settings = self.get_settings_for_project( - session, event, project_entity=project_entity + project_settings = self.get_project_settings_from_event( + event, project_entity ) project_name = project_entity["full_name"] diff --git a/pype/modules/ftrack/lib/ftrack_base_handler.py b/pype/modules/ftrack/lib/ftrack_base_handler.py index ebcc3b3809..00a3cd8cdc 100644 --- a/pype/modules/ftrack/lib/ftrack_base_handler.py +++ b/pype/modules/ftrack/lib/ftrack_base_handler.py @@ -553,10 +553,7 @@ class BaseHandler(object): "Project where id is {}".format(project_data["id"]) ).one() - - def get_settings_for_project( - self, session, event, project_id=None, project_entity=None - ): + def get_project_settings_from_event(self, event, project_entity): """Load or fill pype's project settings from event data. Project data are stored by ftrack id because in most cases it is From 4ca70aa937e14a20ef08f775c33bda9ae83c07eb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Dec 2020 17:49:24 +0100 Subject: [PATCH 085/106] implemented `get_project_entity_from_event` for action handler --- .../ftrack/lib/ftrack_action_handler.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pype/modules/ftrack/lib/ftrack_action_handler.py b/pype/modules/ftrack/lib/ftrack_action_handler.py index 1dcc5ca647..aa6cb7c1b0 100644 --- a/pype/modules/ftrack/lib/ftrack_action_handler.py +++ b/pype/modules/ftrack/lib/ftrack_action_handler.py @@ -126,6 +126,25 @@ class BaseAction(BaseHandler): event["data"]["user_roles"] = user_roles return user_roles + def get_project_entity_from_event(self, session, event, entities): + """Load or query and fill project entity from/to event data. + + Project data are stored by ftrack id because in most cases it is + easier to access project id than project name. + + Args: + session (ftrack_api.Session): Current session. + event (ftrack_api.Event): Processed event by session. + entities (list): Ftrack entities of selection. + """ + + # Try to get project entity from event + project_entity = event["data"].get("project_entity") + if not project_entity: + project_entity = self.get_project_from_entity(entities[0]) + event["data"]["project_entity"] = project_entity + return project_entity + def discover(self, session, entities, event): '''Return true if we can handle the selected entities. From c3c415e4059f1cd39f1a99cf855cecb73ce08c83 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Dec 2020 19:09:44 +0100 Subject: [PATCH 086/106] actions are discovered by settings --- .../action_clean_hierarchical_attributes.py | 12 +- .../actions/action_create_cust_attrs.py | 5 +- .../ftrack/actions/action_delete_asset.py | 24 +-- .../actions/action_delete_old_versions.py | 18 +- .../modules/ftrack/actions/action_delivery.py | 9 +- .../ftrack/actions/action_job_killer.py | 5 +- .../ftrack/actions/action_prepare_project.py | 13 +- pype/modules/ftrack/actions/action_seed.py | 4 +- .../action_store_thumbnails_to_avalon.py | 11 +- .../ftrack/actions/action_sync_to_avalon.py | 14 +- .../actions/action_thumbnail_to_childern.py | 6 +- .../events/event_task_to_parent_status.py | 4 +- .../ftrack/lib/ftrack_action_handler.py | 167 ++++++++++++------ .../modules/ftrack/lib/ftrack_base_handler.py | 7 +- 14 files changed, 190 insertions(+), 109 deletions(-) diff --git a/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py b/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py index dc3a638192..f9824ec8ea 100644 --- a/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py +++ b/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py @@ -9,7 +9,6 @@ class CleanHierarchicalAttrsAction(BaseAction): label = "Pype Admin" variant = "- Clean hierarchical custom attributes" description = "Unset empty hierarchical attribute values." - role_list = ["Pypeclub", "Administrator", "Project Manager"] icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") all_project_entities_query = ( @@ -20,12 +19,17 @@ class CleanHierarchicalAttrsAction(BaseAction): "select value, entity_id from CustomAttributeValue " "where entity_id in ({}) and configuration_id is \"{}\"" ) + settings_key = "clean_hierarchical_attr" def discover(self, session, entities, event): """Show only on project entity.""" - if len(entities) == 1 and entities[0].entity_type.lower() == "project": - return True - return False + if ( + len(entities) != 1 + or entities[0].entity_type.lower() != "project" + ): + return False + + return self.valid_roles(session, entities, event) def launch(self, session, entities, event): project = entities[0] diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index 9d6c16b556..a6601775f1 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -131,9 +131,8 @@ class CustomAttributes(BaseAction): variant = '- Create/Update Avalon Attributes' #: Action description. description = 'Creates Avalon/Mongo ID for double check' - #: roles that are allowed to register this action - role_list = ['Pypeclub', 'Administrator'] icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") + settings_key = "create_update_attributes" required_keys = ("key", "label", "type") @@ -150,7 +149,7 @@ class CustomAttributes(BaseAction): Validation - action is only for Administrators ''' - return True + return self.valid_roles(session, entities, event) def launch(self, session, entities, event): # JOB SETTINGS diff --git a/pype/modules/ftrack/actions/action_delete_asset.py b/pype/modules/ftrack/actions/action_delete_asset.py index 4720273c81..3bdbbe2470 100644 --- a/pype/modules/ftrack/actions/action_delete_asset.py +++ b/pype/modules/ftrack/actions/action_delete_asset.py @@ -18,8 +18,8 @@ class DeleteAssetSubset(BaseAction): #: Action description. description = "Removes from Avalon with all childs and asset from Ftrack" icon = statics_icon("ftrack", "action_icons", "DeleteAsset.svg") - #: roles that are allowed to register this action - role_list = ["Pypeclub", "Administrator", "Project Manager"] + + settings_key = "delete_asset_subset" #: Db connection dbcon = AvalonMongoDB() @@ -32,17 +32,21 @@ class DeleteAssetSubset(BaseAction): """ Validation """ task_ids = [] for ent_info in event["data"]["selection"]: - entType = ent_info.get("entityType", "") - if entType == "task": + if ent_info.get("entityType") == "task": task_ids.append(ent_info["entityId"]) + is_valid = False for entity in entities: - ftrack_id = entity["id"] - if ftrack_id not in task_ids: - continue - if entity.entity_type.lower() != "task": - return True - return False + if ( + entity["id"] in task_ids + and entity.entity_type.lower() != "task" + ): + is_valid = True + break + + if is_valid: + is_valid = self.valid_roles(session, entities, event) + return is_valid def _launch(self, event): try: diff --git a/pype/modules/ftrack/actions/action_delete_old_versions.py b/pype/modules/ftrack/actions/action_delete_old_versions.py index 31d15da9e5..e1c1e173a3 100644 --- a/pype/modules/ftrack/actions/action_delete_old_versions.py +++ b/pype/modules/ftrack/actions/action_delete_old_versions.py @@ -21,7 +21,6 @@ class DeleteOldVersions(BaseAction): "Delete files from older publishes so project can be" " archived with only lates versions." ) - role_list = ["Pypeclub", "Project Manager", "Administrator"] icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") dbcon = AvalonMongoDB() @@ -31,13 +30,16 @@ class DeleteOldVersions(BaseAction): sequence_splitter = "__sequence_splitter__" def discover(self, session, entities, event): - ''' Validation ''' - selection = event["data"].get("selection") or [] - for entity in selection: - entity_type = (entity.get("entityType") or "").lower() - if entity_type == "assetversion": - return True - return False + """ Validation. """ + is_valid = False + for entity in entities: + if entity.entity_type.lower() == "assetversion": + is_valid = True + break + + if is_valid: + is_valid = self.valid_roles(session, entities, event) + return is_valid def interface(self, session, entities, event): # TODO Add roots existence validation diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index 853fe64ec7..e9e939bb47 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -23,6 +23,7 @@ class Delivery(BaseAction): description = "Deliver data to client" role_list = ["Pypeclub", "Administrator", "Project manager"] icon = statics_icon("ftrack", "action_icons", "Delivery.svg") + settings_key = "delivery_action" def __init__(self, *args, **kwargs): self.db_con = AvalonMongoDB() @@ -30,11 +31,15 @@ class Delivery(BaseAction): super(Delivery, self).__init__(*args, **kwargs) def discover(self, session, entities, event): + is_valid = False for entity in entities: if entity.entity_type.lower() == "assetversion": - return True + is_valid = True + break - return False + if is_valid: + is_valid = self.valid_roles(session, entities, event) + return is_valid def interface(self, session, entities, event): if event["data"].get("values", {}): diff --git a/pype/modules/ftrack/actions/action_job_killer.py b/pype/modules/ftrack/actions/action_job_killer.py index cb193b88ce..1ddd1383a7 100644 --- a/pype/modules/ftrack/actions/action_job_killer.py +++ b/pype/modules/ftrack/actions/action_job_killer.py @@ -13,13 +13,12 @@ class JobKiller(BaseAction): #: Action description. description = 'Killing selected running jobs' #: roles that are allowed to register this action - role_list = ['Pypeclub', 'Administrator'] icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") + settings_key = "job_killer" def discover(self, session, entities, event): ''' Validation ''' - - return True + return self.valid_roles(session, entities, event) def interface(self, session, entities, event): if not event['data'].get('values', {}): diff --git a/pype/modules/ftrack/actions/action_prepare_project.py b/pype/modules/ftrack/actions/action_prepare_project.py index 98493f65c7..3a955067d8 100644 --- a/pype/modules/ftrack/actions/action_prepare_project.py +++ b/pype/modules/ftrack/actions/action_prepare_project.py @@ -16,22 +16,23 @@ class PrepareProject(BaseAction): #: Action description. description = 'Set basic attributes on the project' #: roles that are allowed to register this action - role_list = ["Pypeclub", "Administrator", "Project manager"] icon = statics_icon("ftrack", "action_icons", "PrepareProject.svg") + settings_key = "prepare_project" + # Key to store info about trigerring create folder structure create_project_structure_key = "create_folder_structure" item_splitter = {'type': 'label', 'value': '---'} def discover(self, session, entities, event): ''' Validation ''' - if len(entities) != 1: + if ( + len(entities) != 1 + or entities[0].entity_type.lower() != "project" + ): return False - if entities[0].entity_type.lower() != "project": - return False - - return True + return self.valid_roles(session, entities, event) def interface(self, session, entities, event): if event['data'].get('values', {}): diff --git a/pype/modules/ftrack/actions/action_seed.py b/pype/modules/ftrack/actions/action_seed.py index 2610a25024..549afc660c 100644 --- a/pype/modules/ftrack/actions/action_seed.py +++ b/pype/modules/ftrack/actions/action_seed.py @@ -15,7 +15,6 @@ class SeedDebugProject(BaseAction): #: priority priority = 100 #: roles that are allowed to register this action - role_list = ["Pypeclub"] icon = statics_icon("ftrack", "action_icons", "SeedProject.svg") # Asset names which will be created in `Assets` entity @@ -58,9 +57,12 @@ class SeedDebugProject(BaseAction): existing_projects = None new_project_item = "< New Project >" current_project_item = "< Current Project >" + settings_key = "seed_project" def discover(self, session, entities, event): ''' Validation ''' + if not self.valid_roles(session, entities, event): + return False return True def interface(self, session, entities, event): diff --git a/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py b/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py index 6df8271381..84f857e37a 100644 --- a/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py +++ b/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py @@ -21,8 +21,8 @@ class StoreThumbnailsToAvalon(BaseAction): # Action description description = 'Test action' # roles that are allowed to register this action - role_list = ["Pypeclub", "Administrator", "Project Manager"] icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") + settings_key = "store_thubmnail_to_avalon" thumbnail_key = "AVALON_THUMBNAIL_ROOT" @@ -31,10 +31,15 @@ class StoreThumbnailsToAvalon(BaseAction): super(StoreThumbnailsToAvalon, self).__init__(*args, **kwargs) def discover(self, session, entities, event): + is_valid = False for entity in entities: if entity.entity_type.lower() == "assetversion": - return True - return False + is_valid = True + break + + if is_valid: + is_valid = self.valid_roles(session, entities, event) + return is_valid def launch(self, session, entities, event): user = session.query( diff --git a/pype/modules/ftrack/actions/action_sync_to_avalon.py b/pype/modules/ftrack/actions/action_sync_to_avalon.py index 6077511092..b86b469d1c 100644 --- a/pype/modules/ftrack/actions/action_sync_to_avalon.py +++ b/pype/modules/ftrack/actions/action_sync_to_avalon.py @@ -41,20 +41,26 @@ class SyncToAvalonLocal(BaseAction): #: priority priority = 200 #: roles that are allowed to register this action - role_list = ["Pypeclub"] icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") + settings_key = "sync_to_avalon_local" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.entities_factory = SyncEntitiesFactory(self.log, self.session) def discover(self, session, entities, event): - ''' Validation ''' + """ Validate selection. """ + is_valid = False for ent in event["data"]["selection"]: # Ignore entities that are not tasks or projects if ent["entityType"].lower() in ["show", "task"]: - return True - return False + is_valid = True + break + + if is_valid: + is_valid = self.valid_roles(session, entities, event) + return is_valid def launch(self, session, in_entities, event): time_start = time.time() diff --git a/pype/modules/ftrack/actions/action_thumbnail_to_childern.py b/pype/modules/ftrack/actions/action_thumbnail_to_childern.py index 604688d221..b90dfa027c 100644 --- a/pype/modules/ftrack/actions/action_thumbnail_to_childern.py +++ b/pype/modules/ftrack/actions/action_thumbnail_to_childern.py @@ -15,11 +15,9 @@ class ThumbToChildren(BaseAction): icon = statics_icon("ftrack", "action_icons", "Thumbnail.svg") def discover(self, session, entities, event): - ''' Validation ''' - - if (len(entities) != 1 or entities[0].entity_type in ['Project']): + """Show only on project.""" + if (len(entities) != 1 or entities[0].entity_type in ["Project"]): return False - return True def launch(self, session, entities, event): diff --git a/pype/modules/ftrack/events/event_task_to_parent_status.py b/pype/modules/ftrack/events/event_task_to_parent_status.py index 2bb7be1a26..30c995495e 100644 --- a/pype/modules/ftrack/events/event_task_to_parent_status.py +++ b/pype/modules/ftrack/events/event_task_to_parent_status.py @@ -61,8 +61,8 @@ class TaskStatusToParent(BaseEvent): session, event, project_id ) # Load settings - project_settings = self.get_settings_for_project( - session, event, project_entity=project_entity + project_settings = self.get_project_settings_from_event( + event, project_entity ) # Prepare loaded settings and check if can be processed diff --git a/pype/modules/ftrack/lib/ftrack_action_handler.py b/pype/modules/ftrack/lib/ftrack_action_handler.py index aa6cb7c1b0..11952cf3c0 100644 --- a/pype/modules/ftrack/lib/ftrack_action_handler.py +++ b/pype/modules/ftrack/lib/ftrack_action_handler.py @@ -29,6 +29,8 @@ class BaseAction(BaseHandler): icon = None type = 'Action' + settings_frack_subkey = "user_handlers" + def __init__(self, session): '''Expects a ftrack_api.Session instance''' if self.label is None: @@ -88,63 +90,6 @@ class BaseAction(BaseHandler): }] } - @classmethod - def get_user_entity_from_event(cls, session, event): - """Query user entity from event.""" - not_set = object() - - # Check if user is already stored in event data - user_entity = event["data"].get("user_entity", not_set) - if user_entity is not_set: - # Query user entity from event - user_info = event.get("source", {}).get("user", {}) - user_id = user_info.get("id") - username = user_info.get("username") - if user_id: - user_entity = session.query( - "User where id is {}".format(user_id) - ).first() - if not user_entity and username: - user_entity = session.query( - "User where username is {}".format(username) - ).first() - event["data"]["user_entity"] = user_entity - - return user_entity - - @classmethod - def get_user_roles_from_event(cls, session, event): - """Query user entity from event.""" - not_set = object() - - user_roles = event["data"].get("user_roles", not_set) - if user_roles is not_set: - user_roles = [] - user_entity = cls.get_user_entity_from_event(session, event) - for role in user_entity["user_security_roles"]: - user_roles.append(role["security_role"]["name"].lower()) - event["data"]["user_roles"] = user_roles - return user_roles - - def get_project_entity_from_event(self, session, event, entities): - """Load or query and fill project entity from/to event data. - - Project data are stored by ftrack id because in most cases it is - easier to access project id than project name. - - Args: - session (ftrack_api.Session): Current session. - event (ftrack_api.Event): Processed event by session. - entities (list): Ftrack entities of selection. - """ - - # Try to get project entity from event - project_entity = event["data"].get("project_entity") - if not project_entity: - project_entity = self.get_project_from_entity(entities[0]) - event["data"]["project_entity"] = project_entity - return project_entity - def discover(self, session, entities, event): '''Return true if we can handle the selected entities. @@ -253,6 +198,112 @@ class BaseAction(BaseHandler): return result + @staticmethod + def roles_check(settings_roles, user_roles, default=True): + """Compare roles from setting and user's roles. + + Args: + settings_roles(list): List of role names from settings. + user_roles(list): User's lowered role names. + default(bool): If `settings_roles` is empty list. + + Returns: + bool: `True` if user has at least one role from settings or + default if `settings_roles` is empty. + """ + if not settings_roles: + return default + + for role_name in settings_roles: + if role_name.lower() in user_roles: + return True + return False + + @classmethod + def get_user_entity_from_event(cls, session, event): + """Query user entity from event.""" + not_set = object() + + # Check if user is already stored in event data + user_entity = event["data"].get("user_entity", not_set) + if user_entity is not_set: + # Query user entity from event + user_info = event.get("source", {}).get("user", {}) + user_id = user_info.get("id") + username = user_info.get("username") + if user_id: + user_entity = session.query( + "User where id is {}".format(user_id) + ).first() + if not user_entity and username: + user_entity = session.query( + "User where username is {}".format(username) + ).first() + event["data"]["user_entity"] = user_entity + + return user_entity + + @classmethod + def get_user_roles_from_event(cls, session, event): + """Query user entity from event.""" + not_set = object() + + user_roles = event["data"].get("user_roles", not_set) + if user_roles is not_set: + user_roles = [] + user_entity = cls.get_user_entity_from_event(session, event) + for role in user_entity["user_security_roles"]: + user_roles.append(role["security_role"]["name"].lower()) + event["data"]["user_roles"] = user_roles + return user_roles + + def get_project_entity_from_event(self, session, event, entities): + """Load or query and fill project entity from/to event data. + + Project data are stored by ftrack id because in most cases it is + easier to access project id than project name. + + Args: + session (ftrack_api.Session): Current session. + event (ftrack_api.Event): Processed event by session. + entities (list): Ftrack entities of selection. + """ + + # Try to get project entity from event + project_entity = event["data"].get("project_entity") + if not project_entity: + project_entity = self.get_project_from_entity( + entities[0], session + ) + event["data"]["project_entity"] = project_entity + return project_entity + + def get_ftrack_settings(self, session, event, entities): + project_entity = self.get_project_entity_from_event( + session, event, entities + ) + project_settings = self.get_project_settings_from_event( + event, project_entity + ) + return project_settings["ftrack"] + + def valid_roles(self, session, entities, event): + """Validate user roles by settings. + + Method requires to have set `settings_key` attribute. + """ + ftrack_settings = self.get_ftrack_settings(session, event, entities) + settings = ( + ftrack_settings[self.settings_frack_subkey][self.settings_key] + ) + if not settings.get("enabled", True): + return False + + user_role_list = self.get_user_roles_from_event(session, event) + if not self.roles_check(settings.get("role_list"), user_role_list): + return False + return True + class ServerAction(BaseAction): """Action class meant to be used on event server. @@ -261,6 +312,8 @@ class ServerAction(BaseAction): For the same reason register is modified to not filter topics by username. """ + settings_frack_subkey = "events" + def register(self): """Register subcription to Ftrack event hub.""" self.session.event_hub.subscribe( diff --git a/pype/modules/ftrack/lib/ftrack_base_handler.py b/pype/modules/ftrack/lib/ftrack_base_handler.py index 00a3cd8cdc..2a8f400101 100644 --- a/pype/modules/ftrack/lib/ftrack_base_handler.py +++ b/pype/modules/ftrack/lib/ftrack_base_handler.py @@ -528,7 +528,7 @@ class BaseHandler(object): "Publishing event: {}" ).format(str(event.__dict__))) - def get_project_from_entity(self, entity): + def get_project_from_entity(self, entity, session=None): low_entity_type = entity.entity_type.lower() if low_entity_type == "project": return entity @@ -549,7 +549,10 @@ class BaseHandler(object): return parent["project"] project_data = entity["link"][0] - return self.session.query( + + if session is None: + session = self.session + return session.query( "Project where id is {}".format(project_data["id"]) ).one() From 067666bbe819c4de19a2bc7a66a145d9255d4176 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 21 Dec 2020 19:30:20 +0100 Subject: [PATCH 087/106] return missing commits from previous 2.x/develop merge --- pype/hosts/maya/expected_files.py | 70 ++++++----- pype/lib/__init__.py | 10 +- pype/lib/plugin_tools.py | 114 ++++++++++++++++++ pype/plugins/global/publish/extract_burnin.py | 29 ++++- pype/plugins/global/publish/extract_jpeg.py | 35 +++++- pype/plugins/global/publish/extract_review.py | 58 +++++++-- pype/plugins/maya/create/create_render.py | 1 + pype/plugins/maya/publish/collect_render.py | 5 +- .../publish/validate_vray_referenced_aovs.py | 90 ++++++++++++++ 9 files changed, 363 insertions(+), 49 deletions(-) create mode 100644 pype/plugins/maya/publish/validate_vray_referenced_aovs.py diff --git a/pype/hosts/maya/expected_files.py b/pype/hosts/maya/expected_files.py index 52c8893e4b..d39e5fa204 100644 --- a/pype/hosts/maya/expected_files.py +++ b/pype/hosts/maya/expected_files.py @@ -32,6 +32,9 @@ Attributes: ImagePrefixes (dict): Mapping between renderers and their respective image prefix atrribute names. +Todo: + Determine `multipart` from render instance. + """ import types @@ -94,6 +97,10 @@ class ExpectedFiles: multipart = False + def __init__(self, render_instance): + """Constructor.""" + self._render_instance = render_instance + def get(self, renderer, layer): """Get expected files for given renderer and render layer. @@ -114,15 +121,20 @@ class ExpectedFiles: renderSetup.instance().switchToLayerUsingLegacyName(layer) if renderer.lower() == "arnold": - return self._get_files(ExpectedFilesArnold(layer)) + return self._get_files(ExpectedFilesArnold(layer, + self._render_instance)) elif renderer.lower() == "vray": - return self._get_files(ExpectedFilesVray(layer)) + return self._get_files(ExpectedFilesVray( + layer, self._render_instance)) elif renderer.lower() == "redshift": - return self._get_files(ExpectedFilesRedshift(layer)) + return self._get_files(ExpectedFilesRedshift( + layer, self._render_instance)) elif renderer.lower() == "mentalray": - return self._get_files(ExpectedFilesMentalray(layer)) + return self._get_files(ExpectedFilesMentalray( + layer, self._render_instance)) elif renderer.lower() == "renderman": - return self._get_files(ExpectedFilesRenderman(layer)) + return self._get_files(ExpectedFilesRenderman( + layer, self._render_instance)) else: raise UnsupportedRendererException( "unsupported {}".format(renderer) @@ -149,9 +161,10 @@ class AExpectedFiles: layer = None multipart = False - def __init__(self, layer): + def __init__(self, layer, render_instance): """Constructor.""" self.layer = layer + self.render_instance = render_instance @abstractmethod def get_aovs(self): @@ -460,9 +473,9 @@ class ExpectedFilesArnold(AExpectedFiles): "maya": "", } - def __init__(self, layer): + def __init__(self, layer, render_instance): """Constructor.""" - super(ExpectedFilesArnold, self).__init__(layer) + super(ExpectedFilesArnold, self).__init__(layer, render_instance) self.renderer = "arnold" def get_aovs(self): @@ -531,9 +544,9 @@ class ExpectedFilesArnold(AExpectedFiles): class ExpectedFilesVray(AExpectedFiles): """Expected files for V-Ray renderer.""" - def __init__(self, layer): + def __init__(self, layer, render_instance): """Constructor.""" - super(ExpectedFilesVray, self).__init__(layer) + super(ExpectedFilesVray, self).__init__(layer, render_instance) self.renderer = "vray" def get_renderer_prefix(self): @@ -614,24 +627,25 @@ class ExpectedFilesVray(AExpectedFiles): if default_ext == "exr (multichannel)" or default_ext == "exr (deep)": default_ext = "exr" + # add beauty as default enabled_aovs.append( (u"beauty", default_ext) ) - if not self.maya_is_true( - cmds.getAttr("vraySettings.relements_enableall") - ): - return enabled_aovs + # handle aovs from references + use_ref_aovs = self.render_instance.data.get( + "vrayUseReferencedAovs", False) or False - # filter all namespace prefixed AOVs - they are pulled in from - # references and are not rendered. - vr_aovs = [ - n - for n in cmds.ls( - type=["VRayRenderElement", "VRayRenderElementSet"] - ) - if len(n.split(":")) == 1 - ] + # this will have list of all aovs no matter if they are coming from + # reference or not. + vr_aovs = cmds.ls( + type=["VRayRenderElement", "VRayRenderElementSet"]) or [] + if not use_ref_aovs: + ref_aovs = cmds.ls( + type=["VRayRenderElement", "VRayRenderElementSet"], + referencedNodes=True) or [] + # get difference + vr_aovs = list(set(vr_aovs) - set(ref_aovs)) for aov in vr_aovs: enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov))) @@ -703,9 +717,9 @@ class ExpectedFilesRedshift(AExpectedFiles): ext_mapping = ["iff", "exr", "tif", "png", "tga", "jpg"] - def __init__(self, layer): + def __init__(self, layer, render_instance): """Construtor.""" - super(ExpectedFilesRedshift, self).__init__(layer) + super(ExpectedFilesRedshift, self).__init__(layer, render_instance) self.renderer = "redshift" def get_renderer_prefix(self): @@ -822,9 +836,9 @@ class ExpectedFilesRenderman(AExpectedFiles): This is very rudimentary and needs more love and testing. """ - def __init__(self, layer): + def __init__(self, layer, render_instance): """Constructor.""" - super(ExpectedFilesRenderman, self).__init__(layer) + super(ExpectedFilesRenderman, self).__init__(layer, render_instance) self.renderer = "renderman" def get_aovs(self): @@ -887,7 +901,7 @@ class ExpectedFilesRenderman(AExpectedFiles): class ExpectedFilesMentalray(AExpectedFiles): """Skeleton unimplemented class for Mentalray renderer.""" - def __init__(self, layer): + def __init__(self, layer, render_instance): """Constructor. Raises: diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 9444ef5195..09cc998b7c 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -56,7 +56,11 @@ from .plugin_tools import ( filter_pyblish_plugins, source_hash, get_unique_layer_name, - get_background_layers + get_background_layers, + oiio_supported, + decompress, + get_decompress_dir, + should_decompress ) from .user_settings import ( @@ -108,6 +112,10 @@ __all__ = [ "source_hash", "get_unique_layer_name", "get_background_layers", + "oiio_supported", + "decompress", + "get_decompress_dir", + "should_decompress", "version_up", "get_version_from_path", diff --git a/pype/lib/plugin_tools.py b/pype/lib/plugin_tools.py index 13d311d96c..c9441dbd88 100644 --- a/pype/lib/plugin_tools.py +++ b/pype/lib/plugin_tools.py @@ -5,6 +5,8 @@ import inspect import logging import re import json +import pype.api +import tempfile from pype.settings import get_project_settings @@ -134,3 +136,115 @@ def get_background_layers(file_url): layer.get("filename")). replace("\\", "/")) return layers + + +def oiio_supported(): + """ + Checks if oiiotool is configured for this platform. + + Expects full path to executable. + + 'should_decompress' will throw exception if configured, + but not present or not working. + Returns: + (bool) + """ + oiio_path = os.getenv("PYPE_OIIO_PATH", "") + if not oiio_path or not os.path.exists(oiio_path): + log.debug("OIIOTool is not configured or not present at {}". + format(oiio_path)) + return False + + return True + + +def decompress(target_dir, file_url, + input_frame_start=None, input_frame_end=None, log=None): + """ + Decompresses DWAA 'file_url' .exr to 'target_dir'. + + Creates uncompressed files in 'target_dir', they need to be cleaned. + + File url could be for single file or for a sequence, in that case + %0Xd will be as a placeholder for frame number AND input_frame* will + be filled. + In that case single oiio command with '--frames' will be triggered for + all frames, this should be faster then looping and running sequentially + + Args: + target_dir (str): extended from stagingDir + file_url (str): full urls to source file (with or without %0Xd) + input_frame_start (int) (optional): first frame + input_frame_end (int) (optional): last frame + log (Logger) (optional): pype logger + """ + is_sequence = input_frame_start is not None and \ + input_frame_end is not None and \ + (int(input_frame_end) > int(input_frame_start)) + + oiio_cmd = [] + oiio_cmd.append(os.getenv("PYPE_OIIO_PATH")) + + oiio_cmd.append("--compression none") + + base_file_name = os.path.basename(file_url) + oiio_cmd.append(file_url) + + if is_sequence: + oiio_cmd.append("--frames {}-{}".format(input_frame_start, + input_frame_end)) + + oiio_cmd.append("-o") + oiio_cmd.append(os.path.join(target_dir, base_file_name)) + + subprocess_exr = " ".join(oiio_cmd) + + if not log: + log = logging.getLogger(__name__) + + log.debug("Decompressing {}".format(subprocess_exr)) + pype.api.subprocess( + subprocess_exr, shell=True, logger=log + ) + + +def get_decompress_dir(): + """ + Creates temporary folder for decompressing. + Its local, in case of farm it is 'local' to the farm machine. + + Should be much faster, needs to be cleaned up later. + """ + return os.path.normpath( + tempfile.mkdtemp(prefix="pyblish_tmp_") + ) + + +def should_decompress(file_url): + """ + Tests that 'file_url' is compressed with DWAA. + + Uses 'oiio_supported' to check that OIIO tool is available for this + platform. + + Shouldn't throw exception as oiiotool is guarded by check function. + Currently implemented this way as there is no support for Mac and Linux + In the future, it should be more strict and throws exception on + misconfiguration. + + Args: + file_url (str): path to rendered file (in sequence it would be + first file, if that compressed it is expected that whole seq + will be too) + Returns: + (bool): 'file_url' is DWAA compressed and should be decompressed + and we can decompress (oiiotool supported) + """ + if oiio_supported(): + output = pype.api.subprocess([ + os.getenv("PYPE_OIIO_PATH"), + "--info", "-v", file_url]) + return "compression: \"dwaa\"" in output or \ + "compression: \"dwab\"" in output + + return False diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py index 501162b6a6..d29af63483 100644 --- a/pype/plugins/global/publish/extract_burnin.py +++ b/pype/plugins/global/publish/extract_burnin.py @@ -6,6 +6,9 @@ import tempfile import pype.api import pyblish +from pype.lib import should_decompress, \ + get_decompress_dir, decompress +import shutil class ExtractBurnin(pype.api.Extractor): @@ -28,7 +31,8 @@ class ExtractBurnin(pype.api.Extractor): "premiere", "standalonepublisher", "harmony", - "fusion" + "fusion", + "aftereffects" ] optional = True @@ -204,6 +208,26 @@ class ExtractBurnin(pype.api.Extractor): # Prepare paths and files for process. self.input_output_paths(new_repre, temp_data, filename_suffix) + decompressed_dir = '' + full_input_path = temp_data["full_input_path"] + do_decompress = should_decompress(full_input_path) + if do_decompress: + decompressed_dir = get_decompress_dir() + + decompress( + decompressed_dir, + full_input_path, + temp_data["frame_start"], + temp_data["frame_end"], + self.log + ) + + # input path changed, 'decompressed' added + input_file = os.path.basename(full_input_path) + temp_data["full_input_path"] = os.path.join( + decompressed_dir, + input_file) + # Data for burnin script script_data = { "input": temp_data["full_input_path"], @@ -263,6 +287,9 @@ class ExtractBurnin(pype.api.Extractor): os.remove(filepath) self.log.debug("Removed: \"{}\"".format(filepath)) + if do_decompress and os.path.exists(decompressed_dir): + shutil.rmtree(decompressed_dir) + def prepare_basic_data(self, instance): """Pick data from instance for processing and for burnin strings. diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index 551e57796a..af90d4366d 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -3,6 +3,9 @@ import os import pyblish.api import pype.api import pype.lib +from pype.lib import should_decompress, \ + get_decompress_dir, decompress +import shutil class ExtractJpegEXR(pyblish.api.InstancePlugin): @@ -22,7 +25,8 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): if 'crypto' in instance.data['subset']: return - # ffmpeg doesn't support multipart exrs + do_decompress = False + # ffmpeg doesn't support multipart exrs, use oiiotool if available if instance.data.get("multipartExr") is True: return @@ -36,10 +40,6 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): # filter out mov and img sequences representations_new = representations[:] - if instance.data.get("multipartExr"): - # ffmpeg doesn't support multipart exrs - return - for repre in representations: tags = repre.get("tags", []) self.log.debug(repre) @@ -60,6 +60,19 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): full_input_path = os.path.join(stagingdir, input_file) self.log.info("input {}".format(full_input_path)) + decompressed_dir = '' + do_decompress = should_decompress(full_input_path) + if do_decompress: + decompressed_dir = get_decompress_dir() + + decompress( + decompressed_dir, + full_input_path) + # input path changed, 'decompressed' added + full_input_path = os.path.join( + decompressed_dir, + input_file) + filename = os.path.splitext(input_file)[0] if not filename.endswith('.'): filename += "." @@ -93,7 +106,14 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): # run subprocess self.log.debug("{}".format(subprocess_jpeg)) - pype.api.subprocess(subprocess_jpeg, shell=True) + try: # temporary until oiiotool is supported cross platform + pype.api.subprocess(subprocess_jpeg, shell=True) + except RuntimeError as exp: + if "Compression" in str(exp): + self.log.debug("Unsupported compression on input files. " + + "Skipping!!!") + return + raise if "representations" not in instance.data: instance.data["representations"] = [] @@ -111,4 +131,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): self.log.debug("Adding: {}".format(representation)) representations_new.append(representation) + if do_decompress and os.path.exists(decompressed_dir): + shutil.rmtree(decompressed_dir) + instance.data["representations"] = representations_new diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index aa8d8accb5..37fe83bf10 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -6,6 +6,8 @@ import pyblish.api import clique import pype.api import pype.lib +from pype.lib import should_decompress, \ + get_decompress_dir, decompress class ExtractReview(pyblish.api.InstancePlugin): @@ -14,7 +16,7 @@ class ExtractReview(pyblish.api.InstancePlugin): Compulsory attribute of representation is tags list with "review", otherwise the representation is ignored. - All new represetnations are created and encoded by ffmpeg following + All new representations are created and encoded by ffmpeg following presets found in `pype-config/presets/plugins/global/ publish.json:ExtractReview:outputs`. """ @@ -188,9 +190,17 @@ class ExtractReview(pyblish.api.InstancePlugin): temp_data = self.prepare_temp_data(instance, repre, output_def) - ffmpeg_args = self._ffmpeg_arguments( - output_def, instance, new_repre, temp_data - ) + try: # temporary until oiiotool is supported cross platform + ffmpeg_args = self._ffmpeg_arguments( + output_def, instance, new_repre, temp_data + ) + except ZeroDivisionError: + if 'exr' in temp_data["origin_repre"]["ext"]: + self.log.debug("Unsupported compression on input " + + "files. Skipping!!!") + return + raise + subprcs_cmd = " ".join(ffmpeg_args) # run subprocess @@ -318,9 +328,9 @@ class ExtractReview(pyblish.api.InstancePlugin): Args: output_def (dict): Currently processed output definition. instance (Instance): Currently processed instance. - new_repre (dict): Reprensetation representing output of this + new_repre (dict): Representation representing output of this process. - temp_data (dict): Base data for successfull process. + temp_data (dict): Base data for successful process. """ # Get FFmpeg arguments from profile presets @@ -331,9 +341,35 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_video_filters = out_def_ffmpeg_args.get("video_filters") or [] ffmpeg_audio_filters = out_def_ffmpeg_args.get("audio_filters") or [] + if isinstance(new_repre['files'], list): + input_files_urls = [os.path.join(new_repre["stagingDir"], f) for f + in new_repre['files']] + test_path = input_files_urls[0] + else: + test_path = os.path.join( + new_repre["stagingDir"], new_repre['files']) + do_decompress = should_decompress(test_path) + + if do_decompress: + # change stagingDir, decompress first + # calculate all paths with modified directory, used on too many + # places + # will be purged by cleanup.py automatically + orig_staging_dir = new_repre["stagingDir"] + new_repre["stagingDir"] = get_decompress_dir() + # Prepare input and output filepaths self.input_output_paths(new_repre, output_def, temp_data) + if do_decompress: + input_file = temp_data["full_input_path"].\ + replace(new_repre["stagingDir"], orig_staging_dir) + + decompress(new_repre["stagingDir"], input_file, + temp_data["frame_start"], + temp_data["frame_end"], + self.log) + # Set output frames len to 1 when ouput is single image if ( temp_data["output_ext_is_image"] @@ -930,7 +966,7 @@ class ExtractReview(pyblish.api.InstancePlugin): return regexes def validate_value_by_regexes(self, value, in_list): - """Validates in any regexe from list match entered value. + """Validates in any regex from list match entered value. Args: in_list (list): List with regexes. @@ -955,9 +991,9 @@ class ExtractReview(pyblish.api.InstancePlugin): def profile_exclusion(self, matching_profiles): """Find out most matching profile byt host, task and family match. - Profiles are selectivelly filtered. Each profile should have + Profiles are selectively filtered. Each profile should have "__value__" key with list of booleans. Each boolean represents - existence of filter for specific key (host, taks, family). + existence of filter for specific key (host, tasks, family). Profiles are looped in sequence. In each sequence are split into true_list and false_list. For next sequence loop are used profiles in true_list if there are any profiles else false_list is used. @@ -1036,7 +1072,7 @@ class ExtractReview(pyblish.api.InstancePlugin): highest_profile_points = -1 # Each profile get 1 point for each matching filter. Profile with most - # points is returnd. For cases when more than one profile will match + # points is returned. For cases when more than one profile will match # are also stored ordered lists of matching values. for profile in self.profiles: profile_points = 0 @@ -1648,7 +1684,7 @@ class ExtractReview(pyblish.api.InstancePlugin): def add_video_filter_args(self, args, inserting_arg): """ - Fixing video filter argumets to be one long string + Fixing video filter arguments to be one long string Args: args (list): list of string arguments diff --git a/pype/plugins/maya/create/create_render.py b/pype/plugins/maya/create/create_render.py index bdd237a54e..b718079b43 100644 --- a/pype/plugins/maya/create/create_render.py +++ b/pype/plugins/maya/create/create_render.py @@ -193,6 +193,7 @@ class CreateRender(avalon.maya.Creator): self.data["tilesX"] = 2 self.data["tilesY"] = 2 self.data["convertToScanline"] = False + self.data["vrayUseReferencedAovs"] = False # Disable for now as this feature is not working yet # self.data["assScene"] = False diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index 3dde3b1592..0853473120 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -149,7 +149,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # return all expected files for all cameras and aovs in given # frame range - ef = ExpectedFiles() + ef = ExpectedFiles(render_instance) exp_files = ef.get(renderer, layer_name) self.log.info("multipart: {}".format(ef.multipart)) assert exp_files, "no file names were generated, this is bug" @@ -248,7 +248,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "tilesX": render_instance.data.get("tilesX") or 2, "tilesY": render_instance.data.get("tilesY") or 2, "priority": render_instance.data.get("priority"), - "convertToScanline": render_instance.data.get("convertToScanline") or False # noqa: E501 + "convertToScanline": render_instance.data.get("convertToScanline") or False, # noqa: E501 + "vrayUseReferencedAovs": render_instance.data.get("vrayUseReferencedAovs") or False # noqa: E501 } if self.sync_workfile_version: diff --git a/pype/plugins/maya/publish/validate_vray_referenced_aovs.py b/pype/plugins/maya/publish/validate_vray_referenced_aovs.py new file mode 100644 index 0000000000..120677021d --- /dev/null +++ b/pype/plugins/maya/publish/validate_vray_referenced_aovs.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +"""Validate if there are AOVs pulled from references.""" +import pyblish.api +import types +from maya import cmds + +import pype.hosts.maya.action + + +class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin): + """Validate whether the V-Ray Render Elements (AOVs) include references. + + This will check if there are AOVs pulled from references. If + `Vray Use Referenced Aovs` is checked on render instance, u must add those + manually to Render Elements as Pype will expect them to be rendered. + + """ + + order = pyblish.api.ValidatorOrder + label = 'VRay Referenced AOVs' + hosts = ['maya'] + families = ['renderlayer'] + actions = [pype.api.RepairContextAction] + + def process(self, instance): + """Plugin main entry point.""" + if instance.data.get("renderer") != "vray": + # If not V-Ray ignore.. + return + + ref_aovs = cmds.ls( + type=["VRayRenderElement", "VRayRenderElementSet"], + referencedNodes=True) + ref_aovs_enabled = ValidateVrayReferencedAOVs.maya_is_true( + cmds.getAttr("vraySettings.relements_usereferenced")) + + if not instance.data.get("vrayUseReferencedAovs"): + if ref_aovs_enabled and ref_aovs: + self.log.warning(( + "Referenced AOVs are enabled in Vray " + "Render Settings and are detected in scene, but " + "Pype render instance option for referenced AOVs is " + "disabled. Those AOVs will be rendered but not published " + "by Pype." + )) + self.log.warning(", ".join(ref_aovs)) + else: + if not ref_aovs: + self.log.warning(( + "Use of referenced AOVs enabled but there are none " + "in the scene." + )) + if not ref_aovs_enabled: + self.log.error(( + "'Use referenced' not enabled in Vray Render Settings." + )) + raise AssertionError("Invalid render settings") + + @classmethod + def repair(cls, context): + """Repair action.""" + vray_settings = cmds.ls(type="VRaySettingsNode") + if not vray_settings: + node = cmds.createNode("VRaySettingsNode") + else: + node = vray_settings[0] + + cmds.setAttr("{}.relements_usereferenced".format(node), True) + + @staticmethod + def maya_is_true(attr_val): + """Whether a Maya attr evaluates to True. + + When querying an attribute value from an ambiguous object the + Maya API will return a list of values, which need to be properly + handled to evaluate properly. + + Args: + attr_val (mixed): Maya attribute to be evaluated as bool. + + Returns: + bool: cast Maya attribute to Pythons boolean value. + + """ + if isinstance(attr_val, types.BooleanType): + return attr_val + elif isinstance(attr_val, (types.ListType, types.GeneratorType)): + return any(attr_val) + else: + return bool(attr_val) From c99950a1c7f7b16626cf16b873947d537661ce7c Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 21 Dec 2020 21:09:16 +0100 Subject: [PATCH 088/106] plugin tools use execute instead of subprocess --- pype/lib/plugin_tools.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pype/lib/plugin_tools.py b/pype/lib/plugin_tools.py index c9441dbd88..6d074329bc 100644 --- a/pype/lib/plugin_tools.py +++ b/pype/lib/plugin_tools.py @@ -5,9 +5,10 @@ import inspect import logging import re import json -import pype.api import tempfile +from . import execute + from pype.settings import get_project_settings @@ -203,7 +204,7 @@ def decompress(target_dir, file_url, log = logging.getLogger(__name__) log.debug("Decompressing {}".format(subprocess_exr)) - pype.api.subprocess( + execute.execute( subprocess_exr, shell=True, logger=log ) @@ -241,7 +242,7 @@ def should_decompress(file_url): and we can decompress (oiiotool supported) """ if oiio_supported(): - output = pype.api.subprocess([ + output = execute.execute([ os.getenv("PYPE_OIIO_PATH"), "--info", "-v", file_url]) return "compression: \"dwaa\"" in output or \ From 3c0d97787ca9a5effa14c32966391adbd6dff0e1 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 21 Dec 2020 21:10:15 +0100 Subject: [PATCH 089/106] temporarily disable sync from integration --- pype/plugins/global/publish/integrate_new.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 26e5fff699..133b4fc6ef 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -15,7 +15,7 @@ from avalon import io from avalon.vendor import filelink import pype.api from datetime import datetime -from pype.modules import ModulesManager +# from pype.modules import ModulesManager # this is needed until speedcopy for linux is fixed if sys.platform == "win32": @@ -933,15 +933,15 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): remote_site = None sync_server_presets = None - manager = ModulesManager() - sync_server = manager.modules_by_name["sync_server"] - try: - if sync_server.enabled: - local_site, remote_site = sync_server.get_sites_for_project() - except ValueError: - log.debug(("There are not set presets for SyncServer." - " No credentials provided, no synching possible"). - format(str(sync_server_presets))) + # manager = ModulesManager() + # sync_server = manager.modules_by_name["sync_server"] + # try: + # if sync_server.enabled: + # local_site, remote_site = sync_server.get_sites_for_project() + # except ValueError: + # log.debug(("There are not set presets for SyncServer." + # " No credentials provided, no synching possible"). + # format(str(sync_server_presets))) rec = { "_id": io.ObjectId(), From 8bacfc83ab861d42323df1771a595ddb1da24f13 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 21 Dec 2020 22:39:06 +0100 Subject: [PATCH 090/106] tweak to defaults --- pype/settings/defaults/project_settings/ftrack.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pype/settings/defaults/project_settings/ftrack.json b/pype/settings/defaults/project_settings/ftrack.json index 2bf11de468..a16295f84c 100644 --- a/pype/settings/defaults/project_settings/ftrack.json +++ b/pype/settings/defaults/project_settings/ftrack.json @@ -94,13 +94,11 @@ "ignored_statuses": [ "In Progress", "Omitted", - "On hold" + "On hold", + "Approved" ], "status_change": { - "In Progress": [], - "Ready": [ - "Not Ready" - ] + "In Progress": [] } }, "create_update_attributes": { @@ -167,7 +165,8 @@ "sync_to_avalon_local": { "enabled": true, "role_list": [ - "Pypeclub" + "Pypeclub", + "Administrator" ] }, "seed_project": { From 1dab85efd880b5f7a949b517656ddcc56ce862c9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 10:21:14 +0100 Subject: [PATCH 091/106] renamed logging module to log_viewer --- pype/modules/__init__.py | 4 +- .../{logging => log_viewer}/__init__.py | 0 pype/modules/log_viewer/log_view_module.py | 48 +++++++++++++++++++ .../{logging => log_viewer}/logging_module.py | 0 .../{logging => log_viewer}/tray/__init__.py | 0 .../{logging => log_viewer}/tray/app.py | 0 .../{logging => log_viewer}/tray/models.py | 0 .../{logging => log_viewer}/tray/widgets.py | 0 8 files changed, 50 insertions(+), 2 deletions(-) rename pype/modules/{logging => log_viewer}/__init__.py (100%) create mode 100644 pype/modules/log_viewer/log_view_module.py rename pype/modules/{logging => log_viewer}/logging_module.py (100%) rename pype/modules/{logging => log_viewer}/tray/__init__.py (100%) rename pype/modules/{logging => log_viewer}/tray/app.py (100%) rename pype/modules/{logging => log_viewer}/tray/models.py (100%) rename pype/modules/{logging => log_viewer}/tray/widgets.py (100%) diff --git a/pype/modules/__init__.py b/pype/modules/__init__.py index 4f76dc2df0..46ac0b918c 100644 --- a/pype/modules/__init__.py +++ b/pype/modules/__init__.py @@ -32,7 +32,7 @@ from .ftrack import ( IFtrackEventHandlerPaths ) from .clockify import ClockifyModule -from .logging import LoggingModule +from .log_viewer import LogViewModule from .muster import MusterModule from .standalonepublish_action import StandAlonePublishAction from .websocket_server import WebsocketModule @@ -70,7 +70,7 @@ __all__ = ( "ClockifyModule", "IdleManager", - "LoggingModule", + "LogViewModule", "MusterModule", "StandAlonePublishAction", diff --git a/pype/modules/logging/__init__.py b/pype/modules/log_viewer/__init__.py similarity index 100% rename from pype/modules/logging/__init__.py rename to pype/modules/log_viewer/__init__.py diff --git a/pype/modules/log_viewer/log_view_module.py b/pype/modules/log_viewer/log_view_module.py new file mode 100644 index 0000000000..1252eaf888 --- /dev/null +++ b/pype/modules/log_viewer/log_view_module.py @@ -0,0 +1,48 @@ +from pype.api import Logger +from .. import PypeModule, ITrayModule + + +class LogViewModule(PypeModule, ITrayModule): + name = "log_viewer" + + def initialize(self, modules_settings): + logging_settings = modules_settings[self.name] + self.enabled = logging_settings["enabled"] + + # Tray attributes + self.window = None + + def tray_init(self): + try: + from .tray.app import LogsWindow + self.window = LogsWindow() + except Exception: + self.log.warning( + "Couldn't set Logging GUI due to error.", exc_info=True + ) + + # Definition of Tray menu + def tray_menu(self, tray_menu): + from Qt import QtWidgets + # Menu for Tray App + menu = QtWidgets.QMenu('Logging', tray_menu) + + show_action = QtWidgets.QAction("Show Logs", menu) + show_action.triggered.connect(self._show_logs_gui) + menu.addAction(show_action) + + tray_menu.addMenu(menu) + + def tray_start(self): + return + + def tray_exit(self): + return + + def connect_with_modules(self, _enabled_modules): + """Nothing special.""" + return + + def _show_logs_gui(self): + if self.window: + self.window.show() diff --git a/pype/modules/logging/logging_module.py b/pype/modules/log_viewer/logging_module.py similarity index 100% rename from pype/modules/logging/logging_module.py rename to pype/modules/log_viewer/logging_module.py diff --git a/pype/modules/logging/tray/__init__.py b/pype/modules/log_viewer/tray/__init__.py similarity index 100% rename from pype/modules/logging/tray/__init__.py rename to pype/modules/log_viewer/tray/__init__.py diff --git a/pype/modules/logging/tray/app.py b/pype/modules/log_viewer/tray/app.py similarity index 100% rename from pype/modules/logging/tray/app.py rename to pype/modules/log_viewer/tray/app.py diff --git a/pype/modules/logging/tray/models.py b/pype/modules/log_viewer/tray/models.py similarity index 100% rename from pype/modules/logging/tray/models.py rename to pype/modules/log_viewer/tray/models.py diff --git a/pype/modules/logging/tray/widgets.py b/pype/modules/log_viewer/tray/widgets.py similarity index 100% rename from pype/modules/logging/tray/widgets.py rename to pype/modules/log_viewer/tray/widgets.py From 49c22b6217c942fadaa3b3f1b33bac57dc2ac2f1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 10:22:54 +0100 Subject: [PATCH 092/106] removed previous module file --- pype/modules/log_viewer/logging_module.py | 48 ----------------------- 1 file changed, 48 deletions(-) delete mode 100644 pype/modules/log_viewer/logging_module.py diff --git a/pype/modules/log_viewer/logging_module.py b/pype/modules/log_viewer/logging_module.py deleted file mode 100644 index 06101b51a5..0000000000 --- a/pype/modules/log_viewer/logging_module.py +++ /dev/null @@ -1,48 +0,0 @@ -from pype.api import Logger -from .. import PypeModule, ITrayModule - - -class LoggingModule(PypeModule, ITrayModule): - name = "log_viewer" - - def initialize(self, modules_settings): - logging_settings = modules_settings[self.name] - self.enabled = logging_settings["enabled"] - - # Tray attributes - self.window = None - - def tray_init(self): - try: - from .tray.app import LogsWindow - self.window = LogsWindow() - except Exception: - self.log.warning( - "Couldn't set Logging GUI due to error.", exc_info=True - ) - - # Definition of Tray menu - def tray_menu(self, tray_menu): - from Qt import QtWidgets - # Menu for Tray App - menu = QtWidgets.QMenu('Logging', tray_menu) - - show_action = QtWidgets.QAction("Show Logs", menu) - show_action.triggered.connect(self._show_logs_gui) - menu.addAction(show_action) - - tray_menu.addMenu(menu) - - def tray_start(self): - return - - def tray_exit(self): - return - - def connect_with_modules(self, _enabled_modules): - """Nothing special.""" - return - - def _show_logs_gui(self): - if self.window: - self.window.show() From 1a190670f1a42777a43caf4a90fab6c6a974ef22 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 10:23:38 +0100 Subject: [PATCH 093/106] fix log_viewer init file --- pype/modules/log_viewer/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/modules/log_viewer/__init__.py b/pype/modules/log_viewer/__init__.py index c87d8b7f43..672f47c015 100644 --- a/pype/modules/log_viewer/__init__.py +++ b/pype/modules/log_viewer/__init__.py @@ -1,6 +1,6 @@ -from .logging_module import LoggingModule +from .log_view_module import LogViewModule __all__ = ( - "LoggingModule", + "LogViewModule", ) From 3fec8b293a0d08b479a1ab3585b129f5e64de49a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 10:47:05 +0100 Subject: [PATCH 094/106] implemented `ILaunchHookPaths` interface --- pype/modules/__init__.py | 2 ++ pype/modules/base.py | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/pype/modules/__init__.py b/pype/modules/__init__.py index 4f76dc2df0..33fd45179a 100644 --- a/pype/modules/__init__.py +++ b/pype/modules/__init__.py @@ -5,6 +5,7 @@ from .base import ( ITrayAction, ITrayService, IPluginPaths, + ILaunchHookPaths, ModulesManager, TrayModulesManager ) @@ -45,6 +46,7 @@ __all__ = ( "ITrayAction", "ITrayService", "IPluginPaths", + "ILaunchHookPaths", "ModulesManager", "TrayModulesManager", diff --git a/pype/modules/base.py b/pype/modules/base.py index 525320f1a7..97e5f891af 100644 --- a/pype/modules/base.py +++ b/pype/modules/base.py @@ -84,6 +84,19 @@ class IPluginPaths: pass +@six.add_metaclass(ABCMeta) +class ILaunchHookPaths: + """Module has launch hook paths to return. + + Expected result is list of paths. + ["path/to/launch_hooks_dir"] + """ + + @abstractmethod + def get_launch_hook_paths(self): + pass + + @six.add_metaclass(ABCMeta) class ITrayModule: """Module has special procedures when used in Pype Tray. @@ -421,6 +434,40 @@ class ModulesManager: ).format(expected_keys, " | ".join(msg_items))) return output + def collect_launch_hook_paths(self): + """Helper to collect hooks from modules inherited ILaunchHookPaths. + + Returns: + list: Paths to launch hook directories. + """ + str_type = type("") + expected_types = (list, tuple, set) + + output = [] + for module in self.get_enabled_modules(): + # Skip module that do not inherit from `ILaunchHookPaths` + if not isinstance(module, ILaunchHookPaths): + continue + + hook_paths = module.get_launch_hook_paths() + if not hook_paths: + continue + + # Convert string to list + if isinstance(hook_paths, str_type): + hook_paths = [hook_paths] + + # Skip invalid types + if not isinstance(hook_paths, expected_types): + self.log.warning(( + "Result of `get_launch_hook_paths`" + " has invalid type {}. Expected {}" + ).format(type(hook_paths), expected_types)) + continue + + output.extend(hook_paths) + return output + class TrayModulesManager(ModulesManager): # Define order of modules in menu From 1ef7dfbd33a3ace6513b5ab98ddd64a34aa8bcac Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 10:48:13 +0100 Subject: [PATCH 095/106] Ftrack module ingerit `ILaunchHookPaths` and has implemented `get_launch_hook_paths` --- pype/modules/ftrack/ftrack_module.py | 20 +++++++++++++++++-- .../launch_hooks}/post_ftrack_changes.py | 0 2 files changed, 18 insertions(+), 2 deletions(-) rename pype/{hooks/global => modules/ftrack/launch_hooks}/post_ftrack_changes.py (100%) diff --git a/pype/modules/ftrack/ftrack_module.py b/pype/modules/ftrack/ftrack_module.py index 44607681ec..d2de27e1b9 100644 --- a/pype/modules/ftrack/ftrack_module.py +++ b/pype/modules/ftrack/ftrack_module.py @@ -3,9 +3,16 @@ from abc import ABCMeta, abstractmethod import six import pype from pype.modules import ( - PypeModule, ITrayModule, IPluginPaths, ITimersManager, IUserModule + PypeModule, + ITrayModule, + IPluginPaths, + ITimersManager, + IUserModule, + ILaunchHookPaths ) +FTRACK_MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) + @six.add_metaclass(ABCMeta) class IFtrackEventHandlerPaths: @@ -19,7 +26,12 @@ class IFtrackEventHandlerPaths: class FtrackModule( - PypeModule, ITrayModule, IPluginPaths, ITimersManager, IUserModule + PypeModule, + ITrayModule, + IPluginPaths, + ITimersManager, + IUserModule, + ILaunchHookPaths ): name = "ftrack" @@ -54,6 +66,10 @@ class FtrackModule( "publish": [os.path.join(pype.PLUGINS_DIR, "ftrack", "publish")] } + def get_launch_hook_paths(self): + """Implementation of `ILaunchHookPaths`.""" + return os.path.join(FTRACK_MODULE_DIR, "launch_hooks") + def connect_with_modules(self, enabled_modules): for module in enabled_modules: if not isinstance(module, IFtrackEventHandlerPaths): diff --git a/pype/hooks/global/post_ftrack_changes.py b/pype/modules/ftrack/launch_hooks/post_ftrack_changes.py similarity index 100% rename from pype/hooks/global/post_ftrack_changes.py rename to pype/modules/ftrack/launch_hooks/post_ftrack_changes.py From 2cdaf321fb828c64fdc237031963af5b6c2a9173 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 10:48:59 +0100 Subject: [PATCH 096/106] application launch context load modules to collect prelaunch hooks --- pype/lib/applications.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index cccc50d397..f8876179ed 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -610,6 +610,13 @@ class ApplicationLaunchContext: and path not in paths ): paths.append(path) + + # Load modules paths + from pype.modules import ModulesManager + + manager = ModulesManager() + paths.extend(manager.collect_launch_hook_paths()) + return paths def discover_launch_hooks(self, force=False): From 526f15a8528bffe0a8e151e590b53cd68cf71695 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 10:51:30 +0100 Subject: [PATCH 097/106] removed TODO --- pype/lib/applications.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index f8876179ed..c7d6464418 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -580,7 +580,6 @@ class ApplicationLaunchContext: paths = [] # TODO load additional studio paths from settings - # TODO add paths based on used modules (like `ftrack`) import pype pype_dir = os.path.dirname(os.path.abspath(pype.__file__)) From 6c0cf5067ae150e72689efc290202bea2e680f53 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 13:13:42 +0100 Subject: [PATCH 098/106] `get_project_settings_from_event` expect only project name --- pype/modules/ftrack/lib/ftrack_base_handler.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/pype/modules/ftrack/lib/ftrack_base_handler.py b/pype/modules/ftrack/lib/ftrack_base_handler.py index 2a8f400101..74c31d1c6f 100644 --- a/pype/modules/ftrack/lib/ftrack_base_handler.py +++ b/pype/modules/ftrack/lib/ftrack_base_handler.py @@ -556,7 +556,7 @@ class BaseHandler(object): "Project where id is {}".format(project_data["id"]) ).one() - def get_project_settings_from_event(self, event, project_entity): + def get_project_settings_from_event(self, event, project_name): """Load or fill pype's project settings from event data. Project data are stored by ftrack id because in most cases it is @@ -566,24 +566,15 @@ class BaseHandler(object): event (ftrack_api.Event): Processed event by session. project_entity (ftrack_api.Entity): Project entity. """ - if not project_entity: - raise AssertionError(( - "Invalid arguments entered. Project entity or project id" - "must be entered." - )) - - project_id = project_entity["id"] - project_name = project_entity["full_name"] - project_settings_by_id = event["data"].get("project_settings") if not project_settings_by_id: project_settings_by_id = {} event["data"]["project_settings"] = project_settings_by_id - project_settings = project_settings_by_id.get(project_id) + project_settings = project_settings_by_id.get(project_name) if not project_settings: project_settings = get_project_settings(project_name) - event["data"]["project_settings"][project_id] = project_settings + event["data"]["project_settings"][project_name] = project_settings return project_settings @staticmethod From 86419f80c65f855bd4ea01c97651990cf3173634 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 13:14:17 +0100 Subject: [PATCH 099/106] get_project_entity_from_event store only project name not whole entity --- pype/modules/ftrack/lib/ftrack_event_handler.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pype/modules/ftrack/lib/ftrack_event_handler.py b/pype/modules/ftrack/lib/ftrack_event_handler.py index 73cebc4d34..9c43053517 100644 --- a/pype/modules/ftrack/lib/ftrack_event_handler.py +++ b/pype/modules/ftrack/lib/ftrack_event_handler.py @@ -65,14 +65,15 @@ class BaseEvent(BaseHandler): ) ) # Try to get project entity from event - project_entities = event["data"].get("project_entities") - if not project_entities: - project_entities = {} - event["data"]["project_entities"] = project_entities + project_data = event["data"].get("project_data") + if not project_data: + project_data = {} + event["data"]["project_data"] = project_data - project_entity = project_entities.get(project_id) - if not project_entity: + project_name = project_data.get(project_id) + if not project_name: # Get project entity from task and store to event project_entity = session.get("Project", project_id) - event["data"]["project_entities"][project_id] = project_entity - return project_entity + project_name = project_entity["full_name"] + event["data"]["project_data"][project_id] = project_name + return project_name From d4ae3415f0b943906d8fd1ceb8c4f8b8f601bd43 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 13:14:32 +0100 Subject: [PATCH 100/106] renamed `get_project_entity_from_event` to `get_project_name_from_event` --- pype/modules/ftrack/lib/ftrack_event_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/ftrack/lib/ftrack_event_handler.py b/pype/modules/ftrack/lib/ftrack_event_handler.py index 9c43053517..af565c5421 100644 --- a/pype/modules/ftrack/lib/ftrack_event_handler.py +++ b/pype/modules/ftrack/lib/ftrack_event_handler.py @@ -47,7 +47,7 @@ class BaseEvent(BaseHandler): ignore=['socialfeed', 'socialnotification'] ) - def get_project_entity_from_event(self, session, event, project_id): + def get_project_name_from_event(self, session, event, project_id): """Load or query and fill project entity from/to event data. Project data are stored by ftrack id because in most cases it is From 1d11730cd2e48690a88c5de9855e0cabc6b0ba14 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 13:14:51 +0100 Subject: [PATCH 101/106] action handler store only project name not whole entity --- pype/modules/ftrack/lib/ftrack_action_handler.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pype/modules/ftrack/lib/ftrack_action_handler.py b/pype/modules/ftrack/lib/ftrack_action_handler.py index 11952cf3c0..862a7a5c21 100644 --- a/pype/modules/ftrack/lib/ftrack_action_handler.py +++ b/pype/modules/ftrack/lib/ftrack_action_handler.py @@ -257,7 +257,7 @@ class BaseAction(BaseHandler): event["data"]["user_roles"] = user_roles return user_roles - def get_project_entity_from_event(self, session, event, entities): + def get_project_name_from_event(self, session, event, entities): """Load or query and fill project entity from/to event data. Project data are stored by ftrack id because in most cases it is @@ -270,20 +270,22 @@ class BaseAction(BaseHandler): """ # Try to get project entity from event - project_entity = event["data"].get("project_entity") - if not project_entity: + project_name = event["data"].get("project_name") + if not project_name: project_entity = self.get_project_from_entity( entities[0], session ) - event["data"]["project_entity"] = project_entity - return project_entity + project_name = project_entity["full_name"] + + event["data"]["project_name"] = project_name + return project_name def get_ftrack_settings(self, session, event, entities): - project_entity = self.get_project_entity_from_event( + project_name = self.get_project_data_from_event( session, event, entities ) project_settings = self.get_project_settings_from_event( - event, project_entity + event, project_name ) return project_settings["ftrack"] From 328de484004941aa487396581be811cae97e8bfc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 13:17:43 +0100 Subject: [PATCH 102/106] already implemented events use new methods instead of old ones --- .../ftrack/events/event_task_to_parent_status.py | 8 ++++---- .../ftrack/events/event_task_to_version_status.py | 7 ++++--- .../modules/ftrack/events/event_thumbnail_updates.py | 6 +++--- .../ftrack/events/event_version_to_task_statuses.py | 12 +++++++----- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/pype/modules/ftrack/events/event_task_to_parent_status.py b/pype/modules/ftrack/events/event_task_to_parent_status.py index 30c995495e..9b1f61911e 100644 --- a/pype/modules/ftrack/events/event_task_to_parent_status.py +++ b/pype/modules/ftrack/events/event_task_to_parent_status.py @@ -56,17 +56,16 @@ class TaskStatusToParent(BaseEvent): return filtered_entity_info def process_by_project(self, session, event, project_id, entities_info): - # Get project entity - project_entity = self.get_project_entity_from_event( + # Get project name + project_name = self.get_project_name_from_event( session, event, project_id ) # Load settings project_settings = self.get_project_settings_from_event( - event, project_entity + event, project_name ) # Prepare loaded settings and check if can be processed - project_name = project_entity["full_name"] result = self.prepare_settings(project_settings, project_name) if not result: return @@ -133,6 +132,7 @@ class TaskStatusToParent(BaseEvent): obj_id = object_type["id"] object_type_name_by_id[obj_id] = types_mapping[mapping_name] + project_entity = session.get("Project", project_id) project_schema = project_entity["project_schema"] available_statuses_by_obj_id = {} for obj_id in obj_ids: diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py index fa2cb043bd..d27a7f9e98 100644 --- a/pype/modules/ftrack/events/event_task_to_version_status.py +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -99,14 +99,14 @@ class TaskToVersionStatus(BaseEvent): if not entities_info: return - project_entity = self.get_project_entity_from_event( + project_name = self.get_project_name_from_event( session, event, project_id ) + # Load settings project_settings = self.get_project_settings_from_event( - event, project_entity + event, project_name ) - project_name = project_entity["full_name"] event_settings = ( project_settings["ftrack"]["events"][self.settings_key] ) @@ -171,6 +171,7 @@ class TaskToVersionStatus(BaseEvent): } # Final process of changing statuses + project_entity = session.get("Project", project_id) av_statuses_by_low_name, av_statuses_by_id = ( self.get_asset_version_statuses(project_entity) ) diff --git a/pype/modules/ftrack/events/event_thumbnail_updates.py b/pype/modules/ftrack/events/event_thumbnail_updates.py index cda43b05a9..b71322c894 100644 --- a/pype/modules/ftrack/events/event_thumbnail_updates.py +++ b/pype/modules/ftrack/events/event_thumbnail_updates.py @@ -19,14 +19,14 @@ class ThumbnailEvents(BaseEvent): def process_project_entities( self, session, event, project_id, entities_info ): - project_entity = self.get_project_entity_from_event( + project_name = self.get_project_name_from_event( session, event, project_id ) + # Load settings project_settings = self.get_project_settings_from_event( - event, project_entity + event, project_name ) - project_name = project_entity["full_name"] event_settings = ( project_settings ["ftrack"] diff --git a/pype/modules/ftrack/events/event_version_to_task_statuses.py b/pype/modules/ftrack/events/event_version_to_task_statuses.py index 6debd4aac4..6a15a697e3 100644 --- a/pype/modules/ftrack/events/event_version_to_task_statuses.py +++ b/pype/modules/ftrack/events/event_version_to_task_statuses.py @@ -48,14 +48,15 @@ class VersionToTaskStatus(BaseEvent): def process_by_project(self, session, event, project_id, entities_info): # Check for project data if event is enabled for event handler status_mapping = None - project_entity = self.get_project_entity_from_event( + + project_name = self.get_project_name_from_event( session, event, project_id ) + # Load settings project_settings = self.get_project_settings_from_event( - event, project_entity + event, project_name ) - project_name = project_entity["full_name"] # Load status mapping from presets event_settings = ( project_settings["ftrack"]["events"]["status_version_to_task"] @@ -147,7 +148,7 @@ class VersionToTaskStatus(BaseEvent): # Qeury statuses statusese_by_obj_id = self.statuses_for_tasks( - session, task_entities, project_entity + session, task_entities, project_id ) # Prepare status names by their ids status_name_by_id = { @@ -224,11 +225,12 @@ class VersionToTaskStatus(BaseEvent): exc_info=True ) - def statuses_for_tasks(self, session, task_entities, project_entity): + def statuses_for_tasks(self, session, task_entities, project_id): task_type_ids = set() for task_entity in task_entities: task_type_ids.add(task_entity["type_id"]) + project_entity = session.get("Project", project_id) project_schema = project_entity["project_schema"] output = {} for task_type_id in task_type_ids: From 9311c99f814ddd3b90d40caac49fd82a352ec250 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 13:19:49 +0100 Subject: [PATCH 103/106] fixed method name --- pype/modules/ftrack/lib/ftrack_action_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/ftrack/lib/ftrack_action_handler.py b/pype/modules/ftrack/lib/ftrack_action_handler.py index 862a7a5c21..f42469c675 100644 --- a/pype/modules/ftrack/lib/ftrack_action_handler.py +++ b/pype/modules/ftrack/lib/ftrack_action_handler.py @@ -281,7 +281,7 @@ class BaseAction(BaseHandler): return project_name def get_ftrack_settings(self, session, event, entities): - project_name = self.get_project_data_from_event( + project_name = self.get_project_name_from_event( session, event, entities ) project_settings = self.get_project_settings_from_event( From a4de80a61eccb84e437b3ee3e792221a491f27db Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 15:41:31 +0100 Subject: [PATCH 104/106] call `.copy()` on variable env --- pype/lib/applications.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index c7d6464418..ce49eed961 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -531,15 +531,13 @@ class ApplicationLaunchContext: self.launch_args = executable.as_args() # Handle launch environemtns - passed_env = self.data.pop("env", None) - if passed_env is None: + env = self.data.pop("env", None) + if env is None: env = os.environ - else: - env = passed_env # subprocess.Popen keyword arguments self.kwargs = { - "env": copy.deepcopy(env) + "env": env.copy() } if platform.system().lower() == "windows": From c1d0870a7e471bccabc5342f8edd5268db192d91 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 15:42:35 +0100 Subject: [PATCH 105/106] removed unused import --- pype/lib/applications.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index ce49eed961..94aed984fb 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -1,5 +1,4 @@ import os -import copy import platform import inspect import subprocess From 0b31fe75262153a67f33b0af4506a15d4b6ccb03 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Dec 2020 15:48:43 +0100 Subject: [PATCH 106/106] make sure it's totally independent --- pype/lib/applications.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index 94aed984fb..253ffa0ad2 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -531,12 +531,22 @@ class ApplicationLaunchContext: # Handle launch environemtns env = self.data.pop("env", None) + if env is not None and not isinstance(env, dict): + self.log.warning(( + "Passed `env` kwarg has invalid type: {}. Expected: `dict`." + " Using `os.environ` instead." + ).format(str(type(env)))) + env = None + if env is None: env = os.environ # subprocess.Popen keyword arguments self.kwargs = { - "env": env.copy() + "env": { + key: str(value) + for key, value in env.items() + } } if platform.system().lower() == "windows":