From 8db9d6725b75ecb65105d13180828f1d739c8543 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 14 Dec 2018 13:07:29 +0100 Subject: [PATCH 01/14] initial commit --- pype/plugins/nuke/create/create_read.py | 147 ++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 pype/plugins/nuke/create/create_read.py diff --git a/pype/plugins/nuke/create/create_read.py b/pype/plugins/nuke/create/create_read.py new file mode 100644 index 0000000000..b2f13f84f5 --- /dev/null +++ b/pype/plugins/nuke/create/create_read.py @@ -0,0 +1,147 @@ +from collections import OrderedDict +import avalon.api +import avalon.nuke +from pype.nuke import create_write_node +from pype import api as pype + +import nuke + + +log = pype.Logger.getLogger(__name__, "nuke") + + +def subset_to_families(subset, family, families): + subset_sufx = str(subset).replace(family, "") + new_subset = families + subset_sufx + return "{}.{}".format(family, new_subset) + + +class CrateRead(avalon.nuke.Creator): + # change this to template preset + preset = "render" + + name = "WriteRender" + label = "Create Write Render" + hosts = ["nuke"] + family = "{}_write".format(preset) + families = preset + icon = "sign-out" + + def __init__(self, *args, **kwargs): + super(CrateWriteRender, self).__init__(*args, **kwargs) + + data = OrderedDict() + + data["family"] = self.family.split("_")[1] + data["families"] = self.families + + {data.update({k: v}) for k, v in self.data.items() + if k not in data.keys()} + self.data = data + + def process(self): + self.name = self.data["subset"] + + family = self.family.split("_")[0] + node = self.family.split("_")[1] + + instance = nuke.toNode(self.data["subset"]) + + if not instance: + write_data = { + "class": node, + "preset": family, + "avalon": self.data + } + + create_write_node(self.data["subset"], write_data) + + return + + +class CrateWritePrerender(avalon.nuke.Creator): + # change this to template preset + preset = "prerender" + + name = "WritePrerender" + label = "Create Write Prerender" + hosts = ["nuke"] + family = "{}_write".format(preset) + families = preset + icon = "sign-out" + + def __init__(self, *args, **kwargs): + super(CrateWritePrerender, self).__init__(*args, **kwargs) + + data = OrderedDict() + + data["family"] = self.family.split("_")[1] + data["families"] = self.families + + {data.update({k: v}) for k, v in self.data.items() + if k not in data.keys()} + self.data = data + + def process(self): + self.name = self.data["subset"] + + instance = nuke.toNode(self.data["subset"]) + + family = self.family.split("_")[0] + node = self.family.split("_")[1] + + if not instance: + write_data = { + "class": node, + "preset": family, + "avalon": self.data + } + + create_write_node(self.data["subset"], write_data) + + return + + +class CrateWriteStill(avalon.nuke.Creator): + # change this to template preset + preset = "still" + + name = "WriteStill" + label = "Create Write Still" + hosts = ["nuke"] + family = "{}_write".format(preset) + families = preset + icon = "image" + + def __init__(self, *args, **kwargs): + super(CrateWriteStill, self).__init__(*args, **kwargs) + + data = OrderedDict() + + data["family"] = self.family.split("_")[1] + data["families"] = self.families + + {data.update({k: v}) for k, v in self.data.items() + if k not in data.keys()} + self.data = data + + def process(self): + self.name = self.data["subset"] + + instance = nuke.toNode(self.data["subset"]) + + family = self.family.split("_")[0] + node = self.family.split("_")[1] + + if not instance: + write_data = { + "frame_range": [nuke.frame(), nuke.frame()], + "class": node, + "preset": family, + "avalon": self.data + } + + nuke.createNode("FrameHold", "first_frame {}".format(nuke.frame())) + create_write_node(self.data["subset"], write_data) + + return From 2548cfb0d68cf3ac2b245f83ff1791361415a159 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 18 Dec 2018 17:48:22 +0100 Subject: [PATCH 02/14] create read works. Basic collector --- pype/plugins/nuke/create/create_read.py | 137 ++++---------------- pype/plugins/nuke/publish/collect_reades.py | 105 +++++++++++++++ 2 files changed, 128 insertions(+), 114 deletions(-) create mode 100644 pype/plugins/nuke/publish/collect_reades.py diff --git a/pype/plugins/nuke/create/create_read.py b/pype/plugins/nuke/create/create_read.py index b2f13f84f5..6465315d37 100644 --- a/pype/plugins/nuke/create/create_read.py +++ b/pype/plugins/nuke/create/create_read.py @@ -10,30 +10,20 @@ import nuke log = pype.Logger.getLogger(__name__, "nuke") -def subset_to_families(subset, family, families): - subset_sufx = str(subset).replace(family, "") - new_subset = families + subset_sufx - return "{}.{}".format(family, new_subset) - - class CrateRead(avalon.nuke.Creator): # change this to template preset - preset = "render" - - name = "WriteRender" - label = "Create Write Render" + name = "ReadCopy" + label = "Create Read Copy" hosts = ["nuke"] - family = "{}_write".format(preset) - families = preset + # family = "read" + family = "source" icon = "sign-out" def __init__(self, *args, **kwargs): - super(CrateWriteRender, self).__init__(*args, **kwargs) + super(CrateRead, self).__init__(*args, **kwargs) data = OrderedDict() - - data["family"] = self.family.split("_")[1] - data["families"] = self.families + data['family'] = self.family {data.update({k: v}) for k, v in self.data.items() if k not in data.keys()} @@ -42,106 +32,25 @@ class CrateRead(avalon.nuke.Creator): def process(self): self.name = self.data["subset"] - family = self.family.split("_")[0] - node = self.family.split("_")[1] + nodes = nuke.selectedNodes() - instance = nuke.toNode(self.data["subset"]) - - if not instance: - write_data = { - "class": node, - "preset": family, - "avalon": self.data - } - - create_write_node(self.data["subset"], write_data) + if not nodes: + nuke.message('Please select Read node') + elif len(nodes) == 1: + if nodes[0].Class() != 'Read': + nuke.message('Please select Read node') + else: + node = nodes[0] + name = node["name"].value() + avalon_data = self.data + avalon_data['subset'] = "{}_{}".format(self.family, name) + change_read_node(self.data["subset"], node, avalon_data) + else: + nuke.message('Please select only one Read node') return -class CrateWritePrerender(avalon.nuke.Creator): - # change this to template preset - preset = "prerender" - - name = "WritePrerender" - label = "Create Write Prerender" - hosts = ["nuke"] - family = "{}_write".format(preset) - families = preset - icon = "sign-out" - - def __init__(self, *args, **kwargs): - super(CrateWritePrerender, self).__init__(*args, **kwargs) - - data = OrderedDict() - - data["family"] = self.family.split("_")[1] - data["families"] = self.families - - {data.update({k: v}) for k, v in self.data.items() - if k not in data.keys()} - self.data = data - - def process(self): - self.name = self.data["subset"] - - instance = nuke.toNode(self.data["subset"]) - - family = self.family.split("_")[0] - node = self.family.split("_")[1] - - if not instance: - write_data = { - "class": node, - "preset": family, - "avalon": self.data - } - - create_write_node(self.data["subset"], write_data) - - return - - -class CrateWriteStill(avalon.nuke.Creator): - # change this to template preset - preset = "still" - - name = "WriteStill" - label = "Create Write Still" - hosts = ["nuke"] - family = "{}_write".format(preset) - families = preset - icon = "image" - - def __init__(self, *args, **kwargs): - super(CrateWriteStill, self).__init__(*args, **kwargs) - - data = OrderedDict() - - data["family"] = self.family.split("_")[1] - data["families"] = self.families - - {data.update({k: v}) for k, v in self.data.items() - if k not in data.keys()} - self.data = data - - def process(self): - self.name = self.data["subset"] - - instance = nuke.toNode(self.data["subset"]) - - family = self.family.split("_")[0] - node = self.family.split("_")[1] - - if not instance: - write_data = { - "frame_range": [nuke.frame(), nuke.frame()], - "class": node, - "preset": family, - "avalon": self.data - } - - nuke.createNode("FrameHold", "first_frame {}".format(nuke.frame())) - create_write_node(self.data["subset"], write_data) - - return +def change_read_node(name, node, data): + node = avalon.nuke.lib.imprint(node, data) + node['tile_color'].setValue(16711935) diff --git a/pype/plugins/nuke/publish/collect_reades.py b/pype/plugins/nuke/publish/collect_reades.py new file mode 100644 index 0000000000..110556e61e --- /dev/null +++ b/pype/plugins/nuke/publish/collect_reades.py @@ -0,0 +1,105 @@ +import os +import re + +import nuke +import pyblish.api +import logging +from avalon import io, api + +log = logging.getLogger(__name__) + + +@pyblish.api.log +class CollectNukeWrites(pyblish.api.ContextPlugin): + """Collect all write nodes.""" + + order = pyblish.api.CollectorOrder + 0.1 + label = "Collect Writes" + hosts = ["nuke", "nukeassist"] + + def process(self, context): + asset_data = io.find_one({"type": "asset", + "name": api.Session["AVALON_ASSET"]}) + self.log.debug("asset_data: {}".format(asset_data["data"])) + for instance in context.data["instances"]: + self.log.debug("checking instance: {}".format(instance)) + + node = instance[0] + if node.Class() != "Read": + continue + + file_path = node["file"].value() + items = file_path.split(".") + + isSequence = False + if len(items) > 1: + sequence = items[-2] + print sequence + hash_regex = re.compile(r"([#*])") + seq_regex = re.compile('[%0-9*d]') + hash_match = re.match(hash_regex, sequence) + seq_match = re.match(seq_regex, sequence) + if hash_match is True or seq_match is True: + isSequence = True + + # Get frame range + first_frame = int(nuke.root()["first_frame"].getValue()) + last_frame = int(nuke.root()["last_frame"].getValue()) + + if node["use_limit"].getValue(): + first_frame = int(node["first"].getValue()) + last_frame = int(node["last"].getValue()) + + # get source path + source_path = nuke.filename(node) + source_dir = os.path.dirname(source_path) + self.log.debug('source dir: {}'.format(source_dir)) + # Include start and end render frame in label + name = node.name() + + label = "{0} ({1}-{2})".format( + name, + int(first_frame), + int(last_frame) + ) + + # preredered frames + if not node["render"].value(): + families = "prerendered.frames" + collected_frames = os.listdir(output_dir) + self.log.debug("collected_frames: {}".format(label)) + if "files" not in instance.data: + instance.data["files"] = list() + instance.data["files"].append(collected_frames) + instance.data['transfer'] = False + else: + # dealing with local/farm rendering + if node["render_farm"].value(): + families = "{}.farm".format(instance.data["avalonKnob"]["families"][0]) + else: + families = "{}.local".format(instance.data["avalonKnob"]["families"][0]) + + self.log.debug("checking for error: {}".format(label)) + instance.data.update({ + "path": path, + "outputDir": output_dir, + "ext": ext, + "label": label, + "families": [families, 'ftrack'], + "startFrame": first_frame, + "endFrame": last_frame, + "outputType": output_type, + "stagingDir": output_dir, + "colorspace": node["colorspace"].value(), + "handles": int(asset_data["data"].get("handles", 0)), + "step": 1, + "fps": int(nuke.root()['fps'].value()) + }) + + self.log.debug("instance.data: {}".format(instance.data)) + + self.log.debug("context: {}".format(context)) + + def sort_by_family(self, instance): + """Sort by family""" + return instance.data.get("families", instance.data.get("family")) From 228533fcbe4df00e68f7c0535ce753c8a0ec7bb4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 2 Jan 2019 11:07:16 +0100 Subject: [PATCH 03/14] Read collecting --- pype/plugins/nuke/create/create_read.py | 10 ++- .../plugins/nuke/publish/collect_instances.py | 4 +- .../{collect_reades.py => collect_reads.py} | 79 +++++++++---------- .../nuke/publish/integrate_rendered_frames.py | 2 +- 4 files changed, 45 insertions(+), 50 deletions(-) rename pype/plugins/nuke/publish/{collect_reades.py => collect_reads.py} (51%) diff --git a/pype/plugins/nuke/create/create_read.py b/pype/plugins/nuke/create/create_read.py index 6465315d37..495cdde0ad 100644 --- a/pype/plugins/nuke/create/create_read.py +++ b/pype/plugins/nuke/create/create_read.py @@ -1,7 +1,6 @@ from collections import OrderedDict import avalon.api import avalon.nuke -from pype.nuke import create_write_node from pype import api as pype import nuke @@ -34,20 +33,23 @@ class CrateRead(avalon.nuke.Creator): nodes = nuke.selectedNodes() - if not nodes: + if not nodes or len(nodes) == 0: nuke.message('Please select Read node') elif len(nodes) == 1: if nodes[0].Class() != 'Read': nuke.message('Please select Read node') else: - node = nodes[0] name = node["name"].value() avalon_data = self.data avalon_data['subset'] = "{}_{}".format(self.family, name) change_read_node(self.data["subset"], node, avalon_data) else: - nuke.message('Please select only one Read node') + for node in nodes: + name = node["name"].value() + avalon_data = self.data + avalon_data['subset'] = "{}_{}".format(self.family, name) + change_read_node(self.data["subset"], node, avalon_data) return diff --git a/pype/plugins/nuke/publish/collect_instances.py b/pype/plugins/nuke/publish/collect_instances.py index 91f4fcaac8..9cc835570e 100644 --- a/pype/plugins/nuke/publish/collect_instances.py +++ b/pype/plugins/nuke/publish/collect_instances.py @@ -44,12 +44,12 @@ class CollectNukeInstances(pyblish.api.ContextPlugin): "label": node.name(), "name": node.name(), "subset": subset, - "families": [avalon_knob_data["families"]], "family": avalon_knob_data["family"], "avalonKnob": avalon_knob_data, "publish": node.knob('publish') - }) + if node.Class() == "Write": + instance.data["families"] = [avalon_knob_data["families"]] self.log.info("collected instance: {}".format(instance.data)) instances.append(instance) diff --git a/pype/plugins/nuke/publish/collect_reades.py b/pype/plugins/nuke/publish/collect_reads.py similarity index 51% rename from pype/plugins/nuke/publish/collect_reades.py rename to pype/plugins/nuke/publish/collect_reads.py index 110556e61e..e51967af91 100644 --- a/pype/plugins/nuke/publish/collect_reades.py +++ b/pype/plugins/nuke/publish/collect_reads.py @@ -1,6 +1,6 @@ import os import re - +import clique import nuke import pyblish.api import logging @@ -10,12 +10,12 @@ log = logging.getLogger(__name__) @pyblish.api.log -class CollectNukeWrites(pyblish.api.ContextPlugin): - """Collect all write nodes.""" +class CollectNukeReads(pyblish.api.ContextPlugin): + """Collect all read nodes.""" order = pyblish.api.CollectorOrder + 0.1 - label = "Collect Writes" - hosts = ["nuke", "nukeassist"] + label = "Collect Reads" + hosts = ["nuke"] def process(self, context): asset_data = io.find_one({"type": "asset", @@ -29,67 +29,64 @@ class CollectNukeWrites(pyblish.api.ContextPlugin): continue file_path = node["file"].value() - items = file_path.split(".") + file_name = os.path.basename(file_path) + items = file_name.split(".") + + if len(items) < 2: + raise ValueError + + ext = items[-1] isSequence = False if len(items) > 1: sequence = items[-2] - print sequence - hash_regex = re.compile(r"([#*])") + hash_regex = re.compile(r'([#*])') seq_regex = re.compile('[%0-9*d]') hash_match = re.match(hash_regex, sequence) seq_match = re.match(seq_regex, sequence) - if hash_match is True or seq_match is True: + if hash_match or seq_match: isSequence = True - # Get frame range - first_frame = int(nuke.root()["first_frame"].getValue()) - last_frame = int(nuke.root()["last_frame"].getValue()) - - if node["use_limit"].getValue(): - first_frame = int(node["first"].getValue()) - last_frame = int(node["last"].getValue()) + # # Get frame range + # first_frame = int(nuke.root()["first_frame"].getValue()) + # last_frame = int(nuke.root()["last_frame"].getValue()) + first_frame = node['first'].value() + last_frame = node['last'].value() # get source path - source_path = nuke.filename(node) - source_dir = os.path.dirname(source_path) + path = nuke.filename(node) + source_dir = os.path.dirname(path) self.log.debug('source dir: {}'.format(source_dir)) + + if isSequence: + # collections, remainder = clique.assemble(os.listdir(source_dir)) + # source_files = collections[0] + source_files = os.listdir(source_dir) + else: + source_files = file_name + # Include start and end render frame in label name = node.name() - label = "{0} ({1}-{2})".format( name, int(first_frame), int(last_frame) ) - - # preredered frames - if not node["render"].value(): - families = "prerendered.frames" - collected_frames = os.listdir(output_dir) - self.log.debug("collected_frames: {}".format(label)) - if "files" not in instance.data: - instance.data["files"] = list() - instance.data["files"].append(collected_frames) - instance.data['transfer'] = False - else: - # dealing with local/farm rendering - if node["render_farm"].value(): - families = "{}.farm".format(instance.data["avalonKnob"]["families"][0]) - else: - families = "{}.local".format(instance.data["avalonKnob"]["families"][0]) + nuke.message(str(source_files)) + self.log.debug("collected_frames: {}".format(label)) + if "files" not in instance.data: + instance.data["files"] = list() + instance.data["files"] = source_files + instance.data['transfer'] = False self.log.debug("checking for error: {}".format(label)) instance.data.update({ "path": path, - "outputDir": output_dir, + "stagingDir": source_dir, "ext": ext, "label": label, - "families": [families, 'ftrack'], "startFrame": first_frame, "endFrame": last_frame, - "outputType": output_type, - "stagingDir": output_dir, "colorspace": node["colorspace"].value(), "handles": int(asset_data["data"].get("handles", 0)), "step": 1, @@ -99,7 +96,3 @@ class CollectNukeWrites(pyblish.api.ContextPlugin): self.log.debug("instance.data: {}".format(instance.data)) self.log.debug("context: {}".format(context)) - - def sort_by_family(self, instance): - """Sort by family""" - return instance.data.get("families", instance.data.get("family")) diff --git a/pype/plugins/nuke/publish/integrate_rendered_frames.py b/pype/plugins/nuke/publish/integrate_rendered_frames.py index 8c178df4e4..df6734b0c9 100644 --- a/pype/plugins/nuke/publish/integrate_rendered_frames.py +++ b/pype/plugins/nuke/publish/integrate_rendered_frames.py @@ -24,7 +24,7 @@ class IntegrateFrames(pyblish.api.InstancePlugin): label = "Integrate Frames" order = pyblish.api.IntegratorOrder - families = ["prerendered.frames"] + families = ["prerendered.frames", "source"] def process(self, instance): From c55181548c7a0903bfd96796ccc16e7d862d2ccc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 2 Jan 2019 15:46:53 +0100 Subject: [PATCH 04/14] Backup --- pype/plugins/nuke/create/create_read.py | 38 +++++++++++++--------- pype/plugins/nuke/publish/collect_reads.py | 11 ++++--- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/pype/plugins/nuke/create/create_read.py b/pype/plugins/nuke/create/create_read.py index 495cdde0ad..238abbf599 100644 --- a/pype/plugins/nuke/create/create_read.py +++ b/pype/plugins/nuke/create/create_read.py @@ -14,8 +14,8 @@ class CrateRead(avalon.nuke.Creator): name = "ReadCopy" label = "Create Read Copy" hosts = ["nuke"] - # family = "read" family = "source" + families = family icon = "sign-out" def __init__(self, *args, **kwargs): @@ -23,7 +23,7 @@ class CrateRead(avalon.nuke.Creator): data = OrderedDict() data['family'] = self.family - + data['families'] = self.family {data.update({k: v}) for k, v in self.data.items() if k not in data.keys()} self.data = data @@ -35,24 +35,32 @@ class CrateRead(avalon.nuke.Creator): if not nodes or len(nodes) == 0: nuke.message('Please select Read node') - elif len(nodes) == 1: - if nodes[0].Class() != 'Read': - nuke.message('Please select Read node') - else: - node = nodes[0] - name = node["name"].value() - avalon_data = self.data - avalon_data['subset'] = "{}_{}".format(self.family, name) - change_read_node(self.data["subset"], node, avalon_data) else: + count_reads = 0 for node in nodes: + if node.Class() != 'Read': + continue name = node["name"].value() avalon_data = self.data avalon_data['subset'] = "{}_{}".format(self.family, name) - change_read_node(self.data["subset"], node, avalon_data) + self.change_read_node(self.data["subset"], node, avalon_data) + count_reads += 1 + + if count_reads < 1: + nuke.message('Please select Read node') return + def change_read_node(self, name, node, data): + node = avalon.nuke.lib.imprint(node, data) + node = self.add_transfer_knob(node) + node['tile_color'].setValue(16711935) -def change_read_node(name, node, data): - node = avalon.nuke.lib.imprint(node, data) - node['tile_color'].setValue(16711935) + def add_transfer_knob(self, node): + knob_name = "transferSource" + knob_label = "Transfer" + if knob_name not in node.knobs(): + knob = nuke.Boolean_Knob(knob_name, knob_label) + knob.setValue(True) + knob.setFlag(nuke.STARTLINE) + node.addKnob(knob) + return node diff --git a/pype/plugins/nuke/publish/collect_reads.py b/pype/plugins/nuke/publish/collect_reads.py index e51967af91..2d9ef2656b 100644 --- a/pype/plugins/nuke/publish/collect_reads.py +++ b/pype/plugins/nuke/publish/collect_reads.py @@ -48,8 +48,6 @@ class CollectNukeReads(pyblish.api.ContextPlugin): isSequence = True # # Get frame range - # first_frame = int(nuke.root()["first_frame"].getValue()) - # last_frame = int(nuke.root()["last_frame"].getValue()) first_frame = node['first'].value() last_frame = node['last'].value() @@ -72,12 +70,17 @@ class CollectNukeReads(pyblish.api.ContextPlugin): int(first_frame), int(last_frame) ) - nuke.message(str(source_files)) + self.log.debug("collected_frames: {}".format(label)) if "files" not in instance.data: instance.data["files"] = list() instance.data["files"] = source_files - instance.data['transfer'] = False + + transfer = False + if "transferSource" in node.knobs(): + transfer = node["transferSource"] + + instance.data['transfer'] = transfer self.log.debug("checking for error: {}".format(label)) instance.data.update({ From 95de48b132a2bde35a5657c1a281c820d2d086d1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 3 Jan 2019 11:12:25 +0100 Subject: [PATCH 05/14] reads are collected and source is copied to destination --- pype/plugins/nuke/create/create_read.py | 13 +--------- pype/plugins/nuke/publish/collect_families.py | 13 ++++++---- pype/plugins/nuke/publish/collect_reads.py | 24 ++++++++++--------- .../nuke/publish/integrate_rendered_frames.py | 2 +- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/pype/plugins/nuke/create/create_read.py b/pype/plugins/nuke/create/create_read.py index 238abbf599..7e29c02b57 100644 --- a/pype/plugins/nuke/create/create_read.py +++ b/pype/plugins/nuke/create/create_read.py @@ -23,7 +23,7 @@ class CrateRead(avalon.nuke.Creator): data = OrderedDict() data['family'] = self.family - data['families'] = self.family + data['families'] = self.families {data.update({k: v}) for k, v in self.data.items() if k not in data.keys()} self.data = data @@ -52,15 +52,4 @@ class CrateRead(avalon.nuke.Creator): def change_read_node(self, name, node, data): node = avalon.nuke.lib.imprint(node, data) - node = self.add_transfer_knob(node) node['tile_color'].setValue(16711935) - - def add_transfer_knob(self, node): - knob_name = "transferSource" - knob_label = "Transfer" - if knob_name not in node.knobs(): - knob = nuke.Boolean_Knob(knob_name, knob_label) - knob.setValue(True) - knob.setFlag(nuke.STARTLINE) - node.addKnob(knob) - return node diff --git a/pype/plugins/nuke/publish/collect_families.py b/pype/plugins/nuke/publish/collect_families.py index 226df3b168..cb0fbd8ce6 100644 --- a/pype/plugins/nuke/publish/collect_families.py +++ b/pype/plugins/nuke/publish/collect_families.py @@ -15,13 +15,12 @@ class CollectInstanceFamilies(pyblish.api.ContextPlugin): if not instance.data["publish"]: continue - # set for ftrack to accept - instance.data["families"] = ["ftrack"] - if "write" in instance.data["family"]: - node = instance[0] + # set for ftrack to accept + instance.data["families"] = ["ftrack"] + if not node["render"].value(): families = ["{}.frames".format( instance.data["avalonKnob"]["families"])] @@ -38,6 +37,12 @@ class CollectInstanceFamilies(pyblish.api.ContextPlugin): instance.data["families"].extend(families) + elif "source" in instance.data["family"]: + families = [] + families.append(instance.data["avalonKnob"]["families"]) + + instance.data["families"] = families + # Sort/grouped by family (preserving local index) context[:] = sorted(context, key=self.sort_by_family) diff --git a/pype/plugins/nuke/publish/collect_reads.py b/pype/plugins/nuke/publish/collect_reads.py index 2d9ef2656b..f5d3008b40 100644 --- a/pype/plugins/nuke/publish/collect_reads.py +++ b/pype/plugins/nuke/publish/collect_reads.py @@ -37,6 +37,15 @@ class CollectNukeReads(pyblish.api.ContextPlugin): ext = items[-1] + # # Get frame range + first_frame = node['first'].value() + last_frame = node['last'].value() + + # # Easier way to sequence - Not tested + # isSequence = True + # if first_frame == last_frame: + # isSequence = False + isSequence = False if len(items) > 1: sequence = items[-2] @@ -47,18 +56,12 @@ class CollectNukeReads(pyblish.api.ContextPlugin): if hash_match or seq_match: isSequence = True - # # Get frame range - first_frame = node['first'].value() - last_frame = node['last'].value() - # get source path path = nuke.filename(node) source_dir = os.path.dirname(path) self.log.debug('source dir: {}'.format(source_dir)) if isSequence: - # collections, remainder = clique.assemble(os.listdir(source_dir)) - # source_files = collections[0] source_files = os.listdir(source_dir) else: source_files = file_name @@ -72,13 +75,12 @@ class CollectNukeReads(pyblish.api.ContextPlugin): ) self.log.debug("collected_frames: {}".format(label)) - if "files" not in instance.data: - instance.data["files"] = list() - instance.data["files"] = source_files + + instance.data["files"] = [source_files] transfer = False - if "transferSource" in node.knobs(): - transfer = node["transferSource"] + if "publish" in node.knobs(): + transfer = node["publish"] instance.data['transfer'] = transfer diff --git a/pype/plugins/nuke/publish/integrate_rendered_frames.py b/pype/plugins/nuke/publish/integrate_rendered_frames.py index 908955c873..1f4992400a 100644 --- a/pype/plugins/nuke/publish/integrate_rendered_frames.py +++ b/pype/plugins/nuke/publish/integrate_rendered_frames.py @@ -24,7 +24,7 @@ class IntegrateFrames(pyblish.api.InstancePlugin): label = "Integrate Frames" order = pyblish.api.IntegratorOrder - family_targets = [".frames", ".local", ".review"] + family_targets = [".frames", ".local", ".review", "source"] def process(self, instance): From c69749c3aa90b12e186cf956bfbfa65be909eb2d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 3 Jan 2019 12:50:42 +0100 Subject: [PATCH 06/14] removed 'publish check' from 'collect families' so entities are loaded even if 'publish' is not checked when pyblish is opened --- pype/plugins/nuke/publish/collect_families.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pype/plugins/nuke/publish/collect_families.py b/pype/plugins/nuke/publish/collect_families.py index cb0fbd8ce6..d0e61c349b 100644 --- a/pype/plugins/nuke/publish/collect_families.py +++ b/pype/plugins/nuke/publish/collect_families.py @@ -12,9 +12,6 @@ class CollectInstanceFamilies(pyblish.api.ContextPlugin): def process(self, context): for instance in context.data["instances"]: - if not instance.data["publish"]: - continue - if "write" in instance.data["family"]: node = instance[0] From 477528b565ed0175bfed71be2d253d5ab0cf9d58 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 3 Jan 2019 16:24:50 +0100 Subject: [PATCH 07/14] changed icon --- pype/plugins/nuke/create/create_read.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/nuke/create/create_read.py b/pype/plugins/nuke/create/create_read.py index 7e29c02b57..8597737167 100644 --- a/pype/plugins/nuke/create/create_read.py +++ b/pype/plugins/nuke/create/create_read.py @@ -16,7 +16,7 @@ class CrateRead(avalon.nuke.Creator): hosts = ["nuke"] family = "source" families = family - icon = "sign-out" + icon = "film" def __init__(self, *args, **kwargs): super(CrateRead, self).__init__(*args, **kwargs) From 7dbc0376f641b6dfbd66d62e80e0b52ab5eddb27 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 4 Jan 2019 16:28:26 +0100 Subject: [PATCH 08/14] - script is validated. - added format in collect script plugin --- pype/plugins/nuke/publish/collect_script.py | 9 ++ pype/plugins/nuke/publish/validate_script.py | 106 +++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 pype/plugins/nuke/publish/validate_script.py diff --git a/pype/plugins/nuke/publish/collect_script.py b/pype/plugins/nuke/publish/collect_script.py index 92557b2665..f0c917b449 100644 --- a/pype/plugins/nuke/publish/collect_script.py +++ b/pype/plugins/nuke/publish/collect_script.py @@ -34,6 +34,12 @@ class CollectScript(pyblish.api.ContextPlugin): first_frame = int(root["first_frame"].getValue()) last_frame = int(root["last_frame"].getValue()) + # Get format + format = root['format'].value() + resolution_width = format.width() + resolution_height = format.height() + pixel_aspect = format.pixelAspect() + # Create instance instance = context.create_instance(subset) instance.add(root) @@ -45,6 +51,9 @@ class CollectScript(pyblish.api.ContextPlugin): "name": base_name, "startFrame": first_frame, "endFrame": last_frame, + "resolution_width": resolution_width, + "resolution_height": resolution_height, + "pixel_aspect": pixel_aspect, "publish": root.knob('publish').value(), "family": family, "representation": "nk", diff --git a/pype/plugins/nuke/publish/validate_script.py b/pype/plugins/nuke/publish/validate_script.py new file mode 100644 index 0000000000..a4ec60d96d --- /dev/null +++ b/pype/plugins/nuke/publish/validate_script.py @@ -0,0 +1,106 @@ +import pyblish.api +from avalon import io + + +@pyblish.api.log +class ValidateScript(pyblish.api.InstancePlugin): + """ Validates file output. """ + + order = pyblish.api.ValidatorOrder + 0.1 + families = ["nukescript"] + label = "Check nukescript settings" + hosts = ["nuke"] + + def process(self, instance): + instance_data = instance.data + asset_name = instance_data["asset"] + + asset = io.find_one({ + "type": "asset", + "name": asset_name + }) + asset_data = asset["data"] + + # These attributes will be checked + attributes = [ + "fps", "fstart", "fend", + "resolution_width", "resolution_height", "pixel_aspect" + ] + + # Value of these attributes can be found on parents + hierarchical_attributes = ["fps"] + + missing_attributes = [] + asset_attributes = {} + for attr in attributes: + if attr in asset_data: + asset_attributes[attr] = asset_data[attr] + + elif attr in hierarchical_attributes: + # Try to find fps on parent + parent = asset['parent'] + if asset_data['visualParent'] is not None: + parent = asset_data['visualParent'] + + value = self.check_parent_hierarchical(parent, attr) + if value is None: + missing_attributes.append(attr) + else: + asset_attributes[attr] = value + + else: + missing_attributes.append(attr) + + # Raise error if attributes weren't found on asset in database + if len(missing_attributes) > 0: + atr = ", ".join(missing_attributes) + msg = 'Missing attributes "{}" in asset "{}"' + message = msg.format(atr, asset_name) + raise ValueError(message) + + # Get handles from database, Default is 0 (if not found) + handles = 0 + if "handles" in asset_data: + handles = asset_data["handles"] + + # Set frame range with handles + asset_attributes["fstart"] -= handles + asset_attributes["fend"] += handles + + # Get values from nukescript + script_attributes = { + "fps": instance_data["fps"], + "fstart": instance_data["startFrame"], + "fend": instance_data["endFrame"], + "resolution_width": instance_data["resolution_width"], + "resolution_height": instance_data["resolution_height"], + "pixel_aspect": instance_data["pixel_aspect"] + } + + # Compare asset's values Nukescript X Database + not_matching = [] + for attr in attributes: + if asset_attributes[attr] != script_attributes[attr]: + not_matching.append(attr) + + # Raise error if not matching + if len(not_matching) > 0: + msg = "Attributes '{}' aro not set correctly" + # Alert user that handles are set if Frame start/end not match + if ( + (("fstart" in not_matching) or ("fend" in not_matching)) and + (handles > 0) + ): + handles = str(handles).replace(".0", "") + msg += " (handles are set to {})".format(handles) + message = msg.format(", ".join(not_matching)) + raise ValueError(message) + + def check_parent_hierarchical(self, entityId, attr): + if entityId is None: + return None + entity = io.find_one({"_id": entityId}) + if attr in entity['data']: + return entity['data'][attr] + else: + return self.check_parent_hierarchical(entity['parent'], attr) From 60683b49d44c6cdc869a808d2c56cab494690d7b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Jan 2019 13:18:24 +0100 Subject: [PATCH 09/14] Sync to avalon store silo as asset with silo == None --- pype/ftrack/actions/action_sync_to_avalon_local.py | 2 +- pype/ftrack/events/action_sync_to_avalon.py | 2 +- pype/ftrack/events/event_sync_to_avalon.py | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pype/ftrack/actions/action_sync_to_avalon_local.py b/pype/ftrack/actions/action_sync_to_avalon_local.py index da08cad0fc..04a56c756a 100644 --- a/pype/ftrack/actions/action_sync_to_avalon_local.py +++ b/pype/ftrack/actions/action_sync_to_avalon_local.py @@ -244,7 +244,7 @@ class SyncToAvalon(BaseAction): # return if entity is silo if len(data['parents']) == 0: - return + silo = None else: silo = data['parents'][0] diff --git a/pype/ftrack/events/action_sync_to_avalon.py b/pype/ftrack/events/action_sync_to_avalon.py index e305b30739..44dbf722b6 100644 --- a/pype/ftrack/events/action_sync_to_avalon.py +++ b/pype/ftrack/events/action_sync_to_avalon.py @@ -259,7 +259,7 @@ class Sync_To_Avalon(BaseAction): # return if entity is silo if len(data['parents']) == 0: - return + silo = None else: silo = data['parents'][0] diff --git a/pype/ftrack/events/event_sync_to_avalon.py b/pype/ftrack/events/event_sync_to_avalon.py index 0f2cb9d29f..ebf901ff72 100644 --- a/pype/ftrack/events/event_sync_to_avalon.py +++ b/pype/ftrack/events/event_sync_to_avalon.py @@ -182,9 +182,10 @@ class Sync_to_Avalon(BaseEvent): # only check name if entity is silo if len(data['parents']) == 0: - if self.checkSilo(entity, event, session) is False: - raise ExpectedError - return + silo = None + # if self.checkSilo(entity, event, session) is False: + # raise ExpectedError + # return else: silo = data['parents'][0] From 17dc705107f9325f7b24399893777a13e9be1008 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 11 Jan 2019 14:39:28 +0100 Subject: [PATCH 10/14] get_data fill visualParent to all assets instead of silo asset --- pype/ftrack/events/event_sync_to_avalon.py | 113 ++++++++++----------- pype/ftrack/ftrack_utils.py | 39 ++++--- 2 files changed, 83 insertions(+), 69 deletions(-) diff --git a/pype/ftrack/events/event_sync_to_avalon.py b/pype/ftrack/events/event_sync_to_avalon.py index ebf901ff72..f9e76b4853 100644 --- a/pype/ftrack/events/event_sync_to_avalon.py +++ b/pype/ftrack/events/event_sync_to_avalon.py @@ -1,11 +1,9 @@ import os import sys -import re import ftrack_api from ftrack_event_handler import BaseEvent from pype import lib from avalon import io, inventory -from avalon.vendor import toml from bson.objectid import ObjectId from pype.ftrack import ftrack_utils @@ -46,7 +44,10 @@ class Sync_to_Avalon(BaseEvent): # check if project have Custom Attribute 'avalon_mongo_id' if self.ca_mongoid not in self.proj['custom_attributes']: - message = "Custom attribute '{}' for 'Project' is not created or don't have set permissions for API".format(self.ca_mongoid) + message = ( + "Custom attribute '{}' for 'Project' is not created" + " or don't have set permissions for API" + ).format(self.ca_mongoid) self.log.warning(message) self.show_message(event, message, False) return @@ -85,7 +86,11 @@ class Sync_to_Avalon(BaseEvent): 'custom_attributes' not in entity or self.ca_mongoid not in entity['custom_attributes'] ): - message = "Custom attribute '{}' for '{}' is not created or don't have set permissions for API".format(self.ca_mongoid, entity.entity_type) + message = ( + "Custom attribute '{}' for '{}' is not created" + " or don't have set permissions for API" + ).format(self.ca_mongoid, entity.entity_type) + self.log.warning(message) self.show_message(event, message, False) return @@ -119,7 +124,10 @@ class Sync_to_Avalon(BaseEvent): except Exception as e: message = str(e) - ftrack_message = "SyncToAvalon event ended with unexpected error please check log file for more information." + ftrack_message = ( + 'SyncToAvalon event ended with unexpected error' + ' please check log file for more information.' + ) items = [{ 'label': 'Error', 'type': 'textarea', @@ -135,7 +143,11 @@ class Sync_to_Avalon(BaseEvent): def importToAvalon(self, session, event, entity): if self.ca_mongoid not in entity['custom_attributes']: - raise ValueError("Custom attribute '{}' for '{}' is not created or don't have set permissions for API".format(self.ca_mongoid, entity['name'])) + msg = ( + "Custom attribute '{}' for '{}' is not created" + " or don't have set permissions for API" + ).format(self.ca_mongoid, entity['name']) + raise ValueError(msg) ftrack_utils.avalon_check_name(entity) @@ -155,13 +167,19 @@ class Sync_to_Avalon(BaseEvent): entity['name'] = self.avalon_project['name'] session.commit() - msg = 'You can\'t change name {} to {}, avalon wouldn\'t work properly!\nName was changed back!'.format(self.avalon_project['name'], name) + msg = ( + 'You can\'t change name {} to {}' + ', avalon wouldn\'t work properly!' + '\nName was changed back!' + ).format(self.avalon_project['name'], name) self.errors.append(msg) return self.projectId = self.avalon_project['_id'] - data = ftrack_utils.get_data(self, entity, session, self.custom_attributes) + data = ftrack_utils.get_data( + self, entity, session, self.custom_attributes + ) io.update_many( {"_id": ObjectId(self.projectId)}, @@ -178,14 +196,13 @@ class Sync_to_Avalon(BaseEvent): if self.avalon_project is None: self.importToAvalon(session, event, self.proj) - data = ftrack_utils.get_data(self, entity, session, self.custom_attributes) + data = ftrack_utils.get_data( + self, entity, session, self.custom_attributes + ) # only check name if entity is silo if len(data['parents']) == 0: silo = None - # if self.checkSilo(entity, event, session) is False: - # raise ExpectedError - # return else: silo = data['parents'][0] @@ -204,27 +221,44 @@ class Sync_to_Avalon(BaseEvent): if avalon_asset is None: avalon_asset = io.find_one({'type': 'asset', 'name': name}) if avalon_asset is None: - mongo_id = inventory.create_asset(name, silo, data, ObjectId(self.projectId)) + mongo_id = inventory.create_asset( + name, silo, data, ObjectId(self.projectId) + ) # Raise error if it seems to be different ent. with same name elif ( avalon_asset['data']['parents'] != data['parents'] or avalon_asset['silo'] != silo ): - msg = 'In Avalon DB already exists entity with name "{0}"'.format(name) + msg = ( + 'In Avalon DB already exists entity with name "{0}"' + ).format(name) self.errors.append(msg) return else: if avalon_asset['name'] != entity['name']: - if self.checkChilds(entity) is False: - msg = 'You can\'t change name {} to {}, avalon wouldn\'t work properly!\n\nName was changed back!\n\nCreate new entity if you want to change name.'.format(avalon_asset['name'], entity['name']) + if silo is None or self.checkChilds(entity) is False: + msg = ( + 'You can\'t change name {} to {}' + ', avalon wouldn\'t work properly!' + '\n\nName was changed back!' + '\n\nCreate new entity if you want to change name.' + ).format(avalon_asset['name'], entity['name']) entity['name'] = avalon_asset['name'] session.commit() self.errors.append(msg) - if avalon_asset['silo'] != silo or avalon_asset['data']['parents'] != data['parents']: + if ( + avalon_asset['silo'] != silo or + avalon_asset['data']['parents'] != data['parents'] + ): old_path = "/".join(avalon_asset['data']['parents']) new_path = "/".join(data['parents']) - msg = 'You can\'t move with entities.\nEntity "{}" was moved from "{}" to "{}"\n\nAvalon won\'t work properly, please move them back!'.format(avalon_asset['name'], old_path, new_path) + msg = ( + 'You can\'t move with entities.' + '\nEntity "{}" was moved from "{}" to "{}"' + '\n\nAvalon won\'t work properly, please move them back!' + ).format(avalon_asset['name'], old_path, new_path) + self.errors.append(msg) if len(self.errors) > 0: @@ -262,44 +296,6 @@ class Sync_to_Avalon(BaseEvent): # If everything is allright return True - def checkSilo(self, entity, event, session): - changes = event['data']['entities'][0]['changes'] - if 'name' not in changes: - return True - new_name = changes['name']['new'] - old_name = changes['name']['old'] - - if 'children' not in entity or len(entity['children']) < 1: - return True - - if self.checkChilds(entity) is True: - self.updateSilo(old_name, new_name) - return True - - new_found = 0 - old_found = 0 - for asset in io.find({'silo': new_name}): - new_found += 1 - for asset in io.find({'silo': old_name}): - old_found += 1 - - if new_found > 0 or old_found == 0: - return True - - # If any condition is possible, show error to user and change name back - msg = 'You can\'t change name {} to {}, avalon wouldn\'t work properly!\n\nName was changed back!\n\nCreate new entity if you want to change name.'.format(old_name, new_name) - self.errors.append(msg) - entity['name'] = old_name - session.commit() - - return False - - def updateSilo(self, old, new): - io.update_many( - {'silo': old}, - {'$set': {'silo': new}} - ) - def setAvalonAttributes(self): self.custom_attributes = [] query = 'CustomAttributeGroup where name is "avalon"' @@ -333,7 +329,10 @@ class Sync_to_Avalon(BaseEvent): continue _entities.append( ( - session.get(self._get_entity_type(entity), entity.get('entityId')) + session.get( + self._get_entity_type(entity), + entity.get('entityId') + ) ) ) diff --git a/pype/ftrack/ftrack_utils.py b/pype/ftrack/ftrack_utils.py index 2177b3f8c3..d5cd9a4482 100644 --- a/pype/ftrack/ftrack_utils.py +++ b/pype/ftrack/ftrack_utils.py @@ -25,11 +25,16 @@ def get_config_data(): data = json.load(data_file) except Exception as e: - msg = 'Loading "Ftrack Config file" Failed. Please check log for more information. Times are set to default.' + msg = ( + 'Loading "Ftrack Config file" Failed.' + ' Please check log for more information.' + ' Times are set to default.' + ) log.warning("{} - {}".format(msg, str(e))) return data + def get_data(parent, entity, session, custom_attributes): entity_type = entity.entity_type @@ -42,14 +47,21 @@ def get_data(parent, entity, session, custom_attributes): if cust_attr['entity_type'].lower() in ['asset']: data[key] = entity['custom_attributes'][key] - elif cust_attr['entity_type'].lower() in ['show'] and entity_type.lower() == 'project': + elif ( + cust_attr['entity_type'].lower() in ['show'] and + entity_type.lower() == 'project' + ): data[key] = entity['custom_attributes'][key] - elif cust_attr['entity_type'].lower() in ['task'] and entity_type.lower() != 'project': + elif ( + cust_attr['entity_type'].lower() in ['task'] and + entity_type.lower() != 'project' + ): # Put space between capitals (e.g. 'AssetBuild' -> 'Asset Build') entity_type_full = re.sub(r"(\w)([A-Z])", r"\1 \2", entity_type) # Get object id of entity type - ent_obj_type_id = session.query('ObjectType where name is "{}"'.format(entity_type_full)).one()['id'] + query = 'ObjectType where name is "{}"'.format(entity_type_full) + ent_obj_type_id = session.query(query).one()['id'] if cust_attr['object_type_id'] == ent_obj_type_id: data[key] = entity['custom_attributes'][key] @@ -68,11 +80,13 @@ def get_data(parent, entity, session, custom_attributes): parents = [] folderStruct = [] for i in range(1, len(entity['link'])-1): - parEnt = session.get(entity['link'][i]['type'], entity['link'][i]['id']) + parEnt = session.get( + entity['link'][i]['type'], + entity['link'][i]['id'] + ) parName = parEnt['name'] folderStruct.append(parName) - if i > 1: - parents.append(parEnt) + parents.append(parEnt) parentId = None @@ -91,7 +105,8 @@ def get_data(parent, entity, session, custom_attributes): return data -def avalon_check_name(entity, inSchema = None): + +def avalon_check_name(entity, inSchema=None): ValidationError = jsonschema.ValidationError alright = True name = entity['name'] @@ -123,8 +138,8 @@ def avalon_check_name(entity, inSchema = None): alright = False if alright is False: - raise ValueError("{} includes unsupported symbols like 'dash' or 'space'".format(name)) - + msg = "{} includes unsupported symbols like 'dash' or 'space'" + raise ValueError(msg.format(name)) def get_apps(entity): @@ -149,6 +164,7 @@ def get_apps(entity): log.warning('Error with application {0} - {1}'.format(app, e)) return apps + def get_config(entity): config = {} config['schema'] = lib.get_avalon_project_config_schema() @@ -158,6 +174,7 @@ def get_config(entity): return config + def checkRegex(): # _handle_result -> would be solution? # """ TODO Check if name of entities match REGEX""" @@ -186,7 +203,6 @@ def checkRegex(): def get_context(entity): - parents = [] item = entity while True: @@ -209,7 +225,6 @@ def get_context(entity): ctx[entity['object_type']['name']] = entityDic - # add all parents to the context for parent in parents: tempdic = {} From 9a0f2ab81bb587220c8066bf479c6524575fc87d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 15 Jan 2019 15:45:24 +0100 Subject: [PATCH 11/14] ftrack_utils separated into 2 files, avalon's environments are set before io.install() so required env error don't show up --- pype/ftrack/actions/action_Apps.py | 46 +-- .../actions/action_sync_to_avalon_local.py | 241 +++++------ pype/ftrack/actions/action_test.py | 29 -- pype/ftrack/actions/ftrack_action_handler.py | 197 ++++++--- pype/ftrack/events/action_sync_to_avalon.py | 217 ++++------ pype/ftrack/events/event_sync_to_avalon.py | 296 +++----------- pype/ftrack/ftrack_utils/__init__.py | 2 + pype/ftrack/ftrack_utils/avalon_sync.py | 376 ++++++++++++++++++ .../ftrack/{ => ftrack_utils}/ftrack_utils.py | 111 +----- 9 files changed, 766 insertions(+), 749 deletions(-) create mode 100644 pype/ftrack/ftrack_utils/__init__.py create mode 100644 pype/ftrack/ftrack_utils/avalon_sync.py rename pype/ftrack/{ => ftrack_utils}/ftrack_utils.py (62%) diff --git a/pype/ftrack/actions/action_Apps.py b/pype/ftrack/actions/action_Apps.py index 3225724524..3f6e8b870b 100644 --- a/pype/ftrack/actions/action_Apps.py +++ b/pype/ftrack/actions/action_Apps.py @@ -1,54 +1,49 @@ import os -import logging import toml -import ftrack_api from ftrack_action_handler import AppAction from avalon import io, lib from app.api import Logger log = Logger.getLogger(__name__) + def registerApp(app, session): name = app['name'] variant = "" try: variant = app['name'].split("_")[1] - except Exception as e: - log.warning("'{0}' - App 'name' and 'variant' is not separated by '_' (variant is not set)".format(app['name'])) + except Exception: + log.warning(( + '"{0}" - App "name" and "variant" is not separated by "_"' + ' (variant is not set)' + ).format(app['name'])) return abspath = lib.which_app(app['name']) - if abspath == None: - log.error("'{0}' - App don't have config toml file".format(app['name'])) + if abspath is None: + log.error( + "'{0}' - App don't have config toml file".format(app['name']) + ) return apptoml = toml.load(abspath) executable = apptoml['executable'] - - label = app['label'] - if 'ftrack_label' in apptoml: - label = apptoml['ftrack_label'] - - icon = None - ftrack_resources = "" # Path to resources here - - if 'icon' in apptoml: - icon = apptoml['ftrack_icon'] - if '{ftrack_resources}' in icon: - icon = icon.format(ftrack_resources) - - description = None - if 'description' in apptoml: - description = apptoml['description'] + label = apptoml.get('ftrack_label', app['label']) + icon = apptoml.get('ftrack_icon', None) + description = apptoml.get('description', None) # register action - AppAction(session, label, name, executable, variant, icon, description).register() + AppAction( + session, label, name, executable, variant, icon, description + ).register() def register(session): - # TODO AVALON_PROJECT, AVALON_ASSET, AVALON_SILO need to be set or debug from avalon - + # set avalon environ - they just must exist + os.environ['AVALON_PROJECT'] = '' + os.environ['AVALON_ASSET'] = '' + os.environ['AVALON_SILO'] = '' # Get all projects from Avalon DB try: io.install() @@ -61,7 +56,6 @@ def register(session): appNames = [] # Get all application from all projects for project in projects: - os.environ['AVALON_PROJECT'] = project['name'] for app in project['config']['apps']: if app['name'] not in appNames: appNames.append(app['name']) diff --git a/pype/ftrack/actions/action_sync_to_avalon_local.py b/pype/ftrack/actions/action_sync_to_avalon_local.py index 04a56c756a..4f6f03e171 100644 --- a/pype/ftrack/actions/action_sync_to_avalon_local.py +++ b/pype/ftrack/actions/action_sync_to_avalon_local.py @@ -1,17 +1,16 @@ +import os import sys import argparse import logging -import os -import ftrack_api import json -import re -from pype import lib -from ftrack_action_handler import BaseAction -from bson.objectid import ObjectId -from avalon import io, inventory +import importlib +import ftrack_api +from avalon import io +from ftrack_action_handler import BaseAction from pype.ftrack import ftrack_utils + class SyncToAvalon(BaseAction): ''' Synchronizing data action - from Ftrack to Avalon DB @@ -54,21 +53,28 @@ class SyncToAvalon(BaseAction): #: Action description. description = 'Send data from Ftrack to Avalon' #: Action icon. - icon = 'https://cdn1.iconfinder.com/data/icons/hawcons/32/699650-icon-92-inbox-download-512.png' + icon = ( + 'https://cdn1.iconfinder.com/data/icons/hawcons/32/' + '699650-icon-92-inbox-download-512.png' + ) + def __init__(self, session): + super(SyncToAvalon, self).__init__(session) + # reload utils on initialize (in case of server restart) + importlib.reload(ftrack_utils) def discover(self, session, entities, event): ''' Validation ''' - roleCheck = False + role_check = False discover = False - roleList = ['Pypeclub'] - userId = event['source']['user']['id'] - user = session.query('User where id is ' + userId).one() + role_list = ['Pypeclub'] + user_id = event['source']['user']['id'] + user = session.query('User where id is ' + user_id).one() for role in user['user_security_roles']: - if role['security_role']['name'] in roleList: - roleCheck = True - if roleCheck is True: + if role['security_role']['name'] in role_list: + role_check = True + if role_check is True: for entity in entities: if entity.entity_type.lower() not in ['task', 'assetversion']: discover = True @@ -76,7 +82,6 @@ class SyncToAvalon(BaseAction): return discover - def launch(self, session, entities, event): message = "" @@ -93,10 +98,10 @@ class SyncToAvalon(BaseAction): }) try: - self.log.info("Action <" + self.__class__.__name__ + "> is running") - self.ca_mongoid = 'avalon_mongo_id' - #TODO AVALON_PROJECTS, AVALON_ASSET, AVALON_SILO should be set up otherwise console log shows avalon debug - self.setAvalonAttributes() + self.log.info( + "Action <" + self.__class__.__name__ + "> is running" + ) + self.importable = [] # get from top entity in hierarchy all parent entities @@ -108,7 +113,7 @@ class SyncToAvalon(BaseAction): # get all child entities separately/unique for entity in entities: - self.getShotAsset(entity) + self.add_childs_to_importable(entity) # Check names: REGEX in schema/duplicates - raise error if found all_names = [] @@ -122,24 +127,63 @@ class SyncToAvalon(BaseAction): all_names.append(e['name']) if len(duplicates) > 0: - raise ValueError("Entity name duplication: {}".format(", ".join(duplicates))) + raise ValueError( + "Entity name duplication: {}".format(", ".join(duplicates)) + ) + + # ----- PROJECT ------ + # store Ftrack project- self.importable[0] must be project entity!! + ft_project = self.importable[0] - ## ----- PROJECT ------ - # store Ftrack project- self.importable[0] must be project entity!!! - self.entityProj = self.importable[0] # set AVALON_ env - os.environ["AVALON_PROJECT"] = self.entityProj["full_name"] - os.environ["AVALON_ASSET"] = self.entityProj["full_name"] + os.environ["AVALON_PROJECT"] = ft_project["full_name"] + os.environ["AVALON_ASSET"] = ft_project["full_name"] + os.environ["AVALON_SILO"] = "" - self.avalon_project = None + avalon_project = ftrack_utils.get_avalon_proj(ft_project) + custom_attributes = ftrack_utils.get_avalon_attr(session) io.install() # Import all entities to Avalon DB - for e in self.importable: - self.importToAvalon(session, e) + for entity in self.importable: + result = ftrack_utils.import_to_avalon( + session=session, + entity=entity, + ft_project=ft_project, + av_project=avalon_project, + custom_attributes=custom_attributes + ) - io.uninstall() + if 'errors' in result and len(result['errors']) > 0: + print('error') + items = [] + for error in result['errors']: + for key, message in error.items(): + name = key.lower().replace(' ', '') + info = { + 'label': key, + 'type': 'textarea', + 'name': name, + 'value': message + } + items.append(info) + self.log.error( + '{}: {}'.format(key, message) + ) + io.uninstall() + + job['status'] = 'failed' + session.commit() + self.show_interface(event, items) + return { + 'success': False, + 'message': "Sync to avalon FAILED" + } + + if avalon_project is None: + if 'project' in result: + avalon_project = result['project'] job['status'] = 'done' session.commit() @@ -156,9 +200,18 @@ class SyncToAvalon(BaseAction): session.commit() exc_type, exc_obj, exc_tb = sys.exc_info() fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] - log_message = "{}/{}/Line: {}".format(exc_type, fname, exc_tb.tb_lineno) - self.log.error('Error during syncToAvalon: {}'.format(log_message)) - message = 'Unexpected Error - Please check Log for more information' + log_message = "{}/{}/Line: {}".format( + exc_type, fname, exc_tb.tb_lineno + ) + self.log.error( + 'Error during syncToAvalon: {}'.format(log_message) + ) + message = ( + 'Unexpected Error' + ' - Please check Log for more information' + ) + + io.uninstall() if len(message) > 0: message = "Unable to sync: {}".format(message) @@ -172,14 +225,7 @@ class SyncToAvalon(BaseAction): 'message': "Synchronization was successfull" } - def setAvalonAttributes(self): - self.custom_attributes = [] - all_avalon_attr = self.session.query('CustomAttributeGroup where name is "avalon"').one() - for cust_attr in all_avalon_attr['custom_attribute_configurations']: - if 'avalon_' not in cust_attr['key']: - self.custom_attributes.append(cust_attr) - - def getShotAsset(self, entity): + def add_childs_to_importable(self, entity): if not (entity.entity_type in ['Task']): if entity not in self.importable: self.importable.append(entity) @@ -187,116 +233,7 @@ class SyncToAvalon(BaseAction): if entity['children']: childrens = entity['children'] for child in childrens: - self.getShotAsset(child) - - def importToAvalon(self, session, entity): - # --- Begin: PUSH TO Avalon --- - - entity_type = entity.entity_type - - if entity_type.lower() in ['project']: - # Set project Config - config = ftrack_utils.get_config(entity) - # Set project template - template = lib.get_avalon_project_template_schema() - if self.ca_mongoid in entity['custom_attributes']: - try: - projectId = ObjectId(self.entityProj['custom_attributes'][self.ca_mongoid]) - self.avalon_project = io.find_one({"_id": projectId}) - except: - self.log.debug("Entity {} don't have stored entity id in ftrack".format(entity['name'])) - - if self.avalon_project is None: - self.avalon_project = io.find_one({ - "type": "project", - "name": entity["full_name"] - }) - if self.avalon_project is None: - inventory.save(entity['full_name'], config, template) - self.avalon_project = io.find_one({ - "type": "project", - "name": entity["full_name"] - }) - - elif self.avalon_project['name'] != entity['full_name']: - raise ValueError('You can\'t change name {} to {}, avalon DB won\'t work properly!'.format(self.avalon_project['name'], name)) - - data = ftrack_utils.get_data(self, entity, session,self.custom_attributes) - - # Store info about project (FtrackId) - io.update_many({ - 'type': 'project', - 'name': entity['full_name'] - }, { - '$set':{'data':data, 'config':config} - }) - - self.projectId = self.avalon_project["_id"] - if self.ca_mongoid in entity['custom_attributes']: - entity['custom_attributes'][self.ca_mongoid] = str(self.projectId) - else: - self.log.error('Custom attribute for "{}" is not created.'.format(entity['name'])) - return - - ## ----- ASSETS ------ - # Presets: - data = ftrack_utils.get_data(self, entity, session, self.custom_attributes) - - # return if entity is silo - if len(data['parents']) == 0: - silo = None - else: - silo = data['parents'][0] - - os.environ['AVALON_SILO'] = silo - - name = entity['name'] - os.environ['AVALON_ASSET'] = name - - - # Try to find asset in current database - avalon_asset = None - if self.ca_mongoid in entity['custom_attributes']: - try: - entityId = ObjectId(entity['custom_attributes'][self.ca_mongoid]) - avalon_asset = io.find_one({"_id": entityId}) - except: - self.log.debug("Entity {} don't have stored entity id in ftrack".format(entity['name'])) - - if avalon_asset is None: - avalon_asset = io.find_one({'type': 'asset', 'name': name}) - # Create if don't exists - if avalon_asset is None: - inventory.create_asset(name, silo, data, self.projectId) - self.log.debug("Asset {} - created".format(name)) - - # Raise error if it seems to be different ent. with same name - elif (avalon_asset['data']['parents'] != data['parents'] or - avalon_asset['silo'] != silo): - raise ValueError('In Avalon DB already exists entity with name "{0}"'.format(name)) - - elif avalon_asset['name'] != entity['name']: - raise ValueError('You can\'t change name {} to {}, avalon DB won\'t work properly - please set name back'.format(avalon_asset['name'], name)) - elif avalon_asset['silo'] != silo or avalon_asset['data']['parents'] != data['parents']: - old_path = "/".join(avalon_asset['data']['parents']) - new_path = "/".join(data['parents']) - raise ValueError('You can\'t move with entities. Entity "{}" was moved from "{}" to "{}" '.format(avalon_asset['name'], old_path, new_path)) - - # Update info - io.update_many({'type': 'asset','name': name}, - {'$set':{'data':data, 'silo': silo}}) - - self.log.debug("Asset {} - updated".format(name)) - - entityId = io.find_one({'type': 'asset', 'name': name})['_id'] - ## FTRACK FEATURE - FTRACK MUST HAVE avalon_mongo_id FOR EACH ENTITY TYPE EXCEPT TASK - # Set custom attribute to avalon/mongo id of entity (parentID is last) - if self.ca_mongoid in entity['custom_attributes']: - entity['custom_attributes'][self.ca_mongoid] = str(entityId) - else: - self.log.error("Custom attribute for <{}> is not created.".format(entity['name'])) - - session.commit() + self.add_childs_to_importable(child) def register(session, **kw): diff --git a/pype/ftrack/actions/action_test.py b/pype/ftrack/actions/action_test.py index 432dd44f70..f40807a420 100644 --- a/pype/ftrack/actions/action_test.py +++ b/pype/ftrack/actions/action_test.py @@ -11,7 +11,6 @@ import re import ftrack_api from ftrack_action_handler import BaseAction from avalon import io, inventory, schema -from avalon.vendor import toml class TestAction(BaseAction): @@ -24,7 +23,6 @@ class TestAction(BaseAction): #: Action description. description = 'Test action' - def discover(self, session, entities, event): ''' Validation ''' discover = False @@ -39,36 +37,9 @@ class TestAction(BaseAction): return discover - def launch(self, session, entities, event): entity = entities[0] - - entity_type = entity.entity_type - data = {} - """ - custom_attributes = [] - - all_avalon_attr = session.query('CustomAttributeGroup where name is "avalon"').one() - for cust_attr in all_avalon_attr['custom_attribute_configurations']: - if 'avalon_' not in cust_attr['key']: - custom_attributes.append(cust_attr) - """ - for cust_attr in custom_attributes: - if cust_attr['entity_type'].lower() in ['asset']: - data[cust_attr['key']] = entity['custom_attributes'][cust_attr['key']] - - elif cust_attr['entity_type'].lower() in ['show'] and entity_type.lower() == 'project': - data[cust_attr['key']] = entity['custom_attributes'][cust_attr['key']] - - elif cust_attr['entity_type'].lower() in ['task'] and entity_type.lower() != 'project': - # Put space between capitals (e.g. 'AssetBuild' -> 'Asset Build') - entity_type = re.sub(r"(\w)([A-Z])", r"\1 \2", entity_type) - # Get object id of entity type - ent_obj_type_id = session.query('ObjectType where name is "{}"'.format(entity_type)).one()['id'] - if cust_attr['type_id'] == ent_obj_type_id: - data[cust_attr['key']] = entity['custom_attributes'][cust_attr['key']] - return True diff --git a/pype/ftrack/actions/ftrack_action_handler.py b/pype/ftrack/actions/ftrack_action_handler.py index d147a2630b..56af11b557 100644 --- a/pype/ftrack/actions/ftrack_action_handler.py +++ b/pype/ftrack/actions/ftrack_action_handler.py @@ -2,20 +2,14 @@ # :copyright: Copyright (c) 2017 ftrack import os import sys -import logging -import getpass import platform import ftrack_api -import toml -from avalon import io, lib, pipeline -from avalon import session as sess +from avalon import io, lib import acre - from pype import api as pype - class AppAction(object): '''Custom Action base class @@ -27,7 +21,10 @@ class AppAction(object): - icon in ftrack ''' - def __init__(self, session, label, name, executable, variant=None, icon=None, description=None): + def __init__( + self, session, label, name, executable, + variant=None, icon=None, description=None + ): '''Expects a ftrack_api.Session instance''' self.log = pype.Logger.getLogger(self.__class__.__name__) @@ -54,22 +51,34 @@ class AppAction(object): '''Return current session.''' return self._session - def register(self, priority = 100): + def register(self, priority=100): '''Registers the action, subscribing the discover and launch topics.''' - self.session.event_hub.subscribe( - 'topic=ftrack.action.discover and source.user.username={0}'.format( - self.session.api_user - ), self._discover,priority=priority - ) + + discovery_subscription = ( + 'topic=ftrack.action.discover and source.user.username={0}' + ).format(self.session.api_user) self.session.event_hub.subscribe( - 'topic=ftrack.action.launch and data.actionIdentifier={0} and source.user.username={1}'.format( - self.identifier, - self.session.api_user - ), + discovery_subscription, + self._discover, + priority=priority + ) + + launch_subscription = ( + 'topic=ftrack.action.launch' + ' and data.actionIdentifier={0}' + ' and source.user.username={1}' + ).format( + self.identifier, + self.session.api_user + ) + self.session.event_hub.subscribe( + launch_subscription, self._launch ) - self.log.info("Application '{} {}' - Registered successfully".format(self.label,self.variant)) + self.log.info(( + "Application '{} {}' - Registered successfully" + ).format(self.label, self.variant)) def _discover(self, event): args = self._translate_event( @@ -81,7 +90,7 @@ class AppAction(object): ) if accepts: - self.log.info('Selection is valid') + self.log.debug('Selection is valid') return { 'items': [{ 'label': self.label, @@ -92,7 +101,7 @@ class AppAction(object): }] } else: - self.log.info('Selection is _not_ valid') + self.log.debug('Selection is _not_ valid') def discover(self, session, entities, event): '''Return true if we can handle the selected entities. @@ -120,18 +129,32 @@ class AppAction(object): if len(entities) > 1: return False - ft_project = entity['project'] if (entity.entity_type != 'Project') else entity + ft_project = entity + if (entity.entity_type != 'Project'): + ft_project = entity['project'] + + silo = "" + if 'ancestors' in entity: + for ancestor in entity['ancestors']: + silo = ancestor['name'] + break os.environ['AVALON_PROJECT'] = ft_project['full_name'] + os.environ['AVALON_ASSET'] = entity['name'] + os.environ['AVALON_SILO'] = silo + io.install() - project = io.find_one({"type": "project", "name": ft_project['full_name']}) + avalon_project = io.find_one({ + "type": "project", + "name": ft_project['full_name'] + }) io.uninstall() - if project is None: + if avalon_project is None: return False else: apps = [] - for app in project['config']['apps']: + for app in avalon_project['config']['apps']: apps.append(app['name']) if self.identifier not in apps: @@ -220,8 +243,9 @@ class AppAction(object): ''' - # TODO Delete this line - self.log.info("Action - {0} ({1}) - just started".format(self.label, self.identifier)) + self.log.info(( + "Action - {0} ({1}) - just started" + ).format(self.label, self.identifier)) entity, id = entities[0] entity = session.get(entity, id) @@ -238,8 +262,10 @@ class AppAction(object): anatomy = pype.Anatomy io.install() - hierarchy = io.find_one({"type": 'asset', "name": entity['parent']['name']})[ - 'data']['parents'] + hierarchy = io.find_one({ + "type": 'asset', + "name": entity['parent']['name'] + })['data']['parents'] io.uninstall() if hierarchy: hierarchy = os.path.join(*hierarchy) @@ -252,8 +278,12 @@ class AppAction(object): try: anatomy = anatomy.format(data) except Exception as e: - self.log.error("{0} Error in anatomy.format: {1}".format(__name__, e)) - os.environ["AVALON_WORKDIR"] = os.path.join(anatomy.work.root, anatomy.work.folder) + self.log.error( + "{0} Error in anatomy.format: {1}".format(__name__, e) + ) + os.environ["AVALON_WORKDIR"] = os.path.join( + anatomy.work.root, anatomy.work.folder + ) # collect all parents from the task parents = [] @@ -307,36 +337,37 @@ class AppAction(object): try: fp = open(execfile) except PermissionError as p: - self.log.error('Access denied on {0} - {1}'. - format(execfile, p)) + self.log.error('Access denied on {0} - {1}'.format( + execfile, p)) return { 'success': False, - 'message': "Access denied on launcher - {}". - format(execfile) + 'message': "Access denied on launcher - {}".format( + execfile) } fp.close() # check executable permission if not os.access(execfile, os.X_OK): - self.log.error('No executable permission on {}'. - format(execfile)) + self.log.error('No executable permission on {}'.format( + execfile)) return { 'success': False, - 'message': "No executable permission - {}" - .format(execfile) + 'message': "No executable permission - {}".format( + execfile) } pass else: - self.log.error('Launcher doesn\'t exist - {}'. - format(execfile)) + self.log.error('Launcher doesn\'t exist - {}'.format( + execfile)) return { 'success': False, - 'message': "Launcher doesn't exist - {}" - .format(execfile) + 'message': "Launcher doesn't exist - {}".format(execfile) } pass # Run SW if was found executable if execfile is not None: - lib.launch('/usr/bin/env', args=['bash', execfile], environment=env) + lib.launch( + '/usr/bin/env', args=['bash', execfile], environment=env + ) else: return { 'success': False, @@ -345,11 +376,10 @@ class AppAction(object): } pass - - # RUN TIMER IN FTRACK username = event['source']['user']['username'] - user = session.query('User where username is "{}"'.format(username)).one() + user_query = 'User where username is "{}"'.format(username) + user = session.query(user_query).one() task = session.query('Task where id is {}'.format(entity['id'])).one() self.log.info('Starting timer for task: ' + task['name']) user.start_timer(task, force=True) @@ -455,7 +485,7 @@ class BaseAction(object): def reset_session(self): self.session.reset() - def register(self, priority = 100): + def register(self, priority=100): ''' Registers the action, subscribing the the discover and launch topics. - highest priority event will show last @@ -466,15 +496,21 @@ class BaseAction(object): ), self._discover, priority=priority ) + launch_subscription = ( + 'topic=ftrack.action.launch' + ' and data.actionIdentifier={0}' + ' and source.user.username={1}' + ).format( + self.identifier, + self.session.api_user + ) self.session.event_hub.subscribe( - 'topic=ftrack.action.launch and data.actionIdentifier={0} and source.user.username={1}'.format( - self.identifier, - self.session.api_user - ), + launch_subscription, self._launch ) - self.log.info("Action '{}' - Registered successfully".format(self.__class__.__name__)) + self.log.info("Action '{}' - Registered successfully".format( + self.__class__.__name__)) def _discover(self, event): args = self._translate_event( @@ -525,8 +561,10 @@ class BaseAction(object): for entity in _selection: _entities.append( ( - session.get(self._get_entity_type(entity), entity.get('entityId')) - # self._get_entity_type(entity), entity.get('entityId') + session.get( + self._get_entity_type(entity), + entity.get('entityId') + ) ) ) @@ -623,7 +661,7 @@ class BaseAction(object): ''' return None - def show_message(self, event, input_message, result = False): + def show_message(self, event, input_message, result=False): """ Shows message to user who triggered event - event - just source of user id @@ -637,10 +675,13 @@ class BaseAction(object): try: message = str(input_message) - except: + except Exception: return user_id = event['source']['user']['id'] + target = ( + 'applicationId=ftrack.client.web and user.id="{0}"' + ).format(user_id) self.session.event_hub.publish( ftrack_api.event.base.Event( topic='ftrack.action.trigger-user-interface', @@ -649,7 +690,7 @@ class BaseAction(object): success=result, message=message ), - target='applicationId=ftrack.client.web and user.id="{0}"'.format(user_id) + target=target ), on_error='ignore' ) @@ -667,13 +708,19 @@ class BaseAction(object): } elif isinstance(result, dict): - for key in ('success', 'message'): - if key in result: - continue + if 'items' in result: + items = result['items'] + if not isinstance(items, list): + raise ValueError('Invalid items format, must be list!') - raise KeyError( - 'Missing required key: {0}.'.format(key) - ) + else: + for key in ('success', 'message'): + if key in result: + continue + + raise KeyError( + 'Missing required key: {0}.'.format(key) + ) else: self.log.error( @@ -681,3 +728,25 @@ class BaseAction(object): ) return result + + def show_interface(self, event, items): + """ + Shows interface to user who triggered event + - 'items' must be list containing Ftrack interface items + """ + user_id = event['source']['user']['id'] + target = ( + 'applicationId=ftrack.client.web and user.id="{0}"' + ).format(user_id) + + self.session.event_hub.publish( + ftrack_api.event.base.Event( + topic='ftrack.action.trigger-user-interface', + data=dict( + type='widget', + items=items + ), + target=target + ), + on_error='ignore' + ) diff --git a/pype/ftrack/events/action_sync_to_avalon.py b/pype/ftrack/events/action_sync_to_avalon.py index 44dbf722b6..524974b816 100644 --- a/pype/ftrack/events/action_sync_to_avalon.py +++ b/pype/ftrack/events/action_sync_to_avalon.py @@ -12,6 +12,7 @@ from avalon import io, inventory from pype.ftrack import ftrack_utils + class Sync_To_Avalon(BaseAction): ''' Synchronizing data action - from Ftrack to Avalon DB @@ -54,7 +55,10 @@ class Sync_To_Avalon(BaseAction): #: Action description. description = 'Send data from Ftrack to Avalon' #: Action icon. - icon = 'https://cdn1.iconfinder.com/data/icons/hawcons/32/699650-icon-92-inbox-download-512.png' + icon = ( + 'https://cdn1.iconfinder.com/data/icons/hawcons/32/' + '699650-icon-92-inbox-download-512.png' + ) def register(self): '''Registers the action, subscribing the the discover and launch topics.''' @@ -69,8 +73,10 @@ class Sync_To_Avalon(BaseAction): ), self._launch ) - - self.log.info("Action '{}' - Registered successfully".format(self.__class__.__name__)) + msg = ( + "Action '{}' - Registered successfully" + ).format(self.__class__.__name__) + self.log.info(msg) def discover(self, session, entities, event): ''' Validation ''' @@ -91,7 +97,6 @@ class Sync_To_Avalon(BaseAction): return discover - def launch(self, session, entities, event): message = "" @@ -108,10 +113,10 @@ class Sync_To_Avalon(BaseAction): }) try: - self.log.info("Action <" + self.__class__.__name__ + "> is running") - self.ca_mongoid = 'avalon_mongo_id' - #TODO AVALON_PROJECTS, AVALON_ASSET, AVALON_SILO should be set up otherwise console log shows avalon debug - self.setAvalonAttributes() + self.log.info( + "Action <" + self.__class__.__name__ + "> is running" + ) + self.importable = [] # get from top entity in hierarchy all parent entities @@ -123,7 +128,7 @@ class Sync_To_Avalon(BaseAction): # get all child entities separately/unique for entity in entities: - self.getShotAsset(entity) + self.add_childs_to_importable(entity) # Check names: REGEX in schema/duplicates - raise error if found all_names = [] @@ -137,24 +142,63 @@ class Sync_To_Avalon(BaseAction): all_names.append(e['name']) if len(duplicates) > 0: - raise ValueError("Entity name duplication: {}".format(", ".join(duplicates))) + raise ValueError( + "Entity name duplication: {}".format(", ".join(duplicates)) + ) + + # ----- PROJECT ------ + # store Ftrack project- self.importable[0] must be project entity!! + ft_project = self.importable[0] - ## ----- PROJECT ------ - # store Ftrack project- self.importable[0] must be project entity!!! - self.entityProj = self.importable[0] # set AVALON_ env - os.environ["AVALON_PROJECT"] = self.entityProj["full_name"] - os.environ["AVALON_ASSET"] = self.entityProj["full_name"] + os.environ["AVALON_PROJECT"] = ft_project["full_name"] + os.environ["AVALON_ASSET"] = ft_project["full_name"] + os.environ["AVALON_SILO"] = "" - self.avalon_project = None + avalon_project = ftrack_utils.get_avalon_proj(ft_project) + custom_attributes = ftrack_utils.get_avalon_attr(session) io.install() # Import all entities to Avalon DB - for e in self.importable: - self.importToAvalon(session, e) + for entity in self.importable: + result = ftrack_utils.import_to_avalon( + session=session, + entity=entity, + ft_project=ft_project, + av_project=avalon_project, + custom_attributes=custom_attributes + ) - io.uninstall() + if 'errors' in result and len(result['errors']) > 0: + print('error') + items = [] + for error in result['errors']: + for key, message in error.items(): + name = key.lower().replace(' ', '') + info = { + 'label': key, + 'type': 'textarea', + 'name': name, + 'value': message + } + items.append(info) + self.log.error( + '{}: {}'.format(key, message) + ) + io.uninstall() + + job['status'] = 'failed' + session.commit() + self.show_interface(event, items) + return { + 'success': False, + 'message': "Sync to avalon FAILED" + } + + if avalon_project is None: + if 'project' in result: + avalon_project = result['project'] job['status'] = 'done' session.commit() @@ -171,9 +215,18 @@ class Sync_To_Avalon(BaseAction): session.commit() exc_type, exc_obj, exc_tb = sys.exc_info() fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] - log_message = "{}/{}/Line: {}".format(exc_type, fname, exc_tb.tb_lineno) - self.log.error('Error during syncToAvalon: {}'.format(log_message)) - message = 'Unexpected Error - Please check Log for more information' + log_message = "{}/{}/Line: {}".format( + exc_type, fname, exc_tb.tb_lineno + ) + self.log.error( + 'Error during syncToAvalon: {}'.format(log_message) + ) + message = ( + 'Unexpected Error' + ' - Please check Log for more information' + ) + + io.uninstall() if len(message) > 0: message = "Unable to sync: {}".format(message) @@ -187,14 +240,7 @@ class Sync_To_Avalon(BaseAction): 'message': "Synchronization was successfull" } - def setAvalonAttributes(self): - self.custom_attributes = [] - all_avalon_attr = self.session.query('CustomAttributeGroup where name is "avalon"').one() - for cust_attr in all_avalon_attr['custom_attribute_configurations']: - if 'avalon_' not in cust_attr['key']: - self.custom_attributes.append(cust_attr) - - def getShotAsset(self, entity): + def add_childs_to_importable(self, entity): if not (entity.entity_type in ['Task']): if entity not in self.importable: self.importable.append(entity) @@ -202,116 +248,7 @@ class Sync_To_Avalon(BaseAction): if entity['children']: childrens = entity['children'] for child in childrens: - self.getShotAsset(child) - - def importToAvalon(self, session, entity): - # --- Begin: PUSH TO Avalon --- - - entity_type = entity.entity_type - - if entity_type.lower() in ['project']: - # Set project Config - config = ftrack_utils.get_config(entity) - # Set project template - template = lib.get_avalon_project_template_schema() - if self.ca_mongoid in entity['custom_attributes']: - try: - projectId = ObjectId(self.entityProj['custom_attributes'][self.ca_mongoid]) - self.avalon_project = io.find_one({"_id": projectId}) - except: - self.log.debug("Entity {} don't have stored entity id in ftrack".format(entity['name'])) - - if self.avalon_project is None: - self.avalon_project = io.find_one({ - "type": "project", - "name": entity["full_name"] - }) - if self.avalon_project is None: - inventory.save(entity['full_name'], config, template) - self.avalon_project = io.find_one({ - "type": "project", - "name": entity["full_name"] - }) - - elif self.avalon_project['name'] != entity['full_name']: - raise ValueError('You can\'t change name {} to {}, avalon DB won\'t work properly!'.format(self.avalon_project['name'], name)) - - data = ftrack_utils.get_data(self, entity, session,self.custom_attributes) - - # Store info about project (FtrackId) - io.update_many({ - 'type': 'project', - 'name': entity['full_name'] - }, { - '$set':{'data':data, 'config':config} - }) - - self.projectId = self.avalon_project["_id"] - if self.ca_mongoid in entity['custom_attributes']: - entity['custom_attributes'][self.ca_mongoid] = str(self.projectId) - else: - self.log.error('Custom attribute for "{}" is not created.'.format(entity['name'])) - return - - ## ----- ASSETS ------ - # Presets: - data = ftrack_utils.get_data(self, entity, session, self.custom_attributes) - - # return if entity is silo - if len(data['parents']) == 0: - silo = None - else: - silo = data['parents'][0] - - os.environ['AVALON_SILO'] = silo - - name = entity['name'] - os.environ['AVALON_ASSET'] = name - - - # Try to find asset in current database - avalon_asset = None - if self.ca_mongoid in entity['custom_attributes']: - try: - entityId = ObjectId(entity['custom_attributes'][self.ca_mongoid]) - avalon_asset = io.find_one({"_id": entityId}) - except: - self.log.debug("Entity {} don't have stored entity id in ftrack".format(entity['name'])) - - if avalon_asset is None: - avalon_asset = io.find_one({'type': 'asset', 'name': name}) - # Create if don't exists - if avalon_asset is None: - inventory.create_asset(name, silo, data, self.projectId) - self.log.debug("Asset {} - created".format(name)) - - # Raise error if it seems to be different ent. with same name - elif (avalon_asset['data']['parents'] != data['parents'] or - avalon_asset['silo'] != silo): - raise ValueError('In Avalon DB already exists entity with name "{0}"'.format(name)) - - elif avalon_asset['name'] != entity['name']: - raise ValueError('You can\'t change name {} to {}, avalon DB won\'t work properly - please set name back'.format(avalon_asset['name'], name)) - elif avalon_asset['silo'] != silo or avalon_asset['data']['parents'] != data['parents']: - old_path = "/".join(avalon_asset['data']['parents']) - new_path = "/".join(data['parents']) - raise ValueError('You can\'t move with entities. Entity "{}" was moved from "{}" to "{}" '.format(avalon_asset['name'], old_path, new_path)) - - # Update info - io.update_many({'type': 'asset','name': name}, - {'$set':{'data':data, 'silo': silo}}) - - self.log.debug("Asset {} - updated".format(name)) - - entityId = io.find_one({'type': 'asset', 'name': name})['_id'] - ## FTRACK FEATURE - FTRACK MUST HAVE avalon_mongo_id FOR EACH ENTITY TYPE EXCEPT TASK - # Set custom attribute to avalon/mongo id of entity (parentID is last) - if self.ca_mongoid in entity['custom_attributes']: - entity['custom_attributes'][self.ca_mongoid] = str(entityId) - else: - self.log.error("Custom attribute for <{}> is not created.".format(entity['name'])) - - session.commit() + self.add_childs_to_importable(child) def register(session, **kw): diff --git a/pype/ftrack/events/event_sync_to_avalon.py b/pype/ftrack/events/event_sync_to_avalon.py index f9e76b4853..7c1981bd5f 100644 --- a/pype/ftrack/events/event_sync_to_avalon.py +++ b/pype/ftrack/events/event_sync_to_avalon.py @@ -1,126 +1,123 @@ import os -import sys import ftrack_api from ftrack_event_handler import BaseEvent -from pype import lib -from avalon import io, inventory -from bson.objectid import ObjectId +from avalon import io from pype.ftrack import ftrack_utils -class ExpectedError(Exception): - def __init__(self, *args, **kwargs): - super().__init__(self, *args, **kwargs) - - class Sync_to_Avalon(BaseEvent): def launch(self, session, entities, event): - self.ca_mongoid = 'avalon_mongo_id' + + ca_mongoid = ftrack_utils.get_ca_mongoid() # If mongo_id textfield has changed: RETURN! # - infinite loop for ent in event['data']['entities']: if 'keys' in ent: - if self.ca_mongoid in ent['keys']: + if ca_mongoid in ent['keys']: return - self.proj = None - self.errors = [] + + ft_project = None # get project for entity in entities: try: base_proj = entity['link'][0] - except: + except Exception: continue - self.proj = session.get(base_proj['type'], base_proj['id']) + ft_project = session.get(base_proj['type'], base_proj['id']) break # check if project is set to auto-sync if ( - self.proj is None or - 'avalon_auto_sync' not in self.proj['custom_attributes'] or - self.proj['custom_attributes']['avalon_auto_sync'] is False + ft_project is None or + 'avalon_auto_sync' not in ft_project['custom_attributes'] or + ft_project['custom_attributes']['avalon_auto_sync'] is False ): return # check if project have Custom Attribute 'avalon_mongo_id' - if self.ca_mongoid not in self.proj['custom_attributes']: + if ca_mongoid not in ft_project['custom_attributes']: message = ( "Custom attribute '{}' for 'Project' is not created" " or don't have set permissions for API" - ).format(self.ca_mongoid) + ).format(ca_mongoid) self.log.warning(message) self.show_message(event, message, False) return - self.projectId = self.proj['custom_attributes'][self.ca_mongoid] - - os.environ["AVALON_PROJECT"] = self.proj['full_name'] + os.environ["AVALON_PROJECT"] = ft_project['full_name'] + os.environ["AVALON_ASSET"] = ft_project["full_name"] + os.environ["AVALON_SILO"] = "" # get avalon project if possible - io.install() - try: - self.avalon_project = io.find_one({ - "_id": ObjectId(self.projectId) - }) - except: - self.avalon_project = None + import_entities = [] - importEntities = [] - if self.avalon_project is None: - self.avalon_project = io.find_one({ - "type": "project", - "name": self.proj["full_name"] - }) - if self.avalon_project is None: - importEntities.append(self.proj) - else: - self.projectId = self.avalon_project['_id'] - - io.uninstall() + avalon_project = ftrack_utils.get_avalon_proj(ft_project) + if avalon_project is None: + import_entities.append(ft_project) for entity in entities: if entity.entity_type.lower() in ['task']: entity = entity['parent'] - if ( - 'custom_attributes' not in entity or - self.ca_mongoid not in entity['custom_attributes'] - ): + if 'custom_attributes' not in entity: + continue + if ca_mongoid not in entity['custom_attributes']: + message = ( "Custom attribute '{}' for '{}' is not created" " or don't have set permissions for API" - ).format(self.ca_mongoid, entity.entity_type) + ).format(ca_mongoid, entity.entity_type) self.log.warning(message) self.show_message(event, message, False) return - if entity not in importEntities: - importEntities.append(entity) + if entity not in import_entities: + import_entities.append(entity) - if len(importEntities) < 1: + if len(import_entities) < 1: return - self.setAvalonAttributes() + avalon_project = ftrack_utils.get_avalon_proj(ft_project) + custom_attributes = ftrack_utils.get_avalon_attr(session) io.install() - try: - for entity in importEntities: - self.importToAvalon(session, event, entity) - session.commit() - except ExpectedError as ee: - items = [] - for error in self.errors: - info = { - 'label': 'Error', - 'type': 'textarea', - 'name': 'error', - 'value': error - } - items.append(info) - self.log.warning(error) - self.show_interface(event, items) + try: + for entity in import_entities: + result = ftrack_utils.import_to_avalon( + session=session, + entity=entity, + ft_project=ft_project, + av_project=avalon_project, + custom_attributes=custom_attributes + ) + if 'errors' in result and len(result['errors']) > 0: + print('error') + items = [] + for error in result['errors']: + for key, message in error.items(): + name = key.lower().replace(' ', '') + info = { + 'label': key, + 'type': 'textarea', + 'name': name, + 'value': message + } + items.append(info) + self.log.error( + '{}: {}'.format(key, message) + ) + io.uninstall() + + session.commit() + self.show_interface(event, items) + return + + if avalon_project is None: + if 'project' in result: + avalon_project = result['project'] except Exception as e: message = str(e) @@ -129,7 +126,7 @@ class Sync_to_Avalon(BaseEvent): ' please check log file for more information.' ) items = [{ - 'label': 'Error', + 'label': 'Fatal Error', 'type': 'textarea', 'name': 'error', 'value': ftrack_message @@ -141,169 +138,6 @@ class Sync_to_Avalon(BaseEvent): return - def importToAvalon(self, session, event, entity): - if self.ca_mongoid not in entity['custom_attributes']: - msg = ( - "Custom attribute '{}' for '{}' is not created" - " or don't have set permissions for API" - ).format(self.ca_mongoid, entity['name']) - raise ValueError(msg) - - ftrack_utils.avalon_check_name(entity) - - entity_type = entity.entity_type - - if entity_type in ['Project']: - type = 'project' - name = entity['full_name'] - config = ftrack_utils.get_config(entity) - template = lib.get_avalon_project_template_schema() - - if self.avalon_project is None: - inventory.save(name, config, template) - self.avalon_project = io.find_one({'type': type, 'name': name}) - - elif self.avalon_project['name'] != name: - entity['name'] = self.avalon_project['name'] - session.commit() - - msg = ( - 'You can\'t change name {} to {}' - ', avalon wouldn\'t work properly!' - '\nName was changed back!' - ).format(self.avalon_project['name'], name) - self.errors.append(msg) - return - - self.projectId = self.avalon_project['_id'] - - data = ftrack_utils.get_data( - self, entity, session, self.custom_attributes - ) - - io.update_many( - {"_id": ObjectId(self.projectId)}, - {'$set': { - 'name': name, - 'config': config, - 'data': data, - }}) - - entity['custom_attributes'][self.ca_mongoid] = str(self.projectId) - - return - - if self.avalon_project is None: - self.importToAvalon(session, event, self.proj) - - data = ftrack_utils.get_data( - self, entity, session, self.custom_attributes - ) - - # only check name if entity is silo - if len(data['parents']) == 0: - silo = None - else: - silo = data['parents'][0] - - name = entity['name'] - - os.environ["AVALON_ASSET"] = name - os.environ['AVALON_SILO'] = silo - - avalon_asset = None - # existence of this custom attr is already checked - mongo_id = entity['custom_attributes'][self.ca_mongoid] - - if mongo_id is not "": - avalon_asset = io.find_one({'_id': ObjectId(mongo_id)}) - - if avalon_asset is None: - avalon_asset = io.find_one({'type': 'asset', 'name': name}) - if avalon_asset is None: - mongo_id = inventory.create_asset( - name, silo, data, ObjectId(self.projectId) - ) - # Raise error if it seems to be different ent. with same name - elif ( - avalon_asset['data']['parents'] != data['parents'] or - avalon_asset['silo'] != silo - ): - msg = ( - 'In Avalon DB already exists entity with name "{0}"' - ).format(name) - self.errors.append(msg) - return - else: - if avalon_asset['name'] != entity['name']: - if silo is None or self.checkChilds(entity) is False: - msg = ( - 'You can\'t change name {} to {}' - ', avalon wouldn\'t work properly!' - '\n\nName was changed back!' - '\n\nCreate new entity if you want to change name.' - ).format(avalon_asset['name'], entity['name']) - entity['name'] = avalon_asset['name'] - session.commit() - self.errors.append(msg) - - if ( - avalon_asset['silo'] != silo or - avalon_asset['data']['parents'] != data['parents'] - ): - old_path = "/".join(avalon_asset['data']['parents']) - new_path = "/".join(data['parents']) - msg = ( - 'You can\'t move with entities.' - '\nEntity "{}" was moved from "{}" to "{}"' - '\n\nAvalon won\'t work properly, please move them back!' - ).format(avalon_asset['name'], old_path, new_path) - - self.errors.append(msg) - - if len(self.errors) > 0: - raise ExpectedError - - io.update_many( - {"_id": ObjectId(mongo_id)}, - {'$set': { - 'name': name, - 'silo': silo, - 'data': data, - 'parent': ObjectId(self.projectId)}}) - - entity['custom_attributes'][self.ca_mongoid] = str(mongo_id) - - def checkChilds(self, entity): - if (entity.entity_type.lower() != 'task' and 'children' not in entity): - return True - childs = entity['children'] - for child in childs: - if child.entity_type.lower() == 'task': - config = ftrack_utils.get_config_data() - if 'sync_to_avalon' in config: - config = config['sync_to_avalon'] - if 'statuses_name_change' in config: - available_statuses = config['statuses_name_change'] - else: - available_statuses = [] - ent_status = child['status']['name'].lower() - if ent_status not in available_statuses: - return False - # If not task go deeper - elif self.checkChilds(child) is False: - return False - # If everything is allright - return True - - def setAvalonAttributes(self): - self.custom_attributes = [] - query = 'CustomAttributeGroup where name is "avalon"' - all_avalon_attr = self.session.query(query).one() - for cust_attr in all_avalon_attr['custom_attribute_configurations']: - if 'avalon_' not in cust_attr['key']: - self.custom_attributes.append(cust_attr) - def _launch(self, event): self.session.reset() diff --git a/pype/ftrack/ftrack_utils/__init__.py b/pype/ftrack/ftrack_utils/__init__.py new file mode 100644 index 0000000000..fb09e548e6 --- /dev/null +++ b/pype/ftrack/ftrack_utils/__init__.py @@ -0,0 +1,2 @@ +from .ftrack_utils import * +from .avalon_sync import * diff --git a/pype/ftrack/ftrack_utils/avalon_sync.py b/pype/ftrack/ftrack_utils/avalon_sync.py new file mode 100644 index 0000000000..c7eeee6221 --- /dev/null +++ b/pype/ftrack/ftrack_utils/avalon_sync.py @@ -0,0 +1,376 @@ +import os +import re +from pype import lib +from avalon import io, inventory +from bson.objectid import ObjectId +from pype.ftrack.ftrack_utils import ftrack_utils +from avalon.vendor import jsonschema +from app.api import Logger +ValidationError = jsonschema.ValidationError + +log = Logger.getLogger(__name__) + + +def get_ca_mongoid(): + # returns name of Custom attribute that stores mongo_id + return 'avalon_mongo_id' + + +def import_to_avalon( + session, entity, ft_project, av_project, custom_attributes +): + output = {} + errors = [] + + ca_mongoid = get_ca_mongoid() + # Validate if entity has custom attribute avalon_mongo_id + if ca_mongoid not in entity['custom_attributes']: + msg = ( + 'Custom attribute "{}" for "{}" is not created' + ' or don\'t have set permissions for API' + ).format(ca_mongoid, entity['name']) + errors.append({'Custom attribute error': msg}) + output['errors'] = errors + return output + + # Validate if entity name match REGEX in schema + try: + ftrack_utils.avalon_check_name(entity) + except ValidationError: + msg = '"{}" includes unsupported symbols like "dash" or "space"' + errors.append({'Unsupported character': msg}) + output['errors'] = errors + return output + + entity_type = entity.entity_type + # Project //////////////////////////////////////////////////////////////// + if entity_type in ['Project']: + type = 'project' + name = entity['full_name'] + config = ftrack_utils.get_config(entity) + template = lib.get_avalon_project_template_schema() + + av_project_code = None + if av_project is not None and 'code' in av_project['data']: + av_project_code = av_project['data']['code'] + ft_project_code = ft_project['name'] + + if av_project is None: + inventory.save(name, config, template) + av_project = io.find_one({'type': type, 'name': name}) + + elif av_project['name'] != name or av_project_code != ft_project_code: + msg = ( + 'You can\'t change {0} "{1}" to "{2}"' + ', avalon wouldn\'t work properly!' + '\n{0} was changed back!' + ) + if av_project['name'] != name: + entity['full_name'] = av_project['name'] + errors.append( + {'Changed name error': msg.format( + 'Project name', av_project['name'], name + )} + ) + if av_project_code != ft_project_code: + entity['name'] = av_project_code + errors.append( + {'Changed name error': msg.format( + 'Project code', av_project_code, ft_project_code + )} + ) + + session.commit() + + output['errors'] = errors + return output + + projectId = av_project['_id'] + + data = get_data( + entity, session, custom_attributes + ) + + io.update_many( + {'_id': ObjectId(projectId)}, + {'$set': { + 'name': name, + 'config': config, + 'data': data, + }}) + + entity['custom_attributes'][ca_mongoid] = str(projectId) + session.commit() + + output['project'] = av_project + + return output + + # Asset - ///////////////////////////////////////////////////////////// + if av_project is None: + result = import_to_avalon( + session, ft_project, ft_project, av_project, custom_attributes + ) + + if 'errors' in result: + output['errors'] = result['errors'] + return output + + elif 'project' not in result: + msg = 'During project import went something wrong' + errors.append({'Unexpected error': msg}) + output['errors'] = errors + return output + + av_project = result['project'] + output['project'] = result['project'] + + projectId = av_project['_id'] + data = get_data( + entity, session, custom_attributes + ) + + # 1. hierarchical entity have silo set to None + silo = None + if len(data['parents']) > 0: + silo = data['parents'][0] + + name = entity['name'] + + os.environ['AVALON_SILO'] = silo + os.environ['AVALON_ASSET'] = name + + avalon_asset = None + # existence of this custom attr is already checked + if ca_mongoid not in entity['custom_attributes']: + msg = '"{}" don\'t have "{}" custom attribute' + errors.append({'Missing Custom attribute': msg.format( + entity_type, ca_mongoid + )}) + output['errors'] = errors + return output + + mongo_id = entity['custom_attributes'][ca_mongoid] + + if mongo_id is not '': + avalon_asset = io.find_one({'_id': ObjectId(mongo_id)}) + + if avalon_asset is None: + avalon_asset = io.find_one({'type': 'asset', 'name': name}) + if avalon_asset is None: + mongo_id = inventory.create_asset( + name, silo, data, ObjectId(projectId) + ) + # Raise error if it seems to be different ent. with same name + elif ( + avalon_asset['data']['parents'] != data['parents'] or + avalon_asset['silo'] != silo + ): + msg = ( + 'In Avalon DB already exists entity with name "{0}"' + ).format(name) + errors.append({'Entity name duplication': msg}) + output['errors'] = errors + return output + else: + if avalon_asset['name'] != entity['name']: + if silo is None or changeability_check_childs(entity) is False: + msg = ( + 'You can\'t change name {} to {}' + ', avalon wouldn\'t work properly!' + '\n\nName was changed back!' + '\n\nCreate new entity if you want to change name.' + ).format(avalon_asset['name'], entity['name']) + entity['name'] = avalon_asset['name'] + session.commit() + errors.append({'Changed name error': msg}) + + if ( + avalon_asset['silo'] != silo or + avalon_asset['data']['parents'] != data['parents'] + ): + old_path = '/'.join(avalon_asset['data']['parents']) + new_path = '/'.join(data['parents']) + + msg = ( + 'You can\'t move with entities.' + '\nEntity "{}" was moved from "{}" to "{}"' + '\n\nAvalon won\'t work properly, {}!' + ) + + moved_back = False + if 'visualParent' in avalon_asset['data']: + if silo is None: + asset_parent_id = avalon_asset['parent'] + else: + asset_parent_id = avalon_asset['data']['visualParent'] + + asset_parent = io.find_one({'_id': ObjectId(asset_parent_id)}) + ft_parent_id = asset_parent['data']['ftrackId'] + try: + entity['parent_id'] = ft_parent_id + session.commit() + msg = msg.format( + avalon_asset['name'], old_path, new_path, + 'entity was moved back' + ) + moved_back = True + + except Exception: + moved_back = False + + if moved_back is False: + msg = msg.format( + avalon_asset['name'], old_path, new_path, + 'please move it back' + ) + + errors.append({'Hierarchy change error': msg}) + + if len(errors) > 0: + output['errors'] = errors + return output + + io.update_many( + {'_id': ObjectId(mongo_id)}, + {'$set': { + 'name': name, + 'silo': silo, + 'data': data, + 'parent': ObjectId(projectId) + }}) + + entity['custom_attributes'][ca_mongoid] = str(mongo_id) + session.commit() + + return output + + +def get_avalon_attr(session): + custom_attributes = [] + query = 'CustomAttributeGroup where name is "avalon"' + all_avalon_attr = session.query(query).one() + for cust_attr in all_avalon_attr['custom_attribute_configurations']: + if 'avalon_' not in cust_attr['key']: + custom_attributes.append(cust_attr) + return custom_attributes + + +def changeability_check_childs(entity): + if (entity.entity_type.lower() != 'task' and 'children' not in entity): + return True + childs = entity['children'] + for child in childs: + if child.entity_type.lower() == 'task': + config = ftrack_utils.get_config_data() + if 'sync_to_avalon' in config: + config = config['sync_to_avalon'] + if 'statuses_name_change' in config: + available_statuses = config['statuses_name_change'] + else: + available_statuses = [] + ent_status = child['status']['name'].lower() + if ent_status not in available_statuses: + return False + # If not task go deeper + elif changeability_check_childs(child) is False: + return False + # If everything is allright + return True + + +def get_data(entity, session, custom_attributes): + entity_type = entity.entity_type + + data = {} + data['ftrackId'] = entity['id'] + data['entityType'] = entity_type + + for cust_attr in custom_attributes: + key = cust_attr['key'] + if cust_attr['entity_type'].lower() in ['asset']: + data[key] = entity['custom_attributes'][key] + + elif ( + cust_attr['entity_type'].lower() in ['show'] and + entity_type.lower() == 'project' + ): + data[key] = entity['custom_attributes'][key] + + elif ( + cust_attr['entity_type'].lower() in ['task'] and + entity_type.lower() != 'project' + ): + # Put space between capitals (e.g. 'AssetBuild' -> 'Asset Build') + entity_type_full = re.sub(r"(\w)([A-Z])", r"\1 \2", entity_type) + # Get object id of entity type + query = 'ObjectType where name is "{}"'.format(entity_type_full) + ent_obj_type_id = session.query(query).one()['id'] + + if cust_attr['object_type_id'] == ent_obj_type_id: + data[key] = entity['custom_attributes'][key] + + if entity_type in ['Project']: + data['code'] = entity['name'] + return data + + # Get info for 'Data' in Avalon DB + tasks = [] + for child in entity['children']: + if child.entity_type in ['Task']: + tasks.append(child['name']) + + # Get list of parents without project + parents = [] + folderStruct = [] + for i in range(1, len(entity['link'])-1): + parEnt = session.get( + entity['link'][i]['type'], + entity['link'][i]['id'] + ) + parName = parEnt['name'] + folderStruct.append(parName) + parents.append(parEnt) + + parentId = None + + for parent in parents: + parentId = io.find_one({'type': 'asset', 'name': parName})['_id'] + if parent['parent'].entity_type != 'project' and parentId is None: + parent.importToAvalon(session, parent) + parentId = io.find_one({'type': 'asset', 'name': parName})['_id'] + + hierarchy = os.path.sep.join(folderStruct) + + data['visualParent'] = parentId + data['parents'] = folderStruct + data['tasks'] = tasks + data['hierarchy'] = hierarchy + + return data + + +def get_avalon_proj(ft_project): + io.install() + + ca_mongoid = get_ca_mongoid() + if ca_mongoid not in ft_project['custom_attributes']: + return None + + project_id = ft_project['custom_attributes'][ca_mongoid] + try: + avalon_project = io.find_one({ + "_id": ObjectId(project_id) + }) + except Exception: + avalon_project = None + + if avalon_project is None: + avalon_project = io.find_one({ + "type": "project", + "name": ft_project["full_name"] + }) + + io.uninstall() + + return avalon_project diff --git a/pype/ftrack/ftrack_utils.py b/pype/ftrack/ftrack_utils/ftrack_utils.py similarity index 62% rename from pype/ftrack/ftrack_utils.py rename to pype/ftrack/ftrack_utils/ftrack_utils.py index d5cd9a4482..f6a239639d 100644 --- a/pype/ftrack/ftrack_utils.py +++ b/pype/ftrack/ftrack_utils/ftrack_utils.py @@ -1,14 +1,9 @@ import os -import sys -import re import json -from pprint import * -import ftrack_api from pype import lib -import avalon.io as io -import avalon.api import avalon +import avalon.api from avalon.vendor import toml, jsonschema from app.api import Logger @@ -35,77 +30,6 @@ def get_config_data(): return data -def get_data(parent, entity, session, custom_attributes): - entity_type = entity.entity_type - - data = {} - data['ftrackId'] = entity['id'] - data['entityType'] = entity_type - - for cust_attr in custom_attributes: - key = cust_attr['key'] - if cust_attr['entity_type'].lower() in ['asset']: - data[key] = entity['custom_attributes'][key] - - elif ( - cust_attr['entity_type'].lower() in ['show'] and - entity_type.lower() == 'project' - ): - data[key] = entity['custom_attributes'][key] - - elif ( - cust_attr['entity_type'].lower() in ['task'] and - entity_type.lower() != 'project' - ): - # Put space between capitals (e.g. 'AssetBuild' -> 'Asset Build') - entity_type_full = re.sub(r"(\w)([A-Z])", r"\1 \2", entity_type) - # Get object id of entity type - query = 'ObjectType where name is "{}"'.format(entity_type_full) - ent_obj_type_id = session.query(query).one()['id'] - - if cust_attr['object_type_id'] == ent_obj_type_id: - data[key] = entity['custom_attributes'][key] - - if entity_type in ['Project']: - data['code'] = entity['name'] - return data - - # Get info for 'Data' in Avalon DB - tasks = [] - for child in entity['children']: - if child.entity_type in ['Task']: - tasks.append(child['name']) - - # Get list of parents without project - parents = [] - folderStruct = [] - for i in range(1, len(entity['link'])-1): - parEnt = session.get( - entity['link'][i]['type'], - entity['link'][i]['id'] - ) - parName = parEnt['name'] - folderStruct.append(parName) - parents.append(parEnt) - - parentId = None - - for parent in parents: - parentId = io.find_one({'type': 'asset', 'name': parName})['_id'] - if parent['parent'].entity_type != 'project' and parentId is None: - parent.importToAvalon(session, parent) - parentId = io.find_one({'type': 'asset', 'name': parName})['_id'] - - hierarchy = os.path.sep.join(folderStruct) - - data['visualParent'] = parentId - data['parents'] = folderStruct - data['tasks'] = tasks - data['hierarchy'] = hierarchy - - return data - - def avalon_check_name(entity, inSchema=None): ValidationError = jsonschema.ValidationError alright = True @@ -138,7 +62,7 @@ def avalon_check_name(entity, inSchema=None): alright = False if alright is False: - msg = "{} includes unsupported symbols like 'dash' or 'space'" + msg = '"{}" includes unsupported symbols like "dash" or "space"' raise ValueError(msg.format(name)) @@ -175,33 +99,6 @@ def get_config(entity): return config -def checkRegex(): - # _handle_result -> would be solution? - # """ TODO Check if name of entities match REGEX""" - for entity in importable: - for e in entity['link']: - item = { - "silo": "silo", - "parent": "parent", - "type": "asset", - "schema": "avalon-core:asset-2.0", - "name": e['name'], - "data": dict(), - } - try: - schema.validate(item) - except Exception as e: - print(e) - print(e['name']) - ftrack.EVENT_HUB.publishReply( - event, - data={ - 'success': False, - 'message': 'Entity name contains invalid character!' - } - ) - - def get_context(entity): parents = [] item = entity @@ -220,7 +117,7 @@ def get_context(entity): } try: entityDic['type'] = entity['type']['name'] - except: + except Exception: pass ctx[entity['object_type']['name']] = entityDic @@ -288,7 +185,7 @@ def get_next_task(task): if t.get('typeid') == task.get('typeid'): try: next_types = types_sorted[(types_sorted.index(t) + 1):] - except: + except Exception: pass for nt in next_types: From ae68a414004e0cf8c08113a70a8cc0cab8c23fc1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 16 Jan 2019 14:55:40 +0100 Subject: [PATCH 12/14] removed avalon.io so it won't change context in another apps, replaced with direct connection to DB --- .../actions/action_sync_to_avalon_local.py | 14 +- pype/ftrack/events/action_sync_to_avalon.py | 22 +-- pype/ftrack/events/event_sync_to_avalon.py | 19 +-- pype/ftrack/ftrack_utils/avalon_sync.py | 125 +++++++++++++----- pype/ftrack/ftrack_utils/ftrack_utils.py | 2 +- pype/lib.py | 10 +- 6 files changed, 109 insertions(+), 83 deletions(-) diff --git a/pype/ftrack/actions/action_sync_to_avalon_local.py b/pype/ftrack/actions/action_sync_to_avalon_local.py index 4f6f03e171..85978c0d6c 100644 --- a/pype/ftrack/actions/action_sync_to_avalon_local.py +++ b/pype/ftrack/actions/action_sync_to_avalon_local.py @@ -6,7 +6,6 @@ import json import importlib import ftrack_api -from avalon import io from ftrack_action_handler import BaseAction from pype.ftrack import ftrack_utils @@ -134,17 +133,9 @@ class SyncToAvalon(BaseAction): # ----- PROJECT ------ # store Ftrack project- self.importable[0] must be project entity!! ft_project = self.importable[0] - - # set AVALON_ env - os.environ["AVALON_PROJECT"] = ft_project["full_name"] - os.environ["AVALON_ASSET"] = ft_project["full_name"] - os.environ["AVALON_SILO"] = "" - - avalon_project = ftrack_utils.get_avalon_proj(ft_project) + avalon_project = ftrack_utils.get_avalon_project(ft_project) custom_attributes = ftrack_utils.get_avalon_attr(session) - io.install() - # Import all entities to Avalon DB for entity in self.importable: result = ftrack_utils.import_to_avalon( @@ -171,7 +162,6 @@ class SyncToAvalon(BaseAction): self.log.error( '{}: {}'.format(key, message) ) - io.uninstall() job['status'] = 'failed' session.commit() @@ -211,8 +201,6 @@ class SyncToAvalon(BaseAction): ' - Please check Log for more information' ) - io.uninstall() - if len(message) > 0: message = "Unable to sync: {}".format(message) return { diff --git a/pype/ftrack/events/action_sync_to_avalon.py b/pype/ftrack/events/action_sync_to_avalon.py index 524974b816..92a22695e1 100644 --- a/pype/ftrack/events/action_sync_to_avalon.py +++ b/pype/ftrack/events/action_sync_to_avalon.py @@ -1,16 +1,11 @@ +import os import sys import argparse import logging -import os import ftrack_api import json -import re -from pype import lib -from pype.ftrack.actions.ftrack_action_handler import BaseAction -from bson.objectid import ObjectId -from avalon import io, inventory - from pype.ftrack import ftrack_utils +from pype.ftrack.actions.ftrack_action_handler import BaseAction class Sync_To_Avalon(BaseAction): @@ -149,17 +144,9 @@ class Sync_To_Avalon(BaseAction): # ----- PROJECT ------ # store Ftrack project- self.importable[0] must be project entity!! ft_project = self.importable[0] - - # set AVALON_ env - os.environ["AVALON_PROJECT"] = ft_project["full_name"] - os.environ["AVALON_ASSET"] = ft_project["full_name"] - os.environ["AVALON_SILO"] = "" - - avalon_project = ftrack_utils.get_avalon_proj(ft_project) + avalon_project = ftrack_utils.get_avalon_project(ft_project) custom_attributes = ftrack_utils.get_avalon_attr(session) - io.install() - # Import all entities to Avalon DB for entity in self.importable: result = ftrack_utils.import_to_avalon( @@ -186,7 +173,6 @@ class Sync_To_Avalon(BaseAction): self.log.error( '{}: {}'.format(key, message) ) - io.uninstall() job['status'] = 'failed' session.commit() @@ -226,8 +212,6 @@ class Sync_To_Avalon(BaseAction): ' - Please check Log for more information' ) - io.uninstall() - if len(message) > 0: message = "Unable to sync: {}".format(message) return { diff --git a/pype/ftrack/events/event_sync_to_avalon.py b/pype/ftrack/events/event_sync_to_avalon.py index 7c1981bd5f..8ecd860644 100644 --- a/pype/ftrack/events/event_sync_to_avalon.py +++ b/pype/ftrack/events/event_sync_to_avalon.py @@ -1,8 +1,7 @@ import os import ftrack_api -from ftrack_event_handler import BaseEvent -from avalon import io from pype.ftrack import ftrack_utils +from ftrack_event_handler import BaseEvent class Sync_to_Avalon(BaseEvent): @@ -45,14 +44,12 @@ class Sync_to_Avalon(BaseEvent): self.show_message(event, message, False) return - os.environ["AVALON_PROJECT"] = ft_project['full_name'] - os.environ["AVALON_ASSET"] = ft_project["full_name"] - os.environ["AVALON_SILO"] = "" - # get avalon project if possible import_entities = [] - avalon_project = ftrack_utils.get_avalon_proj(ft_project) + custom_attributes = ftrack_utils.get_avalon_attr(session) + + avalon_project = ftrack_utils.get_avalon_project(ft_project) if avalon_project is None: import_entities.append(ft_project) @@ -79,11 +76,6 @@ class Sync_to_Avalon(BaseEvent): if len(import_entities) < 1: return - avalon_project = ftrack_utils.get_avalon_proj(ft_project) - custom_attributes = ftrack_utils.get_avalon_attr(session) - - io.install() - try: for entity in import_entities: result = ftrack_utils.import_to_avalon( @@ -109,7 +101,6 @@ class Sync_to_Avalon(BaseEvent): self.log.error( '{}: {}'.format(key, message) ) - io.uninstall() session.commit() self.show_interface(event, items) @@ -134,8 +125,6 @@ class Sync_to_Avalon(BaseEvent): self.show_interface(event, items) self.log.error(message) - io.uninstall() - return def _launch(self, event): diff --git a/pype/ftrack/ftrack_utils/avalon_sync.py b/pype/ftrack/ftrack_utils/avalon_sync.py index c7eeee6221..c268d72295 100644 --- a/pype/ftrack/ftrack_utils/avalon_sync.py +++ b/pype/ftrack/ftrack_utils/avalon_sync.py @@ -1,7 +1,7 @@ import os import re from pype import lib -from avalon import io, inventory +from avalon import io, schema from bson.objectid import ObjectId from pype.ftrack.ftrack_utils import ftrack_utils from avalon.vendor import jsonschema @@ -11,6 +11,12 @@ ValidationError = jsonschema.ValidationError log = Logger.getLogger(__name__) +def get_avalon_database(): + if io._database is None: + io.install() + return io._database + + def get_ca_mongoid(): # returns name of Custom attribute that stores mongo_id return 'avalon_mongo_id' @@ -19,6 +25,8 @@ def get_ca_mongoid(): def import_to_avalon( session, entity, ft_project, av_project, custom_attributes ): + database = get_avalon_database() + project_name = ft_project['full_name'] output = {} errors = [] @@ -46,9 +54,9 @@ def import_to_avalon( # Project //////////////////////////////////////////////////////////////// if entity_type in ['Project']: type = 'project' - name = entity['full_name'] + config = ftrack_utils.get_config(entity) - template = lib.get_avalon_project_template_schema() + schema.validate(config) av_project_code = None if av_project is not None and 'code' in av_project['data']: @@ -56,23 +64,46 @@ def import_to_avalon( ft_project_code = ft_project['name'] if av_project is None: - inventory.save(name, config, template) - av_project = io.find_one({'type': type, 'name': name}) + project_schema = lib.get_avalon_project_template_schema() + item = { + 'schema': project_schema, + 'type': type, + 'name': project_name, + 'data': dict(), + 'config': config, + 'parent': None, + } + schema.validate(item) - elif av_project['name'] != name or av_project_code != ft_project_code: + database[project_name].insert_one(item) + + av_project = database[project_name].find_one( + {'type': type} + ) + + elif ( + av_project['name'] != project_name or + ( + av_project_code is not None and + av_project_code != ft_project_code + ) + ): msg = ( 'You can\'t change {0} "{1}" to "{2}"' ', avalon wouldn\'t work properly!' '\n{0} was changed back!' ) - if av_project['name'] != name: + if av_project['name'] != project_name: entity['full_name'] = av_project['name'] errors.append( {'Changed name error': msg.format( - 'Project name', av_project['name'], name + 'Project name', av_project['name'], project_name )} ) - if av_project_code != ft_project_code: + if ( + av_project_code is not None and + av_project_code != ft_project_code + ): entity['name'] = av_project_code errors.append( {'Changed name error': msg.format( @@ -91,10 +122,10 @@ def import_to_avalon( entity, session, custom_attributes ) - io.update_many( + database[project_name].update_many( {'_id': ObjectId(projectId)}, {'$set': { - 'name': name, + 'name': project_name, 'config': config, 'data': data, }}) @@ -137,9 +168,6 @@ def import_to_avalon( name = entity['name'] - os.environ['AVALON_SILO'] = silo - os.environ['AVALON_ASSET'] = name - avalon_asset = None # existence of this custom attr is already checked if ca_mongoid not in entity['custom_attributes']: @@ -153,14 +181,27 @@ def import_to_avalon( mongo_id = entity['custom_attributes'][ca_mongoid] if mongo_id is not '': - avalon_asset = io.find_one({'_id': ObjectId(mongo_id)}) + avalon_asset = database[project_name].find_one( + {'_id': ObjectId(mongo_id)} + ) if avalon_asset is None: - avalon_asset = io.find_one({'type': 'asset', 'name': name}) + avalon_asset = database[project_name].find_one( + {'type': 'asset', 'name': name} + ) if avalon_asset is None: - mongo_id = inventory.create_asset( - name, silo, data, ObjectId(projectId) - ) + asset_schema = lib.get_avalon_asset_template_schema() + item = { + 'schema': asset_schema, + 'name': name, + 'silo': silo, + 'parent': ObjectId(projectId), + 'type': 'asset', + 'data': data + } + schema.validate(item) + mongo_id = database[project_name].insert_one(item).inserted_id + # Raise error if it seems to be different ent. with same name elif ( avalon_asset['data']['parents'] != data['parents'] or @@ -205,7 +246,9 @@ def import_to_avalon( else: asset_parent_id = avalon_asset['data']['visualParent'] - asset_parent = io.find_one({'_id': ObjectId(asset_parent_id)}) + asset_parent = database[project_name].find_one( + {'_id': ObjectId(asset_parent_id)} + ) ft_parent_id = asset_parent['data']['ftrackId'] try: entity['parent_id'] = ft_parent_id @@ -231,7 +274,7 @@ def import_to_avalon( output['errors'] = errors return output - io.update_many( + database[project_name].update_many( {'_id': ObjectId(mongo_id)}, {'$set': { 'name': name, @@ -280,8 +323,18 @@ def changeability_check_childs(entity): def get_data(entity, session, custom_attributes): + database = get_avalon_database() + entity_type = entity.entity_type + if entity_type.lower() == 'project': + ft_project = entity + elif entity_type.lower() != 'project': + ft_project = entity['project'] + av_project = get_avalon_project(ft_project) + + project_name = ft_project['full_name'] + data = {} data['ftrackId'] = entity['id'] data['entityType'] = entity_type @@ -335,10 +388,16 @@ def get_data(entity, session, custom_attributes): parentId = None for parent in parents: - parentId = io.find_one({'type': 'asset', 'name': parName})['_id'] + parentId = database[project_name]( + {'type': 'asset', 'name': parName} + )['_id'] if parent['parent'].entity_type != 'project' and parentId is None: - parent.importToAvalon(session, parent) - parentId = io.find_one({'type': 'asset', 'name': parName})['_id'] + import_to_avalon( + session, parent, ft_project, av_project, custom_attributes + ) + parentId = database[project_name].find_one( + {'type': 'asset', 'name': parName} + )['_id'] hierarchy = os.path.sep.join(folderStruct) @@ -350,27 +409,25 @@ def get_data(entity, session, custom_attributes): return data -def get_avalon_proj(ft_project): - io.install() - +def get_avalon_project(ft_project): + database = get_avalon_database() + project_name = ft_project['full_name'] ca_mongoid = get_ca_mongoid() if ca_mongoid not in ft_project['custom_attributes']: return None + # try to find by Id project_id = ft_project['custom_attributes'][ca_mongoid] try: - avalon_project = io.find_one({ - "_id": ObjectId(project_id) + avalon_project = database[project_name].find_one({ + '_id': ObjectId(project_id) }) except Exception: avalon_project = None if avalon_project is None: - avalon_project = io.find_one({ - "type": "project", - "name": ft_project["full_name"] + avalon_project = database[project_name].find_one({ + 'type': 'project' }) - io.uninstall() - return avalon_project diff --git a/pype/ftrack/ftrack_utils/ftrack_utils.py b/pype/ftrack/ftrack_utils/ftrack_utils.py index f6a239639d..4398beab8e 100644 --- a/pype/ftrack/ftrack_utils/ftrack_utils.py +++ b/pype/ftrack/ftrack_utils/ftrack_utils.py @@ -45,7 +45,7 @@ def avalon_check_name(entity, inSchema=None): if entity.entity_type in ['Project']: # data['type'] = 'project' name = entity['full_name'] - # schema = get_avalon_project_template_schema()['schema'] + # schema = get_avalon_project_template_schema() # elif entity.entity_type in ['AssetBuild','Library']: # data['silo'] = 'Assets' # else: diff --git a/pype/lib.py b/pype/lib.py index fc68e395bd..17fba41323 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -337,14 +337,17 @@ def get_asset_data(asset=None): return data + def get_avalon_project_config_schema(): schema = 'avalon-core:config-1.0' return schema + def get_avalon_project_template_schema(): - schema = {"schema": "avalon-core:inventory-1.0"} + schema = "avalon-core:inventory-1.0" return schema + def get_avalon_project_template(): from app.api import Templates @@ -364,3 +367,8 @@ def get_avalon_project_template(): proj_template['work'] = "{root}/{project}/{hierarchy}/{asset}/work/{task}" proj_template['publish'] = "{root}/{project}/{hierarchy}/{asset}/publish/{family}/{subset}/v{version}/{projectcode}_{asset}_{subset}_v{version}.{representation}" return proj_template + + +def get_avalon_asset_template_schema(): + schema = "avalon-core:asset-2.0" + return schema From 9d50a5b8b76fab433d17e2efd1ec71ab56bfc5e8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 17 Jan 2019 16:06:34 +0100 Subject: [PATCH 13/14] more io usage replaced, added ignore_me variable to actions/events for ftrack server, added 'get avalon projects' function, fixed few bugs, added titles to show_interface in action handlers --- pype/ftrack/actions/action_Apps.py | 23 ++--- .../actions/action_sync_to_avalon_local.py | 5 +- pype/ftrack/actions/ft_utils.py | 2 + pype/ftrack/actions/ftrack_action_handler.py | 71 ++++++++------- pype/ftrack/events/action_sync_to_avalon.py | 5 +- pype/ftrack/events/event_sync_to_avalon.py | 8 +- pype/ftrack/events/ftrack_event_handler.py | 5 +- pype/ftrack/ftrack_run.py | 91 +++++++++++++------ pype/ftrack/ftrack_utils/avalon_sync.py | 15 ++- pype/lib.py | 21 +++++ 10 files changed, 153 insertions(+), 93 deletions(-) diff --git a/pype/ftrack/actions/action_Apps.py b/pype/ftrack/actions/action_Apps.py index 3f6e8b870b..fe7664470a 100644 --- a/pype/ftrack/actions/action_Apps.py +++ b/pype/ftrack/actions/action_Apps.py @@ -1,8 +1,9 @@ -import os import toml +import time from ftrack_action_handler import AppAction -from avalon import io, lib +from avalon import lib from app.api import Logger +from pype import lib as pypelib log = Logger.getLogger(__name__) @@ -28,8 +29,11 @@ def registerApp(app, session): apptoml = toml.load(abspath) + ''' REQUIRED ''' executable = apptoml['executable'] - label = apptoml.get('ftrack_label', app['label']) + + ''' OPTIONAL ''' + label = apptoml.get('ftrack_label', app.get('label', name)) icon = apptoml.get('ftrack_icon', None) description = apptoml.get('description', None) @@ -40,17 +44,7 @@ def registerApp(app, session): def register(session): - # set avalon environ - they just must exist - os.environ['AVALON_PROJECT'] = '' - os.environ['AVALON_ASSET'] = '' - os.environ['AVALON_SILO'] = '' - # Get all projects from Avalon DB - try: - io.install() - projects = sorted(io.projects(), key=lambda x: x['name']) - io.uninstall() - except Exception as e: - log.error(e) + projects = pypelib.get_all_avalon_projects() apps = [] appNames = [] @@ -65,5 +59,6 @@ def register(session): for app in apps: try: registerApp(app, session) + time.sleep(0.05) except Exception as e: log.warning("'{0}' - not proper App ({1})".format(app['name'], e)) diff --git a/pype/ftrack/actions/action_sync_to_avalon_local.py b/pype/ftrack/actions/action_sync_to_avalon_local.py index 85978c0d6c..347172acb1 100644 --- a/pype/ftrack/actions/action_sync_to_avalon_local.py +++ b/pype/ftrack/actions/action_sync_to_avalon_local.py @@ -147,7 +147,6 @@ class SyncToAvalon(BaseAction): ) if 'errors' in result and len(result['errors']) > 0: - print('error') items = [] for error in result['errors']: for key, message in error.items(): @@ -162,10 +161,12 @@ class SyncToAvalon(BaseAction): self.log.error( '{}: {}'.format(key, message) ) + title = 'Hey You! Few Errors were raised! (*look below*)' job['status'] = 'failed' session.commit() - self.show_interface(event, items) + + self.show_interface(event, items, title) return { 'success': False, 'message': "Sync to avalon FAILED" diff --git a/pype/ftrack/actions/ft_utils.py b/pype/ftrack/actions/ft_utils.py index 4b30253f9d..f6f9bfc59b 100644 --- a/pype/ftrack/actions/ft_utils.py +++ b/pype/ftrack/actions/ft_utils.py @@ -6,6 +6,8 @@ import sys import json import base64 + +ignore_me = True # sys.path.append(os.path.dirname(os.path.dirname(__file__))) # from ftrack_kredenc.lucidity.vendor import yaml # from ftrack_kredenc import lucidity diff --git a/pype/ftrack/actions/ftrack_action_handler.py b/pype/ftrack/actions/ftrack_action_handler.py index 56af11b557..b47ae24435 100644 --- a/pype/ftrack/actions/ftrack_action_handler.py +++ b/pype/ftrack/actions/ftrack_action_handler.py @@ -4,12 +4,16 @@ import os import sys import platform import ftrack_api -from avalon import io, lib +from avalon import lib import acre +from pype.ftrack import ftrack_utils from pype import api as pype +ignore_me = True + + class AppAction(object): '''Custom Action base class @@ -122,33 +126,20 @@ class AppAction(object): entity = session.get(entity_type, entity_id) # TODO Should return False if not TASK ?!!! - if entity.entity_type != 'Task': - return False - # TODO Should return False if more than one entity is selected ?!!! - if len(entities) > 1: + if ( + len(entities) > 1 or + entity.entity_type.lower() != 'task' + ): return False - ft_project = entity - if (entity.entity_type != 'Project'): - ft_project = entity['project'] + ft_project = entity['project'] - silo = "" - if 'ancestors' in entity: - for ancestor in entity['ancestors']: - silo = ancestor['name'] - break - - os.environ['AVALON_PROJECT'] = ft_project['full_name'] - os.environ['AVALON_ASSET'] = entity['name'] - os.environ['AVALON_SILO'] = silo - - io.install() - avalon_project = io.find_one({ - "type": "project", - "name": ft_project['full_name'] + database = ftrack_utils.get_avalon_database() + project_name = ft_project['full_name'] + avalon_project = database[project_name].find_one({ + "type": "project" }) - io.uninstall() if avalon_project is None: return False @@ -249,24 +240,37 @@ class AppAction(object): entity, id = entities[0] entity = session.get(entity, id) + project_name = entity['project']['full_name'] + + database = ftrack_utils.get_avalon_database() + + # Get current environments + env_list = [ + 'AVALON_PROJECT', + 'AVALON_SILO', + 'AVALON_ASSET', + 'AVALON_TASK', + 'AVALON_APP', + 'AVALON_APP_NAME' + ] + env_origin = {} + for env in env_list: + env_origin[env] = os.environ.get(env, None) # set environments for Avalon - os.environ["AVALON_PROJECT"] = entity['project']['full_name'] + os.environ["AVALON_PROJECT"] = project_name os.environ["AVALON_SILO"] = entity['ancestors'][0]['name'] os.environ["AVALON_ASSET"] = entity['parent']['name'] os.environ["AVALON_TASK"] = entity['name'] os.environ["AVALON_APP"] = self.identifier.split("_")[0] os.environ["AVALON_APP_NAME"] = self.identifier - os.environ["FTRACK_TASKID"] = id - anatomy = pype.Anatomy - io.install() - hierarchy = io.find_one({ + hierarchy = database[project_name].find_one({ "type": 'asset', "name": entity['parent']['name'] })['data']['parents'] - io.uninstall() + if hierarchy: hierarchy = os.path.join(*hierarchy) @@ -384,6 +388,10 @@ class AppAction(object): self.log.info('Starting timer for task: ' + task['name']) user.start_timer(task, force=True) + # Set origin avalon environments + for key, value in env_origin.items(): + os.environ[key] = value + return { 'success': True, 'message': "Launching {0}".format(self.label) @@ -729,7 +737,7 @@ class BaseAction(object): return result - def show_interface(self, event, items): + def show_interface(self, event, items, title=''): """ Shows interface to user who triggered event - 'items' must be list containing Ftrack interface items @@ -744,7 +752,8 @@ class BaseAction(object): topic='ftrack.action.trigger-user-interface', data=dict( type='widget', - items=items + items=items, + title=title ), target=target ), diff --git a/pype/ftrack/events/action_sync_to_avalon.py b/pype/ftrack/events/action_sync_to_avalon.py index 92a22695e1..0e55c1ed70 100644 --- a/pype/ftrack/events/action_sync_to_avalon.py +++ b/pype/ftrack/events/action_sync_to_avalon.py @@ -158,7 +158,6 @@ class Sync_To_Avalon(BaseAction): ) if 'errors' in result and len(result['errors']) > 0: - print('error') items = [] for error in result['errors']: for key, message in error.items(): @@ -173,10 +172,12 @@ class Sync_To_Avalon(BaseAction): self.log.error( '{}: {}'.format(key, message) ) + title = 'Hey You! Few Errors were raised! (*look below*)' job['status'] = 'failed' session.commit() - self.show_interface(event, items) + + self.show_interface(event, items, title) return { 'success': False, 'message': "Sync to avalon FAILED" diff --git a/pype/ftrack/events/event_sync_to_avalon.py b/pype/ftrack/events/event_sync_to_avalon.py index 8ecd860644..25b8be4359 100644 --- a/pype/ftrack/events/event_sync_to_avalon.py +++ b/pype/ftrack/events/event_sync_to_avalon.py @@ -86,7 +86,6 @@ class Sync_to_Avalon(BaseEvent): custom_attributes=custom_attributes ) if 'errors' in result and len(result['errors']) > 0: - print('error') items = [] for error in result['errors']: for key, message in error.items(): @@ -101,9 +100,9 @@ class Sync_to_Avalon(BaseEvent): self.log.error( '{}: {}'.format(key, message) ) - session.commit() - self.show_interface(event, items) + title = 'Hey You! You raised few Errors! (*look below*)' + self.show_interface(event, items, title) return if avalon_project is None: @@ -122,7 +121,8 @@ class Sync_to_Avalon(BaseEvent): 'name': 'error', 'value': ftrack_message }] - self.show_interface(event, items) + title = 'Hey You! Unknown Error has been raised! (*look below*)' + self.show_interface(event, items, title) self.log.error(message) return diff --git a/pype/ftrack/events/ftrack_event_handler.py b/pype/ftrack/events/ftrack_event_handler.py index 0cb53b74a9..7011a57ed7 100644 --- a/pype/ftrack/events/ftrack_event_handler.py +++ b/pype/ftrack/events/ftrack_event_handler.py @@ -140,7 +140,7 @@ class BaseEvent(object): on_error='ignore' ) - def show_interface(self, event, items): + def show_interface(self, event, items, title=''): """ Shows interface to user who triggered event - 'items' must be list containing Ftrack interface items @@ -153,7 +153,8 @@ class BaseEvent(object): topic='ftrack.action.trigger-user-interface', data=dict( type='widget', - items=items + items=items, + title=title ), target=target ), diff --git a/pype/ftrack/ftrack_run.py b/pype/ftrack/ftrack_run.py index db42c501a7..2a40419cb7 100644 --- a/pype/ftrack/ftrack_run.py +++ b/pype/ftrack/ftrack_run.py @@ -1,4 +1,3 @@ -import sys import os import json import threading @@ -6,6 +5,7 @@ import time import ftrack_api from app import style from app.vendor.Qt import QtCore, QtGui, QtWidgets + from pype.ftrack import credentials, login_dialog as login_dialog from pype.vendor.pynput import mouse, keyboard @@ -21,7 +21,6 @@ log = pype.Logger.getLogger(__name__, "ftrack") class FtrackRunner: - def __init__(self, main_parent=None, parent=None): self.parent = parent @@ -86,7 +85,9 @@ class FtrackRunner: # Actions part def start_action_server(self): if self.thread_action_server is None: - self.thread_action_server = threading.Thread(target=self.set_action_server) + self.thread_action_server = threading.Thread( + target=self.set_action_server + ) self.thread_action_server.daemon = True self.thread_action_server.start() @@ -95,7 +96,14 @@ class FtrackRunner: self.set_menu_visibility() def set_action_server(self): - self.action_server.run_server() + try: + self.action_server.run_server() + except Exception: + msg = 'Ftrack Action server crashed! Please try to start again.' + log.error(msg) + # TODO show message to user + self.bool_action_server = False + self.set_menu_visibility() def reset_action_server(self): self.stop_action_server() @@ -123,11 +131,19 @@ class FtrackRunner: # Actions - server self.smActionS = self.menu.addMenu("Action server") - self.aRunActionS = QtWidgets.QAction("Run action server", self.smActionS) + + self.aRunActionS = QtWidgets.QAction( + "Run action server", self.smActionS + ) + self.aResetActionS = QtWidgets.QAction( + "Reset action server", self.smActionS + ) + self.aStopActionS = QtWidgets.QAction( + "Stop action server", self.smActionS + ) + self.aRunActionS.triggered.connect(self.start_action_server) - self.aResetActionS = QtWidgets.QAction("Reset action server", self.smActionS) self.aResetActionS.triggered.connect(self.reset_action_server) - self.aStopActionS = QtWidgets.QAction("Stop action server", self.smActionS) self.aStopActionS.triggered.connect(self.stop_action_server) self.smActionS.addAction(self.aRunActionS) @@ -168,12 +184,19 @@ class FtrackRunner: self.start_timer_thread() def start_timer_thread(self): - if self.thread_timer is None: - self.thread_timer = FtrackEventsThread(self) - self.bool_timer_event = True - self.thread_timer.signal_timer_started.connect(self.timer_started) - self.thread_timer.signal_timer_stopped.connect(self.timer_stopped) - self.thread_timer.start() + try: + if self.thread_timer is None: + self.thread_timer = FtrackEventsThread(self) + self.bool_timer_event = True + self.thread_timer.signal_timer_started.connect( + self.timer_started + ) + self.thread_timer.signal_timer_stopped.connect( + self.timer_stopped + ) + self.thread_timer.start() + except Exception: + pass def stop_timer_thread(self): try: @@ -188,9 +211,15 @@ class FtrackRunner: def start_countdown_thread(self): if self.thread_timer_coundown is None: self.thread_timer_coundown = CountdownThread(self) - self.thread_timer_coundown.signal_show_question.connect(self.show_widget_timer) - self.thread_timer_coundown.signal_send_time.connect(self.change_count_widget) - self.thread_timer_coundown.signal_stop_timer.connect(self.timer_stop) + self.thread_timer_coundown.signal_show_question.connect( + self.show_widget_timer + ) + self.thread_timer_coundown.signal_send_time.connect( + self.change_count_widget + ) + self.thread_timer_coundown.signal_stop_timer.connect( + self.timer_stop + ) self.thread_timer_coundown.start() def stop_countdown_thread(self): @@ -255,7 +284,9 @@ class FtrackEventsThread(QtCore.QThread): def run(self): self.timer_session = ftrack_api.Session(auto_connect_event_hub=True) self.timer_session.event_hub.subscribe( - 'topic=ftrack.update and source.user.username={}'.format(self.username), + 'topic=ftrack.update and source.user.username={}'.format( + self.username + ), self.event_handler) user_query = 'User where username is "{}"'.format(self.username) @@ -273,7 +304,7 @@ class FtrackEventsThread(QtCore.QThread): try: if event['data']['entities'][0]['objectTypeId'] != 'timer': return - except: + except Exception: return new = event['data']['entities'][0]['changes']['start']['new'] @@ -301,12 +332,6 @@ class FtrackEventsThread(QtCore.QThread): def ftrack_restart_timer(self): try: - last_task = None - if "FTRACK_LAST_TASK_ID" in os.environ: - task_id = os.environ["FTRACK_LAST_TASK_ID"] - query = 'Task where id is {}'.format(task_id) - last_task = self.timer_session.query(query).one() - if (self.last_task is not None) and (self.user is not None): self.user.start_timer(self.last_task) self.timer_session.commit() @@ -386,7 +411,11 @@ class CountdownThread(QtCore.QThread): json_dict = json.load(data_file) data = json_dict['timer'] except Exception as e: - msg = 'Loading "Ftrack Config file" Failed. Please check log for more information. Times are set to default.' + msg = ( + 'Loading "Ftrack Config file" Failed.' + ' Please check log for more information.' + ' Times are set to default.' + ) log.warning("{} - {}".format(msg, str(e))) data = self.validate_timer_values(data) @@ -485,15 +514,17 @@ class StopTimer(QtWidgets.QWidget): def _main(self): self.main = QtWidgets.QVBoxLayout() - self.main.setObjectName("main") + self.main.setObjectName('main') self.form = QtWidgets.QFormLayout() self.form.setContentsMargins(10, 15, 10, 5) - self.form.setObjectName("form") + self.form.setObjectName('form') - msg_info = "You didn't work for a long time." - msg_question = "Would you like to stop Ftrack timer?" - msg_stopped = "Your Ftrack timer was stopped. Do you want to start again?" + msg_info = 'You didn\'t work for a long time.' + msg_question = 'Would you like to stop Ftrack timer?' + msg_stopped = ( + 'Your Ftrack timer was stopped. Do you want to start again?' + ) self.lbl_info = QtWidgets.QLabel(msg_info) self.lbl_info.setFont(self.font) diff --git a/pype/ftrack/ftrack_utils/avalon_sync.py b/pype/ftrack/ftrack_utils/avalon_sync.py index c268d72295..648c119b2f 100644 --- a/pype/ftrack/ftrack_utils/avalon_sync.py +++ b/pype/ftrack/ftrack_utils/avalon_sync.py @@ -1,7 +1,8 @@ import os import re from pype import lib -from avalon import io, schema +from pype.lib import get_avalon_database +from avalon import schema from bson.objectid import ObjectId from pype.ftrack.ftrack_utils import ftrack_utils from avalon.vendor import jsonschema @@ -11,12 +12,6 @@ ValidationError = jsonschema.ValidationError log = Logger.getLogger(__name__) -def get_avalon_database(): - if io._database is None: - io.install() - return io._database - - def get_ca_mongoid(): # returns name of Custom attribute that stores mongo_id return 'avalon_mongo_id' @@ -213,6 +208,10 @@ def import_to_avalon( errors.append({'Entity name duplication': msg}) output['errors'] = errors return output + + # Store new ID (in case that asset was removed from DB) + else: + mongo_id = avalon_asset['_id'] else: if avalon_asset['name'] != entity['name']: if silo is None or changeability_check_childs(entity) is False: @@ -388,7 +387,7 @@ def get_data(entity, session, custom_attributes): parentId = None for parent in parents: - parentId = database[project_name]( + parentId = database[project_name].find_one( {'type': 'asset', 'name': parName} )['_id'] if parent['parent'].entity_type != 'project' and parentId is None: diff --git a/pype/lib.py b/pype/lib.py index 17fba41323..781e665224 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -372,3 +372,24 @@ def get_avalon_project_template(): def get_avalon_asset_template_schema(): schema = "avalon-core:asset-2.0" return schema + + +def get_avalon_database(): + if io._database is None: + project = os.environ.get('AVALON_PROJECT', '') + asset = os.environ.get('AVALON_ASSET', '') + silo = os.environ.get('AVALON_SILO', '') + os.environ['AVALON_PROJECT'] = project + os.environ['AVALON_ASSET'] = asset + os.environ['AVALON_SILO'] = silo + io.install() + return io._database + + +def get_all_avalon_projects(): + db = get_avalon_database() + project_names = db.collection_names() + projects = [] + for name in project_names: + projects.append(db[name].find_one({'type': 'project'})) + return projects From 681a70bf9cc2c0160bdf7ceecd53f4cc05d937fd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 18 Jan 2019 15:19:52 +0100 Subject: [PATCH 14/14] changed project schema name --- pype/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/lib.py b/pype/lib.py index 781e665224..da4503aa8c 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -344,7 +344,7 @@ def get_avalon_project_config_schema(): def get_avalon_project_template_schema(): - schema = "avalon-core:inventory-1.0" + schema = "avalon-core:project-2.0" return schema