From 569cb0a8c34711b2722df9ec65056ac7ef985d49 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 22 Jun 2020 13:26:12 +0100 Subject: [PATCH 001/190] Publish review --- .../photoshop/publish/collect_review.py | 36 ++++++ .../photoshop/publish/extract_review.py | 103 ++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 pype/plugins/photoshop/publish/collect_review.py create mode 100644 pype/plugins/photoshop/publish/extract_review.py diff --git a/pype/plugins/photoshop/publish/collect_review.py b/pype/plugins/photoshop/publish/collect_review.py new file mode 100644 index 0000000000..30042d188b --- /dev/null +++ b/pype/plugins/photoshop/publish/collect_review.py @@ -0,0 +1,36 @@ +import os + +import pythoncom + +import pyblish.api + + +class CollectReview(pyblish.api.ContextPlugin): + """Gather the active document as review instance.""" + + label = "Review" + order = pyblish.api.CollectorOrder + hosts = ["photoshop"] + + def process(self, context): + # Necessary call when running in a different thread which pyblish-qml + # can be. + pythoncom.CoInitialize() + + family = "review" + task = os.getenv("AVALON_TASK", None) + subset = family + task.capitalize() + + file_path = context.data["currentFile"] + base_name = os.path.basename(file_path) + + instance = context.create_instance(subset) + instance.data.update({ + "subset": subset, + "label": base_name, + "name": base_name, + "family": family, + "families": ["ftrack"], + "representations": [], + "asset": os.environ["AVALON_ASSET"] + }) diff --git a/pype/plugins/photoshop/publish/extract_review.py b/pype/plugins/photoshop/publish/extract_review.py new file mode 100644 index 0000000000..607e039d14 --- /dev/null +++ b/pype/plugins/photoshop/publish/extract_review.py @@ -0,0 +1,103 @@ +import os + +import pype.api +import pype.lib +from avalon import photoshop + + +class ExtractReview(pype.api.Extractor): + """Produce a flattened image file from all instances.""" + + label = "Extract Review" + hosts = ["photoshop"] + families = ["review"] + + def process(self, instance): + + staging_dir = self.staging_dir(instance) + self.log.info("Outputting image to {}".format(staging_dir)) + + layers = [] + for image_instance in instance.context: + if image_instance.data["family"] != "image": + continue + layers.append(image_instance[0]) + + # Perform extraction + output_image = "{} copy.jpg".format( + os.path.splitext(photoshop.app().ActiveDocument.Name)[0] + ) + with photoshop.maintained_visibility(): + # Hide all other layers. + extract_ids = [ + x.id for x in photoshop.get_layers_in_layers(layers) + ] + for layer in photoshop.get_layers_in_document(): + if layer.id in extract_ids: + layer.Visible = True + else: + layer.Visible = False + + photoshop.app().ActiveDocument.SaveAs( + staging_dir, photoshop.com_objects.JPEGSaveOptions(), True + ) + + instance.data["representations"].append({ + "name": "jpg", + "ext": "jpg", + "files": output_image, + "stagingDir": staging_dir + }) + instance.data["stagingDir"] = staging_dir + + # Generate thumbnail. + thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg") + args = [ + "ffmpeg", "-y", + "-i", os.path.join(staging_dir, output_image), + "-vf", "scale=300:-1", + "-vframes", "1", + thumbnail_path + ] + output = pype.lib._subprocess(args) + + self.log.debug(output) + + instance.data["representations"].append({ + "name": "thumbnail", + "ext": "jpg", + "files": os.path.basename(thumbnail_path), + "stagingDir": staging_dir, + "tags": ["thumbnail"] + }) + + # Generate mov. + mov_path = os.path.join(staging_dir, "review.mov") + args = [ + "ffmpeg", "-y", + "-i", os.path.join(staging_dir, output_image), + "-vframes", "1", + mov_path + ] + output = pype.lib._subprocess(args) + + self.log.debug(output) + + instance.data["representations"].append({ + "name": "mov", + "ext": "mov", + "files": os.path.basename(mov_path), + "stagingDir": staging_dir, + "frameStart": 1, + "frameEnd": 2, + "fps": 25, + "preview": True, + "tags": ["review", "ftrackreview"] + }) + + # Required for extract_review plugin (L222 onwards). + instance.data["frameStart"] = 1 + instance.data["frameEnd"] = 2 + instance.data["fps"] = 25 + + self.log.info(f"Extracted {instance} to {staging_dir}") From 85a298e4f5867a7bbd7598679496735dd1200f2b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 23 Jun 2020 12:30:20 +0100 Subject: [PATCH 002/190] Flag Outdated containers - on startup as message box appears, and outdated containers are coloured red. - on publish the "Validate Containers" errors. - loaded image containers are coloured green. --- pype/hosts/harmony/__init__.py | 100 +++++++++++- .../global/publish/validate_containers.py | 2 +- .../harmony/load/load_imagesequence.py | 147 ++++++++++++++---- 3 files changed, 217 insertions(+), 32 deletions(-) diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index 169786204e..628397e777 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -1,8 +1,9 @@ import os import sys -from avalon import api, harmony +from avalon import api, io, harmony from avalon.vendor import Qt +import avalon.tools.sceneinventory import pyblish.api from pype import lib @@ -92,6 +93,101 @@ def ensure_scene_settings(): set_scene_settings(valid_settings) +def check_inventory(): + if not lib.any_outdated(): + return + + host = avalon.api.registered_host() + outdated_containers = [] + for container in host.ls(): + representation = container['representation'] + representation_doc = io.find_one( + { + "_id": io.ObjectId(representation), + "type": "representation" + }, + projection={"parent": True} + ) + if representation_doc and not lib.is_latest(representation_doc): + outdated_containers.append(container) + + # Colour nodes. + func = """function func(args){ + for( var i =0; i <= args[0].length - 1; ++i) + { + var red_color = new ColorRGBA(255, 0, 0, 255); + node.setColor(args[0][i], red_color); + } + } + func + """ + outdated_nodes = [x["node"] for x in outdated_containers] + harmony.send({"function": func, "args": [outdated_nodes]}) + + # Warn about outdated containers. + print("Starting new QApplication..") + app = Qt.QtWidgets.QApplication(sys.argv) + + message_box = Qt.QtWidgets.QMessageBox() + message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning) + msg = "There are outdated containers in the scene." + message_box.setText(msg) + message_box.exec_() + + # Garbage collect QApplication. + del app + + +def application_launch(): + ensure_scene_settings() + check_inventory() + + +def export_template(backdrops, nodes, filepath): + func = """function func(args) + { + // Add an extra node just so a new group can be created. + var temp_node = node.add("Top", "temp_note", "NOTE", 0, 0, 0); + var template_group = node.createGroup(temp_node, "temp_group"); + node.deleteNode( template_group + "/temp_note" ); + + // This will make Node View to focus on the new group. + selection.clearSelection(); + selection.addNodeToSelection(template_group); + Action.perform("onActionEnterGroup()", "Node View"); + + // Recreate backdrops in group. + for (var i = 0 ; i < args[0].length; i++) + { + Backdrop.addBackdrop(template_group, args[0][i]); + }; + + // Copy-paste the selected nodes into the new group. + var drag_object = copyPaste.copy(args[1], 1, frame.numberOf, ""); + copyPaste.pasteNewNodes(drag_object, template_group, ""); + + // Select all nodes within group and export as template. + Action.perform( "selectAll()", "Node View" ); + copyPaste.createTemplateFromSelection(args[2], args[3]); + + // Unfocus the group in Node view, delete all nodes and backdrops + // created during the process. + Action.perform("onActionUpToParent()", "Node View"); + node.deleteNode(template_group, true, true); + } + func + """ + harmony.send({ + "function": func, + "args": [ + backdrops, + nodes, + os.path.basename(filepath), + os.path.dirname(filepath) + ] + }) + + def install(): print("Installing Pype config...") @@ -116,7 +212,7 @@ def install(): "instanceToggled", on_pyblish_instance_toggled ) - api.on("application.launched", ensure_scene_settings) + api.on("application.launched", application_launch) def on_pyblish_instance_toggled(instance, old_value, new_value): diff --git a/pype/plugins/global/publish/validate_containers.py b/pype/plugins/global/publish/validate_containers.py index 44cb5def3c..2c7b763f7a 100644 --- a/pype/plugins/global/publish/validate_containers.py +++ b/pype/plugins/global/publish/validate_containers.py @@ -19,7 +19,7 @@ class ValidateContainers(pyblish.api.ContextPlugin): label = "Validate Containers" order = pyblish.api.ValidatorOrder - hosts = ["maya", "houdini", "nuke"] + hosts = ["maya", "houdini", "nuke", "harmony"] optional = True actions = [ShowInventory] diff --git a/pype/plugins/harmony/load/load_imagesequence.py b/pype/plugins/harmony/load/load_imagesequence.py index 7862e027af..b56dba03d4 100644 --- a/pype/plugins/harmony/load/load_imagesequence.py +++ b/pype/plugins/harmony/load/load_imagesequence.py @@ -98,33 +98,63 @@ function import_files(args) transparencyModeAttr.setValue(SGITransparencyMode); if (extension == "psd") transparencyModeAttr.setValue(FlatPSDTransparencyMode); + if (extension == "jpg") + transparencyModeAttr.setValue(LayeredPSDTransparencyMode); node.linkAttr(read, "DRAWING.ELEMENT", uniqueColumnName); - // Create a drawing for each file. - for( var i =0; i <= files.length - 1; ++i) + if (files.length == 1) { - timing = start_frame + i // Create a drawing drawing, 'true' indicate that the file exists. - Drawing.create(elemId, timing, true); + Drawing.create(elemId, 1, true); // Get the actual path, in tmp folder. - var drawingFilePath = Drawing.filename(elemId, timing.toString()); - copyFile( files[i], drawingFilePath ); + var drawingFilePath = Drawing.filename(elemId, "1"); + copyFile(files[0], drawingFilePath); + // Expose the image for the entire frame range. + for( var i =0; i <= frame.numberOf() - 1; ++i) + { + timing = start_frame + i + column.setEntry(uniqueColumnName, 1, timing, "1"); + } + } else { + // Create a drawing for each file. + for( var i =0; i <= files.length - 1; ++i) + { + timing = start_frame + i + // Create a drawing drawing, 'true' indicate that the file exists. + Drawing.create(elemId, timing, true); + // Get the actual path, in tmp folder. + var drawingFilePath = Drawing.filename(elemId, timing.toString()); + copyFile( files[i], drawingFilePath ); - column.setEntry(uniqueColumnName, 1, timing, timing.toString()); + column.setEntry(uniqueColumnName, 1, timing, timing.toString()); + } } + + var green_color = new ColorRGBA(0, 255, 0, 255); + node.setColor(read, green_color); + return read; } import_files """ -replace_files = """function replace_files(args) +replace_files = """var PNGTransparencyMode = 0; //Premultiplied wih Black +var TGATransparencyMode = 0; //Premultiplied wih Black +var SGITransparencyMode = 0; //Premultiplied wih Black +var LayeredPSDTransparencyMode = 1; //Straight +var FlatPSDTransparencyMode = 2; //Premultiplied wih White + +function replace_files(args) { var files = args[0]; + MessageLog.trace(files); + MessageLog.trace(files.length); var _node = args[1]; var start_frame = args[2]; var _column = node.linkedColumn(_node, "DRAWING.ELEMENT"); + var elemId = column.getElementIdOfDrawing(_column); // Delete existing drawings. var timings = column.getDrawingTimings(_column); @@ -133,20 +163,62 @@ replace_files = """function replace_files(args) column.deleteDrawingAt(_column, parseInt(timings[i])); } - // Create new drawings. - for( var i =0; i <= files.length - 1; ++i) - { - timing = start_frame + i - // Create a drawing drawing, 'true' indicate that the file exists. - Drawing.create(node.getElementId(_node), timing, true); - // Get the actual path, in tmp folder. - var drawingFilePath = Drawing.filename( - node.getElementId(_node), timing.toString() - ); - copyFile( files[i], drawingFilePath ); - column.setEntry(_column, 1, timing, timing.toString()); + var filename = files[0]; + var pos = filename.lastIndexOf("."); + if( pos < 0 ) + return null; + var extension = filename.substr(pos+1).toLowerCase(); + + if(extension == "jpeg") + extension = "jpg"; + + var transparencyModeAttr = node.getAttr( + _node, frame.current(), "applyMatteToColor" + ); + if (extension == "png") + transparencyModeAttr.setValue(PNGTransparencyMode); + if (extension == "tga") + transparencyModeAttr.setValue(TGATransparencyMode); + if (extension == "sgi") + transparencyModeAttr.setValue(SGITransparencyMode); + if (extension == "psd") + transparencyModeAttr.setValue(FlatPSDTransparencyMode); + if (extension == "jpg") + transparencyModeAttr.setValue(LayeredPSDTransparencyMode); + + if (files.length == 1) + { + // Create a drawing drawing, 'true' indicate that the file exists. + Drawing.create(elemId, 1, true); + // Get the actual path, in tmp folder. + var drawingFilePath = Drawing.filename(elemId, "1"); + copyFile(files[0], drawingFilePath); + MessageLog.trace(files[0]); + MessageLog.trace(drawingFilePath); + // Expose the image for the entire frame range. + for( var i =0; i <= frame.numberOf() - 1; ++i) + { + timing = start_frame + i + column.setEntry(_column, 1, timing, "1"); + } + } else { + // Create a drawing for each file. + for( var i =0; i <= files.length - 1; ++i) + { + timing = start_frame + i + // Create a drawing drawing, 'true' indicate that the file exists. + Drawing.create(elemId, timing, true); + // Get the actual path, in tmp folder. + var drawingFilePath = Drawing.filename(elemId, timing.toString()); + copyFile( files[i], drawingFilePath ); + + column.setEntry(_column, 1, timing, timing.toString()); + } } + + var green_color = new ColorRGBA(0, 255, 0, 255); + node.setColor(_node, green_color); } replace_files """ @@ -156,8 +228,8 @@ class ImageSequenceLoader(api.Loader): """Load images Stores the imported asset in a container named after the asset. """ - families = ["shot", "render"] - representations = ["jpeg", "png"] + families = ["shot", "render", "image"] + representations = ["jpeg", "png", "jpg"] def load(self, context, name=None, namespace=None, data=None): @@ -165,9 +237,18 @@ class ImageSequenceLoader(api.Loader): os.listdir(os.path.dirname(self.fname)) ) files = [] - for f in list(collections[0]): + if collections: + for f in list(collections[0]): + files.append( + os.path.join( + os.path.dirname(self.fname), f + ).replace("\\", "/") + ) + else: files.append( - os.path.join(os.path.dirname(self.fname), f).replace("\\", "/") + os.path.join( + os.path.dirname(self.fname), remainder[0] + ).replace("\\", "/") ) read_node = harmony.send( @@ -190,15 +271,23 @@ class ImageSequenceLoader(api.Loader): def update(self, container, representation): node = container.pop("node") + path = api.get_representation_path(representation) collections, remainder = clique.assemble( - os.listdir( - os.path.dirname(api.get_representation_path(representation)) - ) + os.listdir(os.path.dirname(path)) ) files = [] - for f in list(collections[0]): + if collections: + for f in list(collections[0]): + files.append( + os.path.join( + os.path.dirname(path), f + ).replace("\\", "/") + ) + else: files.append( - os.path.join(os.path.dirname(self.fname), f).replace("\\", "/") + os.path.join( + os.path.dirname(path), remainder[0] + ).replace("\\", "/") ) harmony.send( From bcd81fa934bcf13ca5861f212cb6871fa439e78a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 Jun 2020 12:52:35 +0100 Subject: [PATCH 003/190] Implemented 'camera' asset for Blender --- pype/plugins/blender/create/create_camera.py | 32 +++ pype/plugins/blender/load/load_camera.py | 241 ++++++++++++++++++ pype/plugins/blender/publish/extract_blend.py | 2 +- 3 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 pype/plugins/blender/create/create_camera.py create mode 100644 pype/plugins/blender/load/load_camera.py diff --git a/pype/plugins/blender/create/create_camera.py b/pype/plugins/blender/create/create_camera.py new file mode 100644 index 0000000000..5817985053 --- /dev/null +++ b/pype/plugins/blender/create/create_camera.py @@ -0,0 +1,32 @@ +"""Create a camera asset.""" + +import bpy + +from avalon import api +from avalon.blender import Creator, lib +import pype.hosts.blender.plugin + + +class CreateCamera(Creator): + """Polygonal static geometry""" + + name = "cameraMain" + label = "Camera" + family = "camera" + icon = "video-camera" + + def process(self): + + asset = self.data["asset"] + subset = self.data["subset"] + name = pype.hosts.blender.plugin.asset_name(asset, subset) + collection = bpy.data.collections.new(name=name) + bpy.context.scene.collection.children.link(collection) + self.data['task'] = api.Session.get('AVALON_TASK') + lib.imprint(collection, self.data) + + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): + collection.objects.link(obj) + + return collection diff --git a/pype/plugins/blender/load/load_camera.py b/pype/plugins/blender/load/load_camera.py new file mode 100644 index 0000000000..a69c01e806 --- /dev/null +++ b/pype/plugins/blender/load/load_camera.py @@ -0,0 +1,241 @@ +"""Load a camera asset in Blender.""" + +import logging +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +from avalon import api, blender +import bpy +import pype.hosts.blender.plugin + +logger = logging.getLogger("pype").getChild("blender").getChild("load_camera") + + +class BlendCameraLoader(pype.hosts.blender.plugin.AssetLoader): + """Load a camera from a .blend file. + + Warning: + Loading the same asset more then once is not properly supported at the + moment. + """ + + families = ["camera"] + representations = ["blend"] + + label = "Link Camera" + icon = "code-fork" + color = "orange" + + def _remove(self, objects, lib_container): + + for obj in objects: + bpy.data.cameras.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + def _process(self, libpath, lib_container, container_name, actions): + + relative = bpy.context.preferences.filepaths.use_relative_paths + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + camera_container = scene.collection.children[lib_container].make_local() + + objects_list = [] + + for obj in camera_container.objects: + obj = obj.make_local() + obj.data.make_local() + + if not obj.get(blender.pipeline.AVALON_PROPERTY): + obj[blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + + + if actions[0] is not None: + if obj.animation_data is None: + obj.animation_data_create() + obj.animation_data.action = actions[0] + + if actions[1] is not None: + if obj.data.animation_data is None: + obj.data.animation_data_create() + obj.data.animation_data.action = actions[1] + + objects_list.append(obj) + + camera_container.pop(blender.pipeline.AVALON_PROPERTY) + + bpy.ops.object.select_all(action='DESELECT') + + return objects_list + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.hosts.blender.plugin.asset_name(asset, subset) + container_name = pype.hosts.blender.plugin.asset_name( + asset, subset, namespace + ) + + container = bpy.data.collections.new(lib_container) + container.name = container_name + blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + container_metadata = container.get( + blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + objects_list = self._process( + libpath, lib_container, container_name, (None, None)) + + # Save the list of objects in the metadata container + container_metadata["objects"] = objects_list + + nodes = list(container.objects) + nodes.append(container) + self[:] = nodes + return nodes + + def update(self, container: Dict, representation: Dict): + """Update the loaded asset. + + This will remove all objects of the current collection, load the new + ones and add them to the collection. + If the objects of the collection are used in another collection they + will not be removed, only unlinked. Normally this should not be the + case though. + + Warning: + No nested collections are supported at the moment! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + + libpath = Path(api.get_representation_path(representation)) + extension = libpath.suffix.lower() + + logger.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert collection, ( + f"The asset is not loaded: {container['objectName']}" + ) + assert not (collection.children), ( + "Nested collections are not supported." + ) + assert libpath, ( + "No existing library file found for {container['objectName']}" + ) + assert libpath.is_file(), ( + f"The file doesn't exist: {libpath}" + ) + assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}" + ) + + collection_metadata = collection.get( + blender.pipeline.AVALON_PROPERTY) + collection_libpath = collection_metadata["libpath"] + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + + normalized_collection_libpath = ( + str(Path(bpy.path.abspath(collection_libpath)).resolve()) + ) + normalized_libpath = ( + str(Path(bpy.path.abspath(str(libpath))).resolve()) + ) + logger.debug( + "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_collection_libpath, + normalized_libpath, + ) + if normalized_collection_libpath == normalized_libpath: + logger.info("Library already loaded, not updating...") + return + + camera = objects[0] + + actions = ( camera.animation_data.action, camera.data.animation_data.action ) + + self._remove(objects, lib_container) + + objects_list = self._process( + str(libpath), lib_container, collection.name, actions) + + # Save the list of objects in the metadata container + collection_metadata["objects"] = objects_list + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) + + bpy.ops.object.select_all(action='DESELECT') + + def remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (avalon-core:container-1.0): Container to remove, + from `host.ls()`. + + Returns: + bool: Whether the container was deleted. + + Warning: + No nested collections are supported at the moment! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + if not collection: + return False + assert not (collection.children), ( + "Nested collections are not supported." + ) + + collection_metadata = collection.get( + blender.pipeline.AVALON_PROPERTY) + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + + self._remove(objects, lib_container) + + bpy.data.collections.remove(collection) + + return True diff --git a/pype/plugins/blender/publish/extract_blend.py b/pype/plugins/blender/publish/extract_blend.py index 0924763f12..a5e76dcf4e 100644 --- a/pype/plugins/blender/publish/extract_blend.py +++ b/pype/plugins/blender/publish/extract_blend.py @@ -9,7 +9,7 @@ class ExtractBlend(pype.api.Extractor): label = "Extract Blend" hosts = ["blender"] - families = ["animation", "model", "rig", "action", "layout"] + families = ["model", "camera", "rig", "action", "layout", "animation"] optional = True def process(self, instance): From 85cf9b728e5e5cae6218a12ad2840527a303aaa6 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 Jun 2020 12:56:05 +0100 Subject: [PATCH 004/190] Pep8 compliance --- pype/plugins/blender/load/load_camera.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pype/plugins/blender/load/load_camera.py b/pype/plugins/blender/load/load_camera.py index a69c01e806..7fd8f94b4e 100644 --- a/pype/plugins/blender/load/load_camera.py +++ b/pype/plugins/blender/load/load_camera.py @@ -60,8 +60,6 @@ class BlendCameraLoader(pype.hosts.blender.plugin.AssetLoader): avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) - - if actions[0] is not None: if obj.animation_data is None: obj.animation_data_create() @@ -192,7 +190,7 @@ class BlendCameraLoader(pype.hosts.blender.plugin.AssetLoader): camera = objects[0] - actions = ( camera.animation_data.action, camera.data.animation_data.action ) + actions = (camera.animation_data.action, camera.data.animation_data.action) self._remove(objects, lib_container) From f6aeee39be43f24568881707f1a25a64c65c3491 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 23 Jun 2020 15:09:18 +0100 Subject: [PATCH 005/190] Subset was not named or validated correctly. --- pype/plugins/photoshop/create/create_image.py | 1 + pype/plugins/photoshop/publish/validate_naming.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pype/plugins/photoshop/create/create_image.py b/pype/plugins/photoshop/create/create_image.py index ff0a5dcb6c..5b2f9f7981 100644 --- a/pype/plugins/photoshop/create/create_image.py +++ b/pype/plugins/photoshop/create/create_image.py @@ -74,4 +74,5 @@ class CreateImage(api.Creator): groups.append(group) for group in groups: + self.data.update({"subset": "image" + group.Name}) photoshop.imprint(group, self.data) diff --git a/pype/plugins/photoshop/publish/validate_naming.py b/pype/plugins/photoshop/publish/validate_naming.py index 1d85ea99a0..51e00da352 100644 --- a/pype/plugins/photoshop/publish/validate_naming.py +++ b/pype/plugins/photoshop/publish/validate_naming.py @@ -1,5 +1,6 @@ import pyblish.api import pype.api +from avalon import photoshop class ValidateNamingRepair(pyblish.api.Action): @@ -22,7 +23,11 @@ class ValidateNamingRepair(pyblish.api.Action): instances = pyblish.api.instances_by_plugin(failed, plugin) for instance in instances: - instance[0].Name = instance.data["name"].replace(" ", "_") + name = instance.data["name"].replace(" ", "_") + instance[0].Name = name + data = photoshop.read(instance[0]) + data["subset"] = "image" + name + photoshop.imprint(instance[0], data) return True @@ -42,3 +47,6 @@ class ValidateNaming(pyblish.api.InstancePlugin): def process(self, instance): msg = "Name \"{}\" is not allowed.".format(instance.data["name"]) assert " " not in instance.data["name"], msg + + msg = "Subset \"{}\" is not allowed.".format(instance.data["subset"]) + assert " " not in instance.data["subset"], msg From b470642bec3ad6c531764443fd5c9a2f2d9cdb46 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 10:10:27 +0200 Subject: [PATCH 006/190] icons in tray at the end of initialization --- pype/tools/tray/pype_tray.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index eec8f61cc4..9b5914ee5e 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -29,11 +29,11 @@ class TrayManager: self.main_window = main_window self.log = Logger().get_logger(self.__class__.__name__) - self.icon_run = QtGui.QIcon(get_resource('circle_green.png')) - self.icon_stay = QtGui.QIcon(get_resource('circle_orange.png')) - self.icon_failed = QtGui.QIcon(get_resource('circle_red.png')) self.services_thread = None + self.icon_run = QtGui.QIcon(get_resource("circle_green.png")) + self.icon_stay = QtGui.QIcon(get_resource("circle_orange.png")) + self.icon_failed = QtGui.QIcon(get_resource("circle_red.png")) def process_presets(self): """Add modules to tray by presets. From 6e9b8d130b20b0a5315de1e883ce96868e74831e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 10:11:08 +0200 Subject: [PATCH 007/190] few class attributes moved to object initialization --- pype/tools/tray/pype_tray.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 9b5914ee5e..55b47f0bad 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -12,11 +12,6 @@ class TrayManager: Load submenus, actions, separators and modules into tray's context. """ - modules = {} - services = {} - services_submenu = None - - errors = [] items = ( config.get_presets(first_run=True) .get('tray', {}) @@ -27,8 +22,14 @@ class TrayManager: def __init__(self, tray_widget, main_window): self.tray_widget = tray_widget self.main_window = main_window + self.log = Logger().get_logger(self.__class__.__name__) + self.modules = {} + self.services = {} + self.services_submenu = None + + self.errors = [] self.services_thread = None self.icon_run = QtGui.QIcon(get_resource("circle_green.png")) From 42615368a3ce3fbaddbd96ba2d79b6874fbe0fb5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 10:12:13 +0200 Subject: [PATCH 008/190] module imports specifications moved to pype --- pype/tools/tray/modules_imports.json | 58 ++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 pype/tools/tray/modules_imports.json diff --git a/pype/tools/tray/modules_imports.json b/pype/tools/tray/modules_imports.json new file mode 100644 index 0000000000..e9526dcddb --- /dev/null +++ b/pype/tools/tray/modules_imports.json @@ -0,0 +1,58 @@ +[ + { + "title": "User settings", + "type": "module", + "import_path": "pype.modules.user", + "fromlist": ["pype", "modules"] + }, { + "title": "Ftrack", + "type": "module", + "import_path": "pype.modules.ftrack.tray", + "fromlist": ["pype", "modules", "ftrack"] + }, { + "title": "Muster", + "type": "module", + "import_path": "pype.modules.muster", + "fromlist": ["pype", "modules"] + }, { + "title": "Avalon", + "type": "module", + "import_path": "pype.modules.avalon_apps", + "fromlist": ["pype", "modules"] + }, { + "title": "Clockify", + "type": "module", + "import_path": "pype.modules.clockify", + "fromlist": ["pype", "modules"] + }, { + "title": "Standalone Publish", + "type": "module", + "import_path": "pype.modules.standalonepublish", + "fromlist": ["pype", "modules"] + }, { + "title": "Logging", + "type": "module", + "import_path": "pype.modules.logging.tray", + "fromlist": ["pype", "modules", "logging"] + }, { + "title": "Idle Manager", + "type": "module", + "import_path": "pype.modules.idle_manager", + "fromlist": ["pype","modules"] + }, { + "title": "Timers Manager", + "type": "module", + "import_path": "pype.modules.timers_manager", + "fromlist": ["pype","modules"] + }, { + "title": "Rest Api", + "type": "module", + "import_path": "pype.modules.rest_api", + "fromlist": ["pype","modules"] + }, { + "title": "Adobe Communicator", + "type": "module", + "import_path": "pype.modules.adobe_communicator", + "fromlist": ["pype", "modules"] + } +] From d81b0221470d2052cb60202b458ada5f0c89939e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 10:16:13 +0200 Subject: [PATCH 009/190] module specifications are loaded from json in pype --- pype/tools/tray/pype_tray.py | 46 ++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 55b47f0bad..b6dcf98edf 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -31,7 +31,17 @@ class TrayManager: self.errors = [] - self.services_thread = None + CURRENT_DIR = os.path.dirname(__file__) + self.modules_imports = config.load_json( + os.path.join(CURRENT_DIR, "modules_imports.json") + ) + presets = config.get_presets(first_run=True) + try: + self.modules_usage = presets["tray"]["menu_items"]["item_usage"] + except Exception: + self.modules_usage = {} + self.log.critical("Couldn't find modules usage data.") + self.icon_run = QtGui.QIcon(get_resource("circle_green.png")) self.icon_stay = QtGui.QIcon(get_resource("circle_orange.png")) self.icon_failed = QtGui.QIcon(get_resource("circle_red.png")) @@ -62,27 +72,23 @@ class TrayManager: } In this case `Statics Server` won't be used. """ - # Backwards compatible presets loading - if isinstance(self.items, list): - items = self.items - else: - items = [] - # Get booleans is module should be used - usages = self.items.get("item_usage") or {} - for item in self.items.get("item_import", []): - import_path = item.get("import_path") - title = item.get("title") - item_usage = usages.get(title) - if item_usage is None: - item_usage = usages.get(import_path, True) + items = [] + # Get booleans is module should be used + for item in self.modules_imports: + import_path = item.get("import_path") + title = item.get("title") - if item_usage: - items.append(item) - else: - if not title: - title = import_path - self.log.debug("{} - Module ignored".format(title)) + item_usage = self.modules_usage.get(title) + if item_usage is None: + item_usage = self.modules_usage.get(import_path, True) + + if item_usage: + items.append(item) + else: + if not title: + title = import_path + self.log.info("{} - Module ignored".format(title)) if items: self.process_items(items, self.tray_widget.menu) From 166df5c58e8c0098d8e30da64df181b233d56fd1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 10:16:22 +0200 Subject: [PATCH 010/190] cleaned docstring --- pype/tools/tray/pype_tray.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index b6dcf98edf..18cf1e0982 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -57,18 +57,6 @@ class TrayManager: "item_usage": { "Statics Server": false } - }, { - "item_import": [{ - "title": "Ftrack", - "type": "module", - "import_path": "pype.ftrack.tray", - "fromlist": ["pype", "ftrack"] - }, { - "title": "Statics Server", - "type": "module", - "import_path": "pype.services.statics_server", - "fromlist": ["pype","services"] - }] } In this case `Statics Server` won't be used. """ From bf868eb97e9e6399fc4f69cc77cadf2d3272e8c8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 10:16:57 +0200 Subject: [PATCH 011/190] removed items attribute from class definition --- pype/tools/tray/pype_tray.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 18cf1e0982..436734e712 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -12,12 +12,7 @@ class TrayManager: Load submenus, actions, separators and modules into tray's context. """ - items = ( - config.get_presets(first_run=True) - .get('tray', {}) - .get('menu_items', []) - ) - available_sourcetypes = ['python', 'file'] + available_sourcetypes = ["python", "file"] def __init__(self, tray_widget, main_window): self.tray_widget = tray_widget From 02804710e49c19a4ee96ff0550aa59fe2f504fc2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 11:55:37 +0200 Subject: [PATCH 012/190] moved default maya workspace.mel to ~/pype/resources/maya/ --- {res => pype/resources/maya}/workspace.mel | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {res => pype/resources/maya}/workspace.mel (100%) diff --git a/res/workspace.mel b/pype/resources/maya/workspace.mel similarity index 100% rename from res/workspace.mel rename to pype/resources/maya/workspace.mel From fd1e9b40abe03b96aec678c1dbe88ef0d22592c3 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 24 Jun 2020 12:16:44 +0100 Subject: [PATCH 013/190] Flag outdated containers on startup and publish. --- pype/hosts/photoshop/__init__.py | 43 ++++++++++++++++++- .../global/publish/validate_containers.py | 2 +- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/pype/hosts/photoshop/__init__.py b/pype/hosts/photoshop/__init__.py index 01ed757a8d..564e9c8a05 100644 --- a/pype/hosts/photoshop/__init__.py +++ b/pype/hosts/photoshop/__init__.py @@ -1,9 +1,48 @@ import os +import sys -from avalon import api +from avalon import api, io +from avalon.vendor import Qt +from pype import lib import pyblish.api +def check_inventory(): + if not lib.any_outdated(): + return + + host = api.registered_host() + outdated_containers = [] + for container in host.ls(): + representation = container['representation'] + representation_doc = io.find_one( + { + "_id": io.ObjectId(representation), + "type": "representation" + }, + projection={"parent": True} + ) + if representation_doc and not lib.is_latest(representation_doc): + outdated_containers.append(container) + + # Warn about outdated containers. + print("Starting new QApplication..") + app = Qt.QtWidgets.QApplication(sys.argv) + + message_box = Qt.QtWidgets.QMessageBox() + message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning) + msg = "There are outdated containers in the scene." + message_box.setText(msg) + message_box.exec_() + + # Garbage collect QApplication. + del app + + +def application_launch(): + check_inventory() + + def install(): print("Installing Pype config...") @@ -27,6 +66,8 @@ def install(): "instanceToggled", on_pyblish_instance_toggled ) + api.on("application.launched", application_launch) + def on_pyblish_instance_toggled(instance, old_value, new_value): """Toggle layer visibility on instance toggles.""" diff --git a/pype/plugins/global/publish/validate_containers.py b/pype/plugins/global/publish/validate_containers.py index 44cb5def3c..9a07770aed 100644 --- a/pype/plugins/global/publish/validate_containers.py +++ b/pype/plugins/global/publish/validate_containers.py @@ -19,7 +19,7 @@ class ValidateContainers(pyblish.api.ContextPlugin): label = "Validate Containers" order = pyblish.api.ValidatorOrder - hosts = ["maya", "houdini", "nuke"] + hosts = ["maya", "houdini", "nuke", "photoshop"] optional = True actions = [ShowInventory] From 5a7846a9fe1e7b13940a71c9a7336e7fa948ef9f Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 24 Jun 2020 13:52:14 +0100 Subject: [PATCH 014/190] Only zip files are supported. --- pype/plugins/harmony/load/load_template_workfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/harmony/load/load_template_workfile.py b/pype/plugins/harmony/load/load_template_workfile.py index a9dcd0c776..b727cf865c 100644 --- a/pype/plugins/harmony/load/load_template_workfile.py +++ b/pype/plugins/harmony/load/load_template_workfile.py @@ -40,5 +40,5 @@ class ImportWorkfileLoader(ImportTemplateLoader): """Import workfiles.""" families = ["workfile"] - representations = ["*"] + representations = ["zip"] label = "Import Workfile" From a2d511a27554345aa98dcb8568a43a91be89dd90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 15:00:57 +0200 Subject: [PATCH 015/190] moved tray icons in subfolder icons --- pype/resources/icons/circle_green.png | Bin 0 -> 35899 bytes pype/resources/icons/circle_orange.png | Bin 0 -> 37564 bytes pype/resources/icons/circle_red.png | Bin 0 -> 36200 bytes .../resources/{icon.png => icons/pype_icon.png} | Bin .../{icon_dev.png => icons/pype_icon_dev.png} | Bin .../{splash.png => icons/pype_splash.png} | Bin .../pype_splash_dev.png} | Bin pype/resources/{ => icons}/working.svg | 0 8 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 pype/resources/icons/circle_green.png create mode 100644 pype/resources/icons/circle_orange.png create mode 100644 pype/resources/icons/circle_red.png rename pype/resources/{icon.png => icons/pype_icon.png} (100%) rename pype/resources/{icon_dev.png => icons/pype_icon_dev.png} (100%) rename pype/resources/{splash.png => icons/pype_splash.png} (100%) rename pype/resources/{splash_dev.png => icons/pype_splash_dev.png} (100%) rename pype/resources/{ => icons}/working.svg (100%) diff --git a/pype/resources/icons/circle_green.png b/pype/resources/icons/circle_green.png new file mode 100644 index 0000000000000000000000000000000000000000..b83369a9e341520d4eb8481ee501b2874f2ce356 GIT binary patch literal 35899 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 literal 0 HcmV?d00001 diff --git a/pype/resources/icons/circle_orange.png b/pype/resources/icons/circle_orange.png new file mode 100644 index 0000000000000000000000000000000000000000..656f318e0c9b4eadc4f6f54def39ab4e01521f5e GIT binary patch literal 37564 zcmW(+cOVq*|G$N^_uixI5gFNgBzuz;q7bszxwA*YCmGqYWhD_gXGN*(9gdRC$vTI- z-+h07-Tn8x@1FO1jVI0A)PS0Tg8~2mP#YQQS^@w-;#VL5N=p2A_`L2h0PtUek*=0? z#L|H?S&X&KxwIr`9S7H9=$fMPZ!)Q51O*xH`!UwYah2Jkqf{`aB!OS)`6~|K%Pd;BO zNZ%I(-gV)S?h68$GgTkI_Cd`*#I)h(cH|RJ*r4hxHxgo7Nc`Ncqxi8rPW)&M&_9xy z&*#i`&PiV0uN6RsrTZCs?B^VS4}mw-*^1oJ&;4nWoSIfmk3ij~Z;np86 zL_>p^h_tj<*7{Wq^y?#Wm-ZYkn%B9xep>NLT|SeQCd#04-^_InmFMQ1?FsrIY)AYm z#eRjL{tKUyH*c2Yl$E2P@oE|Hm-OoDckZyiQ7}pP^GC>MzFAF3NJuj(N)FY6=;~q% zGtR%heicekT4^Es^zAVJ^=tmou?m+9i^X6@hK(2~LcJ`T&eeb=EiLGU;r8?E2%f7Y zFd2=uyE~9eHG+;@i1j7U4@709c3pF`Kqan`3$ov3Ks_Og zVV__`>$Jar|KD*GKPVOo`g@(4I*R@NU16fegme4XC&mx{%Pc_VA4qlt_w>^j4b`FW zPEf5x!7$ox?>-oRqPsR0T$T!lRyedX>C+e)k?)7!i@MADl8aw^;ius`_LxhIKa~z9 zB_UB9zCBUl)o9nE4qvNTqQpO?)TY}Z%k-Qu1k?&GiG$E7Q2|9SxbD3-$zYL`dXb*a zk#!brd-7MZ!gs!TqRB~-H0FMv@%{T1<>gQ<*C9<;TiXh7QLN`*#4@`BTi+L+Ygq>p zFN!Bm>J2ICU(jtUX!HTjk2C;#b4^}V+4;~`i*HZYkl@q9l@z_p)6=^#gY#}T4-*6Muc)w4 zfT-qWJ`k(#C(eMmQT874aE`CDpZnmZJMqrJ%693`A1-gt*02NR&v8&OF)(_D_3B3lpVPs5tgLi;Fpk-sOEuNuH@|U3z7+597N}$3yM| zVdCNWG73-SlWV_~N4_W_huzF~y(?>9ke{!}tp;uXtm|e#D(gS25Aq?NjtZC}UcTqM zbz%1^f`_*DqehqBDDe4STg`alw{K#Q8*-(fO1dLEWk{+#_U+rZfF6>C@hqLd6xke; zo=t@6L>4_g-95LyvK346-#gm5i|MHwYs{6wmc;_8$wK&VQlItfO zaVl!P_uOjZ)sp@1hi!V%;V3U@M1J%8C!`7}UL>|cg8tK^1A2OeCcfle7C6pD{o53G z8&2O`a8`OjaM^ibQl!v}BzDwT7lFDBA5ur*iX12hxn}p5d2k zt)|fr%r&X7(fh4_&Y=e?GZK0O*R?aDVv?!l=bfwa*d_+}XxG~;tP6TX07EtlEc<>h z_pjBTaIuM`|netzt1!^ z^@!Pe7B7bdA;O-D7rnoC?3mGK;4e?O)zBYjd`5cz0A^CXbuanIKsT^Ug5@PTG~7a9 z^$VPXWh@o`!nDcBTlUe!E1(xxmy}0aNovvbl-Z(Qb$g~>QV@*eS&P%4?146B>YA_L zg5OW@u@~n9b>skGoS{ggUxROu+{hBPuXwY3|KmVG=OVq_p`q6=Kb`w zyCGrrUubm~++Xg@2da}&uq`Y{x17;CU)3KQ_%ZANm4F3@)Bc8C!!3>Wkmw`=*JQL7 zK`BSm;vS7c}}lLxqG8{Z}}YpHVPeI7V$F)YE+PR>H$D@#kXy z9CKf6nco#+*q(gbo(2uuUpU@RXBJn8NGqSJxr;TS?+-`=Fl8v0S>IUut*IpiR5g-O zbAVV~W9|gEmMB@Eo?gFs<7n`OmEo|C+OS-hVM40Tv7P4v8GBVOawG$e${P9|;e@ip z;mTOb7}Ei?8Rx$gj^kZMy;hAw>$mQ(G>M16n~veRz+W4;1T6V0C#6Dp#Y@YE|1;|y zGIu`wWV>$d9!f&ZFwY2jO6f#l#m+BO^>OH<>updc8FKDPH%nCt)%l^?=r@S4I?2;p zVMnWl^!*6%qD(AK(O%!bwxYUFDGn1778ch05-NsR)4u+ZjS4^j-c7|eoD@AMRyq#z z+gh3K@a@EkH)FZeqnjL>?r*#np3P@`2i?wHvWjgmncUIpi#&N;Cn*g-O#4K28sfxi zLyj@244pRwnlsam`Zn~siJv7;SQc?;XfDAU#KpH2gl3G*l`Canv6=Z)+ul`90S;}| zd`UU|e+QUSQ%&3*v}|p|i=+G;FF2;x5X5T$OY;vI3D~w_e_4N`tob_yP_j*jaFUI=$^MAe~E?y7A7MTO*9C`~K zQj+cJ)L+XZ%^P0|s3BZ?vXu_jVC+B<+>c#9N6fU&XiPEt0&R^_g*@tRP6G`y8mLD$J>)`=Q7se3*FyF`RqDF zU2}`=A2c#@BKHyU!qe>h=8?pmgynPpFEE}`*G`EPdWQw0qUo3i~7<1I*YPQX13^K zHlIzz!z-JlQnjAe7ie_99~PZJ>)-&VG+r%LkHtm1Fo>{jgZu6?U_0pHqr(Ok8q7(^ zxREbkUiRX4eJ2_PzE%Jk$? zQyd0Di_wXVzNW$~+4#1T$VRC|zI=KTfY^9P)jGy>TK4uIpXwJK$f@$qu)#N~Z~s&i z;X|4Z{qL{y0=tx{jXRJ^rYqD=@Sy!8ZHMJNNtNCe@D)2mfbJXV(uc3`B+7g@2Mo0` z73CayQ)}kQd4y4U{446zUx~!6?v2>>gFl0YarG=`0bT2)A19Ml_jw4)@U!PsmZ$RQ zl=tJj8ZfTMKX5!`Hgo;irw`e3!k~^(E8*|XGgB|%u7t-e;Mg42Qy?U$6@FyK&S%2b zJ;F|pmY4l%r&#{4Tq3_0X7J4;8|v7<6#w|$T{amvH*R{m!lPfG@^AdTSPDP*i)N&C+R9l-Q- zFKs5~KtVvDm^}K=U~L9HcB^y2uIz70^jeq1Qgocq{T*>i)%kA2HBD4!!&tu-JKyWW zp6g3pA;zg89f(CeHcEdsj#G^O{!3YHB?op=%I9+KnfXxMg|I+slVH50Q&?Legok{a z@ROe2eWJzH?AP=J&*Sk@vf#hp2j+gK-SXj!0vybZQ0wwdk^Z!2P?{#!4A{0fwczc= zc~^83fKnTyU7LAM3o?{3z1Mj+xazv*YpvFP$)dl|>!i`}0glAoYH;H2tWZ`J1t39s!?B-RxwK1#f_GsbiZ&AR)ABVLn zKRJxxf4ghBGo^wPSGv&}l^%Q$b(`Cv;RPR;tFL6e;JfIp<`bjDsQ2*&kI!tqqe{dv zXHx1E!ecsaS}wQ!q{iKf@^NRJDEw;G(f(3LR$5Y+kmeE~2awZ0!W);I*vHk5^dMZ} z_F7yArA2;wg1uvm0&g?G4L1Xx&pCKkKZ-um@DXB@=9s!vZLAXWEKUetw%*uM zGiCd`L&@({k?V_cjUebPV*7;D+zw|D)@e0Ko@Ww(mBYWH*Hi8#M#*eQvEJR(<(T&tk*IsYUsvO6Y#(ZVa z3I#lphyUee|1W4tH1MA?9dL#oVd=_|i*{^_x6Y#%a?QUP>>$s3#tV+AKPIsZ-14Uxg!=7F7D(>d87Q8eAFY5iQokX0 z0YhP^fK)%!?wX~LG357=E`+Xe3|eG}#v}Pzi7YeMQgVSG!e&Km{arD5* zD6&}fTF~I7TimaW4NK<++TxN33eK;$UYAKZee~;hj8m?f+fNvi4U2S)vi8=mjV$&% z9k!rli+AWx&!^$aj75ErPR`P%iWsa%m_hgxeJ5NLCw&TI}<`HyxTZ#!um+TYu(XQp2QbIJW z%*=x|CMr~tI_-vr4ydl6w|gN7kCEq(WAT?;v>b-zpzlos(@S?lx*6dD3|FmvggZP% z@_(9p@IRkMFogTu%72}dzZ>)PAaWs&H=ba{a@O*s=f~T_`Rc1BPT}#l`$fN^YBd+_ zBsGO8uQdzfKo+ERG;&K z^zXly=doH}Uqwl!H?n>wf^ZWa^x%4-nHPp#&nqr2e(NGIp#mL;GANah!zp2RFYXTS zb(ScHe(OH?xV)J6YhZw(WN~o+ckNmx@?~*MNTEtN&B=l5k`&p`0OUO5na7lxORQvQ zRY>2*-5K#KYT&h7Mm+2_5S9mp;1eHs#W=67p@Fi9yMk98QLpG1=k0>tjBbskl0X z_X+WnPlHtsbLH+%Kc~Unj82^NlUbExFbZAuzaN`5nOT&f%>32TPa)S08&)m*jVu(O z#MntVE2*;o0Su8e?=9AT?A`Cy)b=%VK)pu@W(QX?ssGraNNegYUr+=&p-k+JS|d+r zSghZDRJXWIgdg9PAzis2*g6^3lC8@3LT(`}xKt&L&>=zqe(Z7XFLo&G^kglS>Y+!Z zKy%oB$K2|2Uz|p8L}>cYWZxyfq`KQ3JUM13`;6PhSxSZ%oORW#KIk5P67i;f|5IDA zxqdxOnT`&2f4B72xH`}HfB^mITVAdV(FNq8%TzCyBA%Y)v@Ew5SbY|JNveAHHDM*3Kz0@WEM6mgdE4mHXr*>k z`US3!AKlXd5(oaGb+daeqP5GExcwf8Q}^Ndb`J(x9se5toyI}NXF?{=>%Yr@u{`eN zi#O8bAv;q%$+Wz1AsG@QH@!PF87paThkeH(#Jqu`7_ML4HhIY_|5T`Gt@iZ6l|!VE zrT{gfZ1={_C6ax^_a%zkc`n)#U+|ZwA`9SJx@Unk3-?-+MjQVZ|1QJ)o{6qj+&94Q zTe9@+2n8N()U5C3$OY;urAO1|q3?> z7t=u89!~xs(Kx#>Drpkmq7u?%_1B@Dy_#7&Vn8xxs;TgceNHpjerw_U@6c){|ILgq z-#BvDe32G{IS}}u!H7UC(b9dF)g+u)h-T05tZ7<2;FHimvci<+sA%*HehiUROt z`qjLT(cZfOA;D&iS>IK?MuZ-pm?6AfZ?A-dV`>LAY6Un0ElKNObZy)@LY{68*?&L9 zJ=eI-=BD-iCd$|a|B;uQZ%5pdO3b#iAul!odS$!Q2#Dz|AZEYUeP+wx?f4@|Tr$}; zvtL?@GgR6Nsam;$6Cs z{_mt!Nm@no4}%QV(}%VyCrLQgzF9Hk{vE{WRLe9fWtgzkR9Ck%gsgN+S_)~ZBP5*^ zn8u6moqY4~;r`wGb|KJ+RE7KcqeO&)@j&sx3p^UK&ZP=w7s^!4qQ{(Zuz=pm!+LS8IYR&s( zW}S7{LV^h1iMuo57@rRi@AtB9H&MO}6Ww4l)#JNi4Cnj$?Gyf8&mIT@8(G{F{zKIZ z09JwA>kAuK^t36X-B*V_?ARxD22}X$?)Ee2b#UJm1%A$h@JBa5)0%bq2~kJkk%>CudT_JAMT_NTQQlDW>0+$=3pSrdYclIfti#FoGYJ% zj||LvflB-3@Cr|UNSy9Aw%lR(9|me3PRY0@#J8k+-6#ZDbu&||weT5kEox;C9AmCeq|5yy2*uJ! z?1nohgU`fIO&vzK#j!WBD@6VSceNiKj_^ct*#(TWA*k2-jMT6@09uoSt@yI@pRX3| z{tJEzmu5b@soMmT#`tvI0Pqpu-PJmp_j1Pmn)C*-legH7_>^CJ{U^bw^ozyFO{ke> z%uT4(eb81Pq?!eAZu#_=K|%9$&M9D{bl`>;%?=^V96kDBp`DvjUm* z?wOexyQ9x1<4RM@rIJtT2rA<0LYnAW^}f9G@uF%3Z`xg{!Rfz@@qNJa{bj_0>~7C@ zQd1JljWfs7*QJdLReYe%MumzWTJ<=}v;!?s)?q*N)l{~8#>P{+&H zy^-GL!@8P&u#*)S<~ANogeI3$+2lS#6Xhj{3p{!m2&wrOkaRka&OIBrs0un4I04v+^nSzFVkQyTi zJGUyKZk4tYTIDb;^~SSX_u-K&UhwU!3Z8PIPOm~#GWb$v%hUIvY7`A=y^pGO6;zSR zpUfgUdoIHw8dK<9T773@vn=*}Q4RZ5KpL2P_k#$V9|)ZWa~Hv>6_l=CA6^~xm5JhW zaTzExtJo?WuTm@Th?HvzXZK+zfFP=FXccOaKb8wL-Mh9bq~4y6dd7~bpSR(pYgA}b z5lpqYF=pjDN{Qcu*O2=y#6te&g$z*Zc>n%V&&DPAb6D`(Ik^#hDdEnYoYkKwB<=A& zxe1fRGarjqeyFDFMT#@*a?>L)Z7U@Gnp^pUx7<7~&h-fU_D$JioUySnx?|oYf!@do z{h5VK39Ui>@~5q&=Eg^=K&qG&0T1|l;H>SL-JfrOB&(60A5x$vYz*Wts>tcc-7+vE zYPVYNRdGtOun!}{){qGi-4l1vkr0tO=b+)s&`LD!Y!8ooG@Q=cPuzUEdFUR1CTj5} zr=f&lR@XN{JJqamm=n^N?L2r(8jRQ%*-l?-Kp7`D#{OcTT!}MYn<~6O2^UQ@N|m1Y zf{;n}wcS$my3#2_FSK*f*BdE^s6DU{o~%FA01JEl6xzVhid4y)s~ z<@S9fRg=m7TTS)0W~Jw%ANcY9ZHg(n9G@%{9N2pn zh3a)tD_uTbV}v7L2q}-!PwWz+6>oKdx^}ZAxb$vFzmfVGYIO*aXz7=WgRNJ6rJ{aSjBYzFc8Y_U zypIHLRdWYZ<+1+lFH*58_g$x$B`JWjVKxtEFD?lx{OAeC3vQ>HP~r=phrI>b0uY^7UG&{!p^k+wl?DhbWJ${;`*w3<>qYkj2Cyl0B-(%T9 z`TTF*BXU2A(A1#G2fGxmHG@@sPF45a^xRv=QdW9s;Xa`&#Io&CjsukHUynEw^^zGc z5KeFuw8}ws7Z_+o;C$A#-t+F=p$`7Mvu{JRS9n3zj20VP0-HH}+uir!0}~GntA*9c z^4l&;4u!?|j;9|EfJ-x*``_(&qPQQR$r`8v$z$3WTad`D*T9BX{hZ=XEiAWQqhH0l zR`VYPD`_(QH%GE|e5W;N?M{JRc3L~YV-cpMT!O_O$DnylN`J~C@?CJO6bXBZuvzGe~ zNfj)>It`9%gD;`i@|*o+NT_PV&{i>AiLYA=ZP?F(Z5UXwe#Y*9kQY;@s=nE$kC-N9 zBA{I7>CG?8gcy7#Gh$C~K0Z=nStiL6)(L7 zQ4NEfF&pxWQMkdh z*|#4*)FebmKy%DcqKrnRL0^RnKU(#WsK)2wCoIOI+ROw&ODkrKFzbsx+^#qVo4qn2 zAo{DlxAU~XJ<>ZH9zYqW=moc#q0J5`Ct~w&0Gm)3(mG-%4?PX5*mq(}t}@gQ;-mPY zkVH2FdHJ^_k@o!7dUE-l2eeAh|A;(wGf1*^7bEdW8*O%8wso^Z^Tu29xj7>=8Q@v# zWLZ1vJzitB(}z7r>JR+?vtYaTlSJdknbodYD50?hPbu{jNu8`XlG#h{ujbp6K}%wW z1{W%8XnMh2t98l|(tuY_n>THGwk5Hh)|AeAn;{f0#w{RnBn`%1uGG3ud?H>xI^jyv z?O(9A?>!QF*YsrMJ}~O0UYzI?`J(2Fj;GXbdkG9B3AEi_eJyS$b@mvj*G&wMv_g{( zu8vEIO6r3?-03(ZYK`Mv|Gurc@5Uvv3%)o|?|v$L2E$)^n}_<``&HO63x?r7CPl~U z^|^u8$gw54Ks+?rv>lj_+VTQFgG-Bnt?R8_!LNre7;{$}IKlZ0e7%Nk5m`Ar6Ys=} ztZ!nBNpv5de$g>des2kXd21d}7eM=(9LeI(ge^@nL(N(0n)8%JV^X4K`q z2RD}Z7s7Ow;7Ymxb}gQ2WEeBD?jNsM(to69nrg$<1-w;>eAEfn47yZjP!r|?KWvvW zH+KZ`9;Ui2CstNI3ep-4vZcWoqU2;|n0cbM=}$@buOVk}#ZVzPd6^kBgpUdTtoQ63 zTdEdaUKu%ByHdL%+$gOVcmJjgh7&EA5amqnl)#)`86PLrkJ3tEngoy!PV*MeY8O)D zUM|6F?_g~DPhxiCF0}+_YXs3RnV51u3%3BV_#nwQx6`#~4ZZX$m--XWG??(s$i3#iJ8cPb zQS*&>^KcC*sl0l;+~XK{SDfi@oC|I|n(+S;3s%82G zEm$yRWfDNUxAxWa%r27tjSBqFBf;!t^dm6rw>Z~-bL6?B5!e_f`6179?fw?|v6tZo zOSA$3|0ALb__|`R{$5(un#TUhCfW8Td1^`mEU47DhYxI;e{*#dN0-8F`FYyhZTkvr zb+Z$N;rn#sg2>lD7?|N55idfM&c&~6=#?T|CYx~UI7)kVH|DdF*TBDyF6*FKX>??&ZMrk|Mnx34E#$1iShXS^|22c7N*&rvEn zTa++mUrBZKWVQ1tW9fm^KmIZw56I1S&75MyGeE)^QJUL_zh*9Ljo8Z@K-pN57@hkq zj%NhswYB^cNBg4WKaZ((X|!i4hThAO;M3#dSt@9`4Zdg@M%mfJ>Of)jlD%M^S+@0X zxtw2yprD{T59neJKjpV^+4quj$H#r7gZTDe_#w&};FCwZhfl6Liq9u>Xy5+F{?*h* zKEAA!fst`IvCnG^7QcCdj87pwjIZ+yxw6SWaf0JubYL8PzSfQe)YhJQ$u;^JS@AQ zVBj$cR9aQiZJ`zR{npizZwvZozx+j$W5?5aO}dcRBv>6lRG&s649O|?x455C*d-x> z_NsueNNoOBbd5Qfi|do|{<7|a_;-J;8HFN~V@8j7ebAi1=KsYfDP6QnRzXEqeKnxG zMuCkVKXyW-Z_;Qw4QA8!iww-0>7%V8E$l-{4R6_3*9b(=h?7JO9)g(v@@6Eo|Ku1| z@ADYpIcmG$z^e5@sVPlLWA1&vSY`5~l&X>=HWHe!}BRPS63p^rvausts|GFgJ+0IKU`DL$d_bxIf^6 zFSS5{yTqcL^$JsFtpy-H^JNF3gD~j$uP^Z^F{YBlrhZe*=1<4e`$sQ41|gq_0&3v9d`^gs^&e{hyZLqi3py7dQAyCjs%aN)m3iZg3{wXkZ!;0QvYL-W z8^`U?;cA{Be~jJ2?uiXxU9!{#^^; zB;0A4aH)d_0zW9OTW4xgb2QKZ>nDDWjK;IQ;vSJIAgYN}w@tdaw!HAv zL@Sp48BxP0Obbxfk&^oP4MkLLar#= zjrcsO>RrruRLiR#Di;FZkCU;~2un24$I>MM9P`Up2iYrAW4D6Gn17k3J0*q*}mTtXR@hU016`C{$* z>tGsf%W0NT8{s=o?(x{8$7c4zyxLmuB$FaQ@Cn6XjOBVSa9)+3q_w%Eg29?JA zV=rcU*o*$3SK^y7;EROj68Cm4VD~A3I7AgVW!zBBeb<`-5!TO*6^^|h9_w`;9|t`E zT%!{%?%-OIjMOmRnTA8W(OHT(vY=Aej>uooKm#PmE9NFK6iF*8@^JdU z9bc2{0PaN_O1i7hr11~5ByKPD3jaX`eB*f7QPdxKWv-$w5=jXSPA| zG0-a_9Yb$Ls@eJ0fVTgo7Jup8Ytjcr3jQ%JF01ZZEarlOq~93eL8}^%rmBo+iGTU= z5v@8Fz=^Hkb+o$aSW}dcc##%|)u}A{%6U_NF1=audy5R?RoUds&S#b1BMSB-H~jo? zx_~;czlCBZ+NNsiq(`u!mDz_BIe`{br8m4o8EPs^V%#I0k~+gza~d{oA#}pztU$-` z3bHBcEJInR?~3BPUBd{RK)lswFf=ksG?u!@$`id$9zxc96Z7|bVE|&0wcM5CQ6Y&E zKj7wb+#jNf(RH}nDExs#Jw_=#E}9yB@$2^OFKtLdzAFrP9GS7+-TqB&k}zaSfT$DI zP>to-cze=o+8>??tRpyw>?coEN8v=0#7~t zj5}!)EY9v<(}YA)eQ176Ico9aKtzY{rVmg|5bg?dpmvSB89o-Zs<~b@21L1P-Ye*Y z_S^Gt1VsaSGosW%cJOOERT>tlqF?;yF!`{Xvy%J{nMI|A~?1tA6 zJ*FGh_z%@AMVsh88h~-C`d)rl6%$E;q{i4Z=9Zq^Sbvq(OlHc}=`j~F)K5!K z`B&qBA^-A4y_f9$BK*dOs-dAlHb=A8^q|}zORDSM75{h8oLVoD+vg2&ga>7`daZkBKVL!9;`8G68ko;{Aa+4_)7S+D5j4J z4P3)^J%u3kX6$TSBwOtBpvZD-3A+$-LqJaOl_g6T ziLoq~a5IHb)EzhR-Qj08-7p&bpuaZHivAQgNNu-nw0QLPjt{T{!aI~=s@*HC*?mjI z7!cZn?+!mIRrev1d3XPDa&!OozqyPr`+NQB731O!k>-RP9(1xUdEITw!Xxkr_0I%? z0Mt4(V}MU}0XdHo?B7{w<&Yt@(iC&zH)ya8vp>OPlTOqEsnz*uBIoxK4@6{+dc+@F z@=2m~?}rzBGDEz3R{D1M{9s%GQT8l_>i7f16?PdZL4qvJE-NdeK0^W|DP@)txEbN< zH{ODM^ACf6xGIP3UvKNss-f~YrvM3ez~wfsGq~kecQpy@xlRP0>l8Oq6^+B=p)q{o zGYFZp4k?m&Ytq1e>2w@OujJ&*ruw*Ha(V#M-9bF;d%Vzn#N|PJWLNl!SJmZ{>oqp`^({xP4o!>El4oJ)A=Ul3|ZAKp| z$FoCPjcd%E0;l+okIJclY{PxSzemhQ1&#yu=1P8f^uGF!Vd3K%k*Q?Vzx6I@(m8O_ zAT+J{h!=oXNt+;UabZ1RPbg`~!98XPVgCubMa{pL7%C(2CO)^8E00Tp4(;%VN0zIN>Y+zh#{F zNF{I=`M>aN6(5x|0}G1det*f_%Tgm;5_v5qC(por`)2a%D%>lqK~vvWSsdvqQigNu z=Gdp@mCB}boas_*7`@}%*3n0*Y$1lV4-2f<8+8Hh7iaZj%*KKB{UGG7>W`1G(pR#m zRe^URS_^y!Llo~YdD#u;Ytrq~G4C}fl*2|JVaC*p2OostsNm0xsQjjDn3wj-suDs7 z-H|7MLz5*?BBXDAXHWyTrwOUPExqTTR(r>BgU;bM%p(knzw2u{Zg26X#aV;Czd7R; zx#~+OBHQRoi8T@VdQOYAjSX=yZh64Y=fUYuXBYr0tiBTrlo*hcHDCx1joy2MmL_?cSJ`USHYvMqfN0qsm0rF3-$(p7-*^ zUAtqA*9Nm^Kh}GX^E#ccZ}C0|EuU)q3E%A^TB`(@5~}T%Q)*e~BM#JUn9i6f>D~-0 zGh**8!h_Kn1lwO=J?m_NSo&?75yZfv=1W&pINK;wQ5g}9qSz6ynom^mzqeUSv-?G`exb%yHmo`L#oY^soTI}ZhyQHa5R4)G2*~Pnd+=|`~VRyDj z=BDM27l53+?FEFW@-j@BO2K_`1`&BN9GM!X4tAt<@1OMadBJtVD&B;U!k#ueg34;C zMPmF>cPqsYw=yb`O7X@#G!07xe;b{H!xPove)}V-2O6trv*{Yq{bS+M2nY{%UZ!y6#%KUufiNZMKvu>qszkR} zMXe*y_7e(=6-9$#lxCm5O_EaGMCB+Dlk?V8tT4(>uZ13dd1j9FH+zNjh%$o?$&1T~ zvy2o|hU3P1pw+eq1u)^l10iHt<0uzEF1WJ7zv!nusdax&v4^<^o3$Bet5}Yz7CfIL zAE6Nje%3AmZYZJCEWv3!|1)Yzd}DIGc|20)d1h#8+MM;Rp_)K^p`tVtNLPa-$bOFD z$M;l>a^Jd#Z|cGMIkv&s>@hv$UAM_db-0M^tu#_>P27G-~bdKSY2G5!M7kOD+J(!Q3{V*2sA`}gIsEd1Y+_|mLh1UvP zVR^ch@?D}uU6K-FvM-=uxfP-&afv~Jv(VWS>542;50#wrNtm2o18pJZwfNsbu4NAc z`}MY5qjqj}W%)7AUzNRiGxQNZF)&Wxe9pAg@j!o#Ktpgf6ff^)J_bop;GXSn!q>Wz zGjyPTUuvd~gCFIW4*1{0fnh4>#^etRkeH$b^-o8@b;G7d9vvNJ-B!9~7*2Gtq ze*u!D5jxV?asT>yC8~XjHU4+g!`Aq;+PtHp{w!ZYVB35SD&KAAZ_eoa0^H4tVh3uCY#@ zgac$&rFPI{&_k{E+B(0-tmGQ_6ij5qecDnqvEat>9m!r0!faD7Hfj2|VxEAi)@1X~ z{B$e>%1+{G>|}Z7c4TFf@~=3ZHZo*p+DJWZf0V05ub#cq399g=^#o(9WGod|gJuFZ zkEHFxhZNPTj7et)DB}AC?K1V1Hckwm33|2HYb-BRnPFm_bbCIjkQg(;z?7)Ej;lqT zM*S%xe5SQ+emJrech;mqOP+mE_VNmM(dU}5=5+|ya+bLnUpsXDF~t3c93YByCbfr` z5jk>@@t^$@QIbeTM7k&^`eK!tfC!*Jwh>2I(^nm+KW0k`Cep5J<92^?V~0)2pntwv zeN>@f26i*6^Rtyc|NMm4-sTM{`SBA!?bh z4pS&MC8MK<=i#kx2*eC=jlwUqZFw}hQxBDmw;bTLg|rQ8HtkZ<3mW#k>c)rjl4LshRZ9vow@NH}m!y8=G83rYOD z<6j6U%ebxjmQ?dXx@SKIyPooi=;_a|0;@hEI;XEdNf5UN!co`n@pt&O_O0@-*E(0< zM8ONa(X0|!oCfA}7(4Ut7w&Y8yj}VE#br=T{@Yh^)S{gdSmu5jK!>`&V#3!3Th@ix zSQa>rEJhG3X%&0mT{jf+q2W9D@HuUl@7QOzFVZcNKTpV61p3W@8PI@+6D<*Z(ooIr zuJ?HC(#N-n8_V}pIheY|{sWNJdWGGwwKg!A9U&ptdhmqR5DFY~lSfmo!M65n=Gx zkd33_d+q${ThRq~sa#JPmrH7`pZP}CiuZZyYV`+q#ll(x&I;AHAlLih43RgnJ;3Ma zt;P#=l%l6O#D^K^?DQh1wsHoHyZ^aKOi}M?S+`4DDl(m9+bdBGL2sA?kP}3tZsVMU zsJ{t_6txPGSPkU|-+VF!h8%6h{z&dWpNjD77|~&OjDPZAp_8+G;}b}-!m0CRSK^jC z*O#KuJ^am8I7J9tj0aeNBiK2I&QcR?b&)%1!aRfBkEoZe9)Ab;oaXNm2-jPoT_1n* zY`>bW5toCaE_LVg(vx%bSqRU?s5p1@p&(@9_G3#e`O{w6o49x$-zn!6a(JR{1Kry7`gzf>T*#RKW$Wb=}^UH%@ zHrn!bI*ZwJ@^u#*?t(PtKP07MAVcmQ;YNmPST$up2~P<7^>1E6qD^BS!F#e&Z!K^M z!AuG=&xDxI6aHu32=2N%s_|_ugs%mvh2x|pC0~2d5`TpnW^Pslsvyc7LFivRUa|el zD_jDK3%?0+jG97kbzMY|@;Yda3F}E^?8S^k&zYi&i zVRn@f`&IW8(kqYVPH?uk-T;+RJ|^RzG=#R9I5_mnl6+%(Fj7{3u4PJpDIC#dzE^66 z&d75k!j@u(a=z|9gpw@7xP^ITzM#Kqw5r1N6ZYL>}TV52}pj zX?BTfq~GHEK?v+oac-`VQj8KO@*2$?adto8sIO)Uh~2ab6y+aJb2p^8PB%(oC0fDD z-jelI&Qrhh>ubo&ceU|*7)Oe>3K1^%Fln|7nHT6;zfcSVp2EXuVoZ;pQASxLcAjI# zULON1z%9+$Xk!WqV;KTxLo@2VF~9XIR3)p24OY}kLO?Okgv^o@y)%0G5j^sOJ^tB> z3Fdgj>LU!dQ%k_a6NF~2fTtrVh0yY!1T#qB$qz-|-mAmtw!>6yu=z)t>r!4ZW##`$ zu206K8~MC{cw;()8P$s7JZEhRV_^uFe><&I=AS&vF$PhRsFt-a9;AJbqMQ@aiZ5Bf z1oIrnhG}C}xOYdiS0NkvOLbN9-8mj?*c0RiPN8<0Pig*^h*U!6$&1mJW+ z;9EZpp&(0h|A&cRIDi2ktEm$99l5#&u@98t2GCPy#<`wwMJ5X1IBg93YUEal_-FJy zv>Zwm3{(ZVooP_OOS(gL-tS&+xq2S<#m8&&5(%l87>WNhnZ6g?;Z~lNNXdvsPiN39 z*55hQlCT<6p(iOZJtr4b#8QNYbs*gYvxA(nMc>a${xk1t)&RwrtlyzU-|Oi4=0MV% zu4_8@q4qA3jb`QkyDX|U)nmJD~y&ioSv;Ssc0PuLy4~|YbgW=pB+AT28 z(LeQlBZJAev&O;8kgX2wH7_7Mv8VhDpa4cFB2*b(3eY}<5RIz&gl4DC!=r3K*P~%a z+i*5+xF?}n7Twf|r|vvgr{ZQvA3E!^AuQZqB+KVS=tjjGGJOE`xmTLViyb#O^malt zj#8gZ0?Sot;pe}0#p)!rQLJ1^ju4H;^+;~8u!U>E;c^u&48=Nk%1~}M1h*{)IFrjT zE-)WI?M>ESW_QWUO##J9K^x!%GTN(U?(kpfa|4^dfaQ;W?2@3enls>| z4R5b;uIEg=eR^8rE2u(|I*zNO+n9{4OA|`XO!_iPSVAd^v$bN&eY^xPK6N$lHo5MZ z#mF8^xUp{B_<~t4hZH$*smwkh{>z4v+xjdn{pk1)joAIBUIUsHF{sAI)D*GObkGX#L8>~a!I;kQsu&c|2N3^)9I zGFf}Mtklj`^%yW8x=>iHztzH*!|3eKpI2!&Hg?un1|p9RrvPU|;TTDrsP^1$j}Zur zYWo8JW=B7`giwY+p_1FcdIOQcQ4$I#{uP`D;wO0w+Q&C*y;ekvI$7`?!i+@XgkeQo z+wkLGY3&2v+h;BwZj8oczX_1sF2fAPs0BRv1gt-Gij(E7KH!M8WvpG@yb@c)^V+2N z9ZL4AlYC%{n9nV?v$1AkCmrpG5jCF$0F4kYm&xXEo)+pRw!uw-Xjl^n|Fv^x7hW6+ zl#D=I7C+5wrdwUS_$450I0uqNRaa9J`a4tv+wNKFtfy}Wm-Vc#sek9;ZLa)Nu#C(o zY50n}bxh=9oXhsBeFbh!qD}Qr-H(0a)c711-n}D({cSf!SU5!$qdSO^u3yPZ1lCT&VIyX>#F+2WFY!exBoFf%+)@EAfM5$ZzBV^BG z>aQ>q9DSZCb|OUde8R~Sj+lw}8$rgy=+{Rgp1g-^sIl)I#mPHEQ}xNS!Bo&SAZYNh zm0>q4?oy#jCH0sO)>;fGIYBH4|C#taN;2tvP{g`@X#QSWb(7c78OR6elHvfpA${U4 zp4j$0HIdRlg9C>;1!7U}{-GgQQtJ<1yrLz49Bl9xE~Cl>Spm_a_QlBEA2|CLz+8z1 z$pvb?Q4fKA{G8}|jM?hUv|E2(EbIqRvlXoF?z^ z#J>2ldRtT)Bj@Cs2|KXa?qzW- z$TTh$Vo{L(7zrM32iCTy#HKkQ5{ei@5Wj(Y>$Y1xYU+l2v~`=_-Dp%(^iCSAI3o3E|p~wyQt? zH*+pV>OyZB<;uuU?p-0)my{vJyW&j}Wvp+aZBa68-!pix+H@^Ff-uNtVFAlJ>&LrX zgY8&JCjecQ-!lQ8!_OGrX24d$6h|>1rmPUiV*@5|F(E(iEe>`j73(b5zT6BbB?Gc@ zawnh%e^MG2MuH|Wih6@s&5bLO6?y>l)LsR9vd`7_k*V86F+uDwGO8y$?qY28h)$q! z+`QJGaNa$?(HK*Ly#ibkS1(-QMa0>;7q_Yx>OEpnWp#mh zTaLvPmJR`fhEa;=nfJ_~V|I&?H9bZ|77c!&9>_;7Px}OjfU}iQFy#1yE&gFrU?wsZ z;ksjGv(VP=b@lb}4-xIYS17KE)RL9s23fu-(Iz`IG0hc%%L4-XQyL%6O7tikU!O3f zbWqi8c&_2BOOWKApC0Tfp&@mD)~uRT8+Gnq3)=rtvCc7=Z3frXzH0l=TW+=?s8_Eo zPSsLtQ<{y_j07}bX4Nh#cf43KL^N>^S+EBSQSPjiikC;Og?m*N0x~V47|Wb@f?!0KZAqoNXy}w znLx!QMd4W)5=Tm!FCkzrS}L&YN~{wa_lx7YbHNtK7T9(EV79_?8@1_Gz3Au?#J}mc z7HcajcsCLAJ)Se&p}pd>A2G0x1GKl$e$U188rI7A6h$7KY#Zvfyb-_)nH@TcyAr@^ z{ZavkP={DlE8t=XGcYrI8QkseKH}=7l!2Hj%3!M*p|EBt1Gn|ny|87^jeG3*6$A(c`4aN=m9QlVEvfP%8FJz0Dg6TucAP=5X z&t4(M=8q^4!gCLK+qLhB*3=%J8#Z{Gsi4Pm*+LYPDOC`3Ng>m2j}M}cLgCoeAjMn} z)FMVmRP@c8{Oi*mgTSv}Hz(atX+i@jf1mge-)Kekud7v!vN;ICGlqMD7lXof@|z%z zdOS||;IVJ(oUhL`!|f|`Ct$j6FYIqh@->$uv7PVIKJq!xixt@Dz5t8?!XS@ z0ji*Z4Zk$cqjkU)+I-_%fbibA0*E=$sUJYt$16R>HSBSkCB>$kN7)vNbortAL@Uqo z0LO4GPitzY&NV_Bg%gvL&zgCq`pY&lx?G!RYcA?pk`XN|kQ@#qPR#`P!ijkfE9NYY z*|^iF7>sYRu4r1{p(?RIONr=mJsDS&L+=zf@TrkAF>oMJ8PqU&X<=gSdvd|KnAiSs zf253J<=r0a#-D-Y??4*40lB<(mwI`&w^Dc&(TLjkmCt#QQ_Wzbb&BY-L3sCWaQ%ll zV84bSzLJD=p&;()o&KE&Ap&2R6s~Y%PmHiWb@HX@Z>`4S@6Oap|BBJ(l^wmQm8~6# zkynLr8fHYjCa$ZFEpE@-nyqOpHZkomyvesM#nm)D$ZWTaegQ$PRXgtI?WmdpDPC(n zj0WJovPWjA&*SVe(NJxWTQ@U?E>Cmt{N+lrZV}Ul)%#9y>W1D)xim*T1ugEyLOGwu zfdyb?01$z!&wic=6sZhg)$sVH7id#`eOW5J*Rk?()y_sqllf(9ck&TGvRnMcO*Ol< zv1!!?Vgs}$=Qgh|Nzd%?Pl+M^OD?ocWto0Mc#wXjn{|ud7;P6{d}oqgaq&@{q4KbS zr_%+A78@=0^BFq$3?RJ#S8i#e!Lj^`j{rAfwN^U^fCdnBZ_Eg(osF6BJW?uYbNkHA zmxEvbx?m}^Hv^YIhW(kDn7r(z-t(P8H{^<6<8yvVqexWf98ILb((AiHpbo$983|b# z4vyhIk;G{9pQYOv;EeP@?oLjUKdfB>E<~&eV>e@xxnZ!5?w1~~%Ucdf&9y;s^ zB-z0+a)g(jh4;9TuF}8%6lnOtMw;jw*|l{ecyzY%Fu6}KYM5gd`(XTbF$M|Q z-*>fjh5pmzb3kBA-nsfH@ngQ4NkcU}fv=T(c?e7qBxjPJkRjQ^iE&%OE5ITe3IX5J zI@nI;m?0kLo*3qG3yFIkYRi9kC&VT)Hep_^wE=>^e6l9(C+l5}_^C_lwU&oxGL>3X zjU?u9;@jX1ccBQ!OVWCeVWbQ`jh_x+6-bLa-oW813(F9`u_qV*J?TE_!j!d01{_n0 zcZ=Bj4%G1%A5&ZRfCq9_z-#1jpvan?TT&N#=FDe9E!P-UQYX*6F>}%~@KTy@RI%=r zt#&$zht0SA40a>5s++pA8zr*!7txk=pqOA;pw87wI8Z4 z&!vS#{LW^ZahF{Wyrn4kU4c&tlP9YB-W@^eVkY28{AV0vi%u)b2tqMsA7YE=27%t+16!y z>Q4B|9#X`lcm+>BjWz^f|HoW^lNP^&FsxF0$URUTDv=9Vz{e4;)csagX<$4Qk_tE; z8Ay8j@!JcC?We(p?_6E;)y`AeTk|%VSE{hR@?7?uWdtQbdOS<)+ejC!KMjJMMxzq& zUyI%FX`CGKNZOvu-4w+1TU^H%+#No`)tM)*GRifD2u0ZBqeS@dXEqa)^BVla;MN@^ zrxW>bIS4a5aK@i!Fzby(o_Lupl*a5fL3s2iA~db`IVtOqRI=nT)rOfd|3tezW3H#(tgid zs;$!58hMy);5B%wcDbY_`6$k;g;K~v{0dw&HxE?mee2HBcKH)mZ#t%t$5o6Q#s$(% zIyqf4zaq_)+wa3;X+eB-Dkx}Wi00gqwO|N|9?_>ezNq)10Y4^|-flscOp9fo|0wQC zGM|c{J0J7bA4h>2M7?5B6XTH-1$HH&@Py+lLI>xqbt#{1YaG z#$5apdFW4u>F}~xW}riMUZ>bCCKsOV0km@gMj2x?6#rT)92wg=@oxYPCCzMdB6g*W zF3TT9MkZzYzIF{bjYb6-;?`FKYTqktF~$ViXV^|GTpPX67!drOn9xPSe};O~Mn&|H)PO^1P!)-Ti|HsL^JFV+60`>}F)E*n=MT1HQdOx%IA`GSy7BHp>WUjXur1! zyyngz8;Le~vuFSM#CvphZ{<{^=JeYspS-dOEirT0RCD0b;;OUMCYLu8(kdhl)3gm5 zz&&)N4&IjbWI+atzqRLbg3Fqv%e9^{6VG`yO$oEHi1pti!#Qn7w;4nmiZZq#gg015=SXyi*vnubHRy zxl5w;9<(s_hs#qHl~2^X>)0bn#hWf)eOv8Tzy>ik&W!G{7K#qmO@7Jd5slg!AL$RC}y-aYynu`Jkg9z6v_oLP4r6-EI$u?HM zOl0a0*6#$R0K#Ymv)6c@ELDu^Ev_)mST=tn{|Z7puZk{2hRAUvt|N5e<{zt|H%PN_ z`WqFc%6<3ZyQ?vLd=|7aw(B7a@K4CpA3JNu@aLhq&XH1uTn7l`-j<5P$KO#|*Vr8! zgd`^7ipFX^i>$y?QPQUc8(y`weQt&`!Mrg5si1xbYYr*JJ{OaxPhfhtSTk5M0 z?knMha-~9iRgxyD`dCaLua`<1@)Q33w@@Po!u|laZ3eiJRw|?k(#`9*P4CmRx;eE-YVBI) zc#PfFQoc+JdIVpnBU_5aT)}#l6lMMj80U@j5Y6jFw zPlA41=d*M~UTu15kosZkSP(~=awK)8WKt|(qc`qCA0ge^1KD0-@Ka`<74vZ`P-+7n1G zswSv?i8~_IT_mvZ=LyqBl78m$+?)A;W0d^T=8o^eG#I{1gSSVUpr=lV%`ARdM~rud zM<_gxqn(6qug2QeqhObByBfi#j~XEO_@N)UPRbp%)a9~IB<<*QeT zmp0|aNdvmj8hwen`x!tl=k8wU#GFp?jpqFb{mJuVW*YY(`~r)h-x9s%-U!<=OJxmf z+l@B?72;l!aDHlrpC)d;BUON~EN#6oba^%4GTFS28jWwLo>o`ozB`jDB%Y#L(&p_u zDgP_&lJq0imJ)a!U-38I#N!&YNncw1vG|@(b#Vv&lTZu)2dV_YFtf}NmPydvCxqnR z^Mn|XgiX9=BZ=gaqr5ss>QI3?t*n z0FKDTOGbrWA_y^F|3kY!y@VR_jbwyB<+A{%M642fjOH*@7<;^*`Ky^XghVvd@cH3; zKv{c@2E7nBY%`Fl0}x_8xJ-NL46Qr~+uK4P3^s34PCi54&OaZ&B3<5kD}?$h&FTa+ z6=~BxI<-`Ot}U$>+ z`UR-wrm_?KZ?W=AbKcfI=u}{_ea0Dm%peo7?C%|lm~DI`@Cm5C4QafWcDwurE+lFp z=@v0b*g5WS2b-q}iUWC!h)~c@@?kcRC%O!(1JKQ&tEn`ax<1w`&+(yLQF{O`E8A6T zM;w1G-6^fN)pZF|72_VR3&RLMpa|0Uk`WvNnc_qEkliI<)=@YRMVl&PTqk}z7viKI zCwoxfFC12Xx%bC0VBZri(t*VY0Hq^67ZnAic2^P+`s7c7CY@`hly;)gtx0ZNx%b!d zUG(we{5Np)g_kK(=VxQd>!9M)UhEW3SAaJYID1+h!(`B#cQ7 zrP=Aut`oZO_RsPAga%maO$C5*i}m9fwqbvI!yj2x0K0X=x7EG`r5fE3#ZAguXH9OF zNI)-e_3yCXm^{n?ATN#(PcmXbFW5O59^mxo9-O!27%&RC`Qb?8)}S8q!PxnQboef@ z!fDlbl;@*I-+7(S_gH7OORjkR2%6pOjEUi-oinwRbwg^jCh)wlq)jz3EH2Nx?Kbc- zUR}YacU0q2*Xb{p6biq|>r6Dp2W zc5pa3oelkRX7ZO$U-EWINoh2}N*m;|#eMsBR|Qh23@T&E48jT^7{!8l2;BJ(&t>nr zM6kENarz~8>t~GX)JGQ!QJ{=@l~n>A%lY>bQ@SP8yDTkCx%#LWJB34X$dpY?a~$fqTnR{S!TtFbsApfi*Axg@m6YbZc~7<`pI5~+n#IJm zmo+0-J8s6lKFM4?zn3Ae>RA39JU@^qCfLqA8oe_xv2%Oy#i7e3)?y@13{oiuQDlXt zHqrj|Rj&=d+&MX9Kt9FIG7z`AdK?5aEu&jJr4HX#8zz1A`_xHD=WQc+ewP_nt9wZ% z+|~Nt<9D=)k2&j4%?_eFm|$PdyU0^!UNf%OTm+_8sgCjUd7-47*!;yW(dOy8(dGrp zm8m>z3u=5vp0N9m0pGt+JB49Yvsk;g_=hGn$F+ri_N}lOrxY$80c=8`#cZ_8m-)ao zGe)0-2nkspjEed5?MWIZ-1?OOfb6f=hYssPE`wT_wb5!9W_P9`MMqCQotu5{G+e2l z+u}XpiSpC4W3{eWbh-v#gI}lX8U;=bRzKlaOIJ;f@qU(HS~?8+vnuzhwbz(^AvY z)6X9bWD_C7u1G$@#r`&)Vsjg4Q6h~dsr92}5AMt0eJNXKX|;az)7N$zQb(T_Lh{F5 zN;b)!bBz$%gPXQnPAW&T_YowZN=IKBm1Tzkotq`7WneQlvM;SF-nf*@>Gge0M&_ytv+eN3FNP zWJgk;WfWvtE9s#uw1qOgQwh6joV6vxV?%W;fpk!MJM8`{Xaent;sZ309}qA33%BBl1#wT!JeHU^I&lja24 zemuSMNaMQ2!S{)x_9LQEZWemBuAATt!Q%*ZX(DfRkkw^5-`-adBfK?3_-^=<4w7i6W5PmOcgp z&pw`ZZ6j867uUgv*``vRr(?|xi_lnxeLTfQ0PuMT2Xvv8weU<4%@S;bzr)GG$W?DU zC@9g%Gee0DVB8Uo4Y8*aC0}rbb0f1)GJi*aG;|u{6*D#!+u;vUfGzMPm;Kw)p-_t` z&v8C9vIMvARQ&v#pFH3IS=8{9iJRAW~1Q2W!Vq?Yp1m&VlhIyhmT z4-r}lTRNovexV345_$xxzqRn6ulQ)(yeChw9Wx3EM8K_=5WB0uS^y|1ZES3WgGN8K z+%#u2uqjtDV)73bt+xR?SpIgL+KsZyb~!=kN!=N`%~v^fP>T41qh~T61kxr-{2S7L zp9sa0KJjp@#mrQ0K@Zw5F>mBTN;2|}D{}RpFP>Mw!4Mt-uo`7vbxH+a4edy_@TpdGwW&8<|hi2W6Yke-c0( zByMW>lea9w6X>0OUOTAuhpa~rKhF5FSsm1*C4`=I19&w1igL8ByhuwmH)qYP;1Gw7{oEuY7xRv;nBm{(HdW5~+c zUKJ;Z_CCjgG9yW3FY{e(CN1s~(<(& zBYxXlSB2eCFJey%Kn(u<{Nz3tC=e=0rzj&}IR#<|P_lJphTAU9|~L0?K8 zb5kIRs0Y1n&)h-a-oom<$I4m4Aa?Uurg7He#~e{)sZ&)54jv9q1=gj%QVB`(Z5#nN z%JZ;ptJ_;H>xhGnb1!1|86MHh3`cD8(1mj(%sK2!l0rVE+2J^-KC$!ZSf7pmNN9k! zid|F1TxHcc4mmI>7Whjd)7mVY4l}o6S6Rkx4(oe03~>P=Y^__h+BISM5m9C;#g~%bX!Vw7nee4)gRI zxKE{)J1VB5ep~Yhqv7=ZGG=QOkE?ma<25Z{!U<<={Jh2n2U0=KOv~lG#=V8 zbHUHjgOZeB8vnvYbpnT>ci2X=7ozqhNb&f>!>>oo9D&c-LRhlYv-lQWpo}imXgtTCdkp%0#*hEkgx|(Bg4~i3tSaq8HzaQZpI!1XSu|ku^8AJ0DSSAD38fx(j%D0tUU+(1llKv>wm)Ft|mITK`CjzdaXT@*qiuo7JP||NR@my;Fv#d6kSwJZhR*%gE}Nenap6{Kyu?gU zU~IFrC#N#>Zw6*j`UrB?<&kJu-{)d(8DtUyP;N&nICG|NdVS z(=m@=)iBo-I{F2xL}ykA@0RQrSPxA%Y zW-D(m5LGT~->js2*6+Cdpb6l1=MT;Zq<-d`({+4~I4e(S7G%7`OZ`qxyRL%zGeOn5 zJnx4r*0G@b8C9EwK(F)0ZGM>eQ9<~S=+;QD(C+H)K6~>@3zEjiC$+}}$BL3YmmG!_ z*e57IT)}ff4kqJUJqXbs#jhYSb3mzxh?p2Os8O!6vXa;EaM$3Vaw42pz)?>6aAOtA zR*KK)M?YG@*Y6BD|5w4!jBu%x;4RJ*x18gM@$;D$gevLcmlpwX$>LWh;POM>No=@` zo@unu=yUCJJeWKX^SbX8=M6sq8J1(C%vPZl>=R8o_&_=9S+B81W53GfhD)7rNAy2$ z$gyEn?7gPR8{ZK8cQ8@&`#hoBGQ9a46gtHSe#_{4#mwkJ`*NTjv6XyxOw4c1tIpns)EGwm2q4(d;?XMV@9q=E3 z(G0sQ_z!di3#JE>Fv-=Zs{D2d4ax)EUg$ncn29&Hl;yo&`e3|zwjn_Kos<5NpWmJ> zs{HGF&v@j;WI0W5SfdBWFok{uliGT@~@33)7*|qtq2_jasSg#-cA_uq#l(ap6 zT4bpe*hhOhT())1YwFEufrFCJX67l9si~+ zfjW7z_76zVYgnzu()GrP4dCo7-XljmiRlaud4|7W-sF4L7A18j-)#N^aJwfX1!BP#YzwyUES+g?ycXE5n>{QFAGRgOh zH}iC9{+ouhQok8HYiDP6D>Pj&AGG=b`p1^kr9z;(5NCQ>+B0E8Q*YtX3-Jh`vE2g#r zWoHx{cosNslcgmg2wFaQX0A-v&w&3)I_Ep*R>%-rzh*Y(@v!wi$JMJ>2}>>(aTB`H z9Nr6Mqi~`<6YaWzA4AcF`QYmD%~f=d+XC0maI(b2o_iv|eAi1rl3+>**4aIK=H!P_ z`M{-LVS!U+6RsQ5+^QOsl;q@R1gp3BXQdhk1Xvw4fWaZI<$KW0OW*s6eNWi}#ljXw zIV`h*om^>OGV$ds4hc(xbiVYSYmQV6WxJ`MIV`JDB ziB_`Dw!YbCSArsJmJd zx;oAc{&qAERw~LIxA0j4^v^%B4e;V3uHi0%&=N=pqv7cr{?|pslGsw-X%Gei!qG3|BE--Y^=T(ZjEa!DatzkuEqUm#KDqlsVQrAd$zzX?s|O zRwC_OiDe^%A4gowg>YX4-u968>%UvNVXwJ}=(?YJPiOBr)^^3>8+z`OPc9PFypv{w zhjL=*dlTg1gYfb5G=XyFB_t@$`;sLxS!!Bs4qD7Oe^Rgc_;ETU0^tGaa?fG0p#L+D zc$;P-gi;1rUoygH8MXfydbZ4SN)27nkgfUpA0o6=#+4}KZXpCh@MxFj3@@Jd!-$u; zocI@YhpEx%5ypBws9RRzb!Nith&JhL(3SUh&Bk1_c+2jna$|8gBU=ApF~*4EGPptq z#l6`Is3y;$!W*odA{wArKAM-E!`!tqx$62YUT;W9^KJ!8R=)N`DT^5rr=`?bz(gzV z;f=TO;(POFwTS|WCw~P>(&x$($A8y4Vvul6-*CNbq;%->LDSm% zq-uQO!ySmNJb!VRt4X5*$HBk1nfeZxYu>zYj1Lp-A|7bZ)bnAUHlb|QT+N!kDC?}a zS{r+PSu`ts|NRL_+9li! z$$y6Sydal^agAEQeT>vA%KSH~bnou`uP6u8kY>wskW|LUk)KacB3|LecsX3>Py8l$ zA{RQB=cVmqGu@Q&=jyQ>vn*z0(ANREPPeL9#%CU5Jc-{TT2&Q{uH*oxG>0qXQY%to zPxwyuwoz0FX3)9@eW*{~w{I8q(ZpL3^1)hl4^Y#Fy&v%Ng%)Ju<1$#0GTmtoRcQ>$G=6pv^h<`MI>ulA ztKw>R!xUo$wS1PS!4FEZzckn^WcCAjaBc z?<}i7rxcY~V9ele%sFa&xA2U`CsUdvO4DG(?8NuVO;ciTz_Xz?>_dcCpB+fmTv{(wJ(&zIhl0bo-DPW1h*}nEf35HV%jwap4kzARZe1%ph=p zHoKpfK@&@(vb%-tlFOgVkP6e{X86T*>GICXX_mmy)law(K&8!*=)N8*) zYn6D#_>P1VXGQjWx%Jm-+FopxXQ zG0NJ|1qVBzr_AG}q8uhO0wSHYyrmAw(}fyXzZTI6B>IXS$w*B-+szc-^zInv=j>U# zFYuW(kgiUw$^gflD`Gj^7!(}yUW@pV8DP(xDchzjIsCsAh53B*IL&&08GHN$`J%Wy6V97P0#%mR zNn;P_T7Hi0Y3gl>S`;~s;XUnh6*OU1846oGVBvsK)-Q((C-)!Hw*penSrV31jAEKD z>H>bcuiw3Mbs!w%c?1Ui^FK<2jzF^bITKKTqp2qc>2npcsvoU3hSmHKL}7Sdlt-CE z<0kh^-?n{{Yaf@~7}-6^*KthdrRW<*4pO|`s-Zxy!Bo-@Mq5G$WGykJmk-fj?t`GY z2^I??-Ykhx^|-t~wdQrJDyN3lHL&IFc{*_rqS775B z^+(|Tu9L7!B3?Xt8YRO3C50)J}r zX46`ELGa+AWKSmkJQjQGXwId>?oUO@XAF?mGnsdo z0d4tl8@uY>Q%lCqHNva!Da$T@t{(CTLQ-erOrRe?+kLUQGV|Pqpz9)U-uf&o6-@-s z#4qGQyvhjV+VV~hOQDXW3sJ91YOZov*T4utY?x($Gagv}!SJzV>YY;lTE^7fwvzv* zcb~zPjcinfNKokbxkvDm`yLC0`0YH3Y0Y?%y!)CBGm^iz-#6pe0aB1r*gehc5gysC zO%|Z|+ z6NPo8p?9F&muhG8!w zZMgqbA^$yjo-X(PeE-my!jm9+@^9o4T+rAP1rb3=Fj9auZ+ITud4_Qi_#A?s1B zJ`m>vY1#q~m#d~@Z z-q@~v{s}lroaHpj-SV_dP?PIh*x#-Hr6_^n!Og2;u)09l#+e@YEd^fPD&|HQhwc%k zi7PiQ&g_(%;veo1YgV&yn(t_UH^-#&56Ow>8v{~zGE~vJ%LxsC8a(z|POej@{zfsg?OgbYK^n5u~ona`$cqRYLJi>&ueT zlE4&KovX!Cq{Af;)qZ48sBDC7T3})$fpsSwWD_`eTsRYh?XzfCgJo+luR&U0o`h^h zd@v6$wwWl}Tx8J`AZ`@RbcEvZNd*hJIsGST5&pME*eyzk;DclFhDrG^(dbA5sltj* zze8nt41lDbE)?QR9ia~lt-zCWpvE5r^HNG&p70>O$cx({ZvPO!UrK5CO;?bj%bj_6 z^Ki2ffbWag*?dTkDken|c)hEKfELfV$zkI*>-?6h<9d$^)cC>TcVEr_Y*B1nd;TeK z;PiyC%{ywTcW1zuV8Gj<@IG~ixS9Gs!Z=eQ@5CR2i}bCNuNdc8dx~PGQd~yRpntLe zd@mU4s6?U}kZ0NI>yKxgW?UiPo@iiOVYy&NlF)1m?y)=YSW$^69k^aC#fuTJ*3bvS zyz6~9YF^nB&H}skZ%J<%OXY`ZENAi4$&cbuIwABG!*Zh{C&Mkk_`EacTvM25l!n)6 zW_S~iG6yLb3_NW7L%y8OP_7@xU(2HGNMPTfi@9VWsqpruE3O@h$RRM{k#W|lpq5(! zN)En&i+uU&>8Qu{qtDi#u`K$zA9@WS@KLzCz5UGhAqIO$JA;vd==C0uF7|DA`7k^v ziP)4u$3#!mh4FrWhkgMHOnk}b9377fFe6@L&z{wnmH%?9Z|e*g4Onzx58KDVkV%-c zucp|u$?WlfcCY4f8aM1w&n`mhkXX&=gUa2nj}?AJ@5qe3!-a|8HwAr#v(!M}9i`pM z>#&CMDy|HU;V*F6LTJbegw(*II0dQcySk3}65BDHNkLC`yezop1#j!0!o8^{k*9w> zt=TqF$UC)ien05W9e$@Ym#y8mY-k7E$rH~OqoQ;rx1MD7M4{tfGlSB`T?81=M-;X+ zz=XTKX|bD1-kNW)uXdJp37@mnS)EME1_JIO=C0V|E{W~G9R}cmi0g&dWmc&Wp!^5Xp<>baT9`#)^f3Xt zS>L~FF$2Vk6AiX+L%@5g&_erXUIPZ4DuiuJ*IVXNgoCay?_edcTo-_nE5(DT5O5tc z`yzXUQVn`VNEZe=-j5^4e7KcMhgS{k`g|CQ zq_{hp@o`sW?b6Q(0kDyyh^N;S*N`>$X&j#v+GqN_8|F4#cUML@LD9a`g)B7@2?yPl>b`I0F1G{ zsGv!*X`O$Ue|ulEnO^xeb)r0a5MFz9auQB37k+p*5@QURCXXCq)lj@UaQH zwCzmocG@8tS=Hg@_A(tVXi*U6I}d&PG@6Y^%Z0V7ll{6PT?_8o@JzoKsJGddi~^up zugyY%cLZ_7Fq^SX-8Z;wzhP8xDY0GD&+TB@{HXFnJQRZTLBeE(EQETK7~ZE1?Bc*H?=6tHEQK z4Z)jMx}Jg54Aq*unvXELk1!E07wwAVluq&Ez}o`bjr$?s-hDP@h52(a8^H+ExY_T) zNxM2Rb~lqmk_X$VcN}F54k499P(#${yE0m)PK?EGycl;8-yI=H=ZxczA_-o+qhFGz zSYIxHw4B~=-za3zFa+d<)p^QdN;_NjQDycQDv`q;Gy4O3vb=U8IYz~}eh3^|eBxW1 zw7hJ%R3fH=Qh_oi-iGe~_ZcFG_@g8j(4%=Kp~$&scelJz_!dtKgYq_{PJ54xnPs`&~}%$9CPw@Rk#KPSwml~D(# zE^cWYJs2B(6vSPgdPvg__(}WIk;LH{c`Sy{bbBIz%;mT`p9M#a!d7XJ9OU(wz3)~Y zw?9dRljq39G~4tay+xr!Z3&I|`m4}S4s{l;-7-8#_v9m5{J0zr>o0J3auln~26&kR zQ4%1_x}_IgWdOrwO0_!t4*1#f8cB=fH~S(AJj2B-BrUm(m0Lbrw(q!4qbT9uBm-Du ze%xgq4K)D|SzOOUrt-&A;j6;H>feZ;eZd8?AdptB%iDihdk+;Mr;M zz}_VnXOi&RDAwTMUIpb%JKO^d1aRijc!aSCZYj1QZ44GPhaOPpnaKvD12v&*m(2Oi zEGmcKPt|Ec@(TXR3z}@=NMeWr4s$=TomXZryGWk;shrSk?nqb;#(F|xZ55=VeA~bb zFw*`EvwQEr{B$=u7slX+93C(FFkDv4`1?K()y#t-2nry*eW6G!L{Pd6?n;vOcOjmm1%{ zJ$p;%JnhVN-Vc0gnKRqZQ8?ByK%X_JvGuMSAq|0!ORl&i+V=B;-a)1i>1)6={EzA&@u3w<;KMcLRaDdZ`D47=1vkp~>-mNDJc6)~)r6~fz@WhyC(y12Rz zT$9VNntXouC|cVlPrHGFi2rti~YoJg#wK1v!Y!sA3^T8Q&Uk&I_H5XM*> zfEyD9?@NFTWZnW}0%d>Re#z4E_S%<~6XBcWngFU}BhLj??S+@e6kLael~}_!&O?Wksxj6d&$<{X@=1(BvdY z;zzIts(e5u{26XbSF;@XBJ_ttOeA@e{;V7sk(?41v=A@knAVk`(bugNgL(9oA@qccGO_t^Zq=<1^ z^DO5{xd=F#9V@XeOvO#LojK*@}nIPjY2p02UhRKjqyZ@}ydhy!Y{Dt}7#U45BC-$=?h5fZ zSsuv`$57gQhdI9RU#5&9tT$^B{RQjdmmBZ|RxXdC@{QKI-h_*0t&Qt5t2P=ZQ{%yS zcrY4i!VP@w-T&Q_a*Da9OUiwLB%z$&fM6@Il^Avt9#g%rjohhrJP-H3#rl9d`9VUF1&%_O7 z|E?>QbSQk9HtaFd@wp~+!zxhoHf+gcvlcCS?e>JTM&^d@cc+ws2nba4JKf;si^Mid zC6U9iV$b>Pz;+hl4_24Oie9|vI)Cq;k#Td`DDK-=a1>jR!sjl4ThUXKJ1*+I4~gmq z0t9RmKTZ<4%$8ab6h?diHgKcs8O%9`+=(#I>WKz?hs%Sma3u3mk%TAKZH%a7m^)Rgew1>!@V(#!&s zk*scf6W}tW2P8EL1dmy26Gv`+Ue~#sspbmEk4Le2J;>Ps^Ii>9N7D{3ZMpOhPS@5zKW7XvKa>9WNLGt{>kc?7c`#eKp#Gq|MjPPa2K@w!1z9mnj+ zq~6oQ<#6-L23bx1*qNQr5#TDS_F~JkH~t zG~pD`$`VE1oC4(uv*Fh3edPES6I0+zq)Xj^sexpBs5~z)q$$GQ2C7m*;yBb>0#Cdb zY78Y27j(Xca_K#c0QrfDY*}7-y|T!)G?4z2^3 z*iXM`$_g(edHzlu;vjne(xeYFpCle7*|^;c(*^rVi81=l{{oE}^(RX&34XI@|A14aUn2?Z*6RIoU7Bx<`g*a?82RCJ1^<~u06Y@VTZgp{L0vY0 z^8>xGEO@+m^Ok8h-~0!*Ymi8Q^Z`+uuwLUhM4d5>|^!FiKH7q!P0c&?cV2B1{3{M_f3vVZ%BY zOhgd18&X2c-JK5@gie58M}&)X0KfbI!4da75x?}TB&$&;|;$3;j4YhiH2VK_T- zKeP%f4vfjh?%Y2~A<+F1VRl&t?|TJO(6esq?L8R*Gx0Q@1$*M5U%vrf;3w7n?gV@) z<*e-C+AVbf=nOv+`bgzpv}mEG@~`v)yi5mpebndG`^Irz^-KWMnMeSl4uO8f7%v-W zdiL38|1&W$Y0#yYUe2f|2J{gsDeVdjoR*j<>6ss2a=e}6RaB-j0%1bR=Z zuh@b=G*n@eenbE(R;*;T{@%TNN$C3@M*S1I+P+K>_4WPz(_ZQSwGx0n8wdgONO3s$ zx(C^H#E21Rcj(aJZpsF56*;tSorxr{0ru=UU|AzV9WE4BD^lxFBZ7+Za3{))@Ko0;F%2xmH^Wve3-WwOz?==#zo967BqI?y z#k3z{-~N4AOD=tna z2KeE_M?(Ad9em!SzIDC*--^UQGJcV{XV$FQI0rHt4jnqA)%quNseKto;ja*={AX0X z|7#@xD_uZq73#HtIwBZ|mrk8J-IkD$*!HTcuEwdb0r;jkf<&;55rSA3&_zG+Opy^8 zBqYSv{XpfI6|7sghFuGf9b_axWY8tDKu!n*mySTNYd1=k0iu5c9EgDqYW_{QcYoo5 zSO)rprWxqr7>L&aKG9wq$rYKpcqE{mDM=)alY4->`N{g7w|4E?_c8i!^n$zG5c$h= zgkTi<&e(eY*GvFBUBFlp7&`@ZM1V(=O+eqkq?Ro+Sy|ArV|$F?yD_cb?Q>3{^{vNM zNF+dH5aulp=#rCjgb~5kty`c;@e$}8aT0n*<-u9e`OY1I;BYh;9JT|cy8*!86X>9= zxAI6dtU3~d;E#mr$TW7&9)Z4P%T_*r)Y{tGx=~!K`dTlz7t$~?PP2L zrZN)Pwr!7X+tBDEkE2=>-+XnZA{`m5S-Tpi%Jx9P(F4#KGlD+RdC(=Q(5o2)LaEo2 zg`Z`BtQaVf1sp34g-tk^vkKjMn{XLNVp;~K_8nosfPv7WMN6OeU`8#0;)~2& zA&h_t_+$e|VGkXh^B^}jSD)@z7*hUnPP(tv`qjU-sQ+sx03Hb#wE=xLpeKSzoGfgE z>&LD`nP5!czWs1v#5gYz>=9-Je3OaiV^XI{bc*wePx_zk)8KLXclM=;i=rM|7#`yC)t3$TcG&|B5|;&ThE?7{}dM&-<^ym-5L#fh;!lFQ7kNTL;}9cNupR=if$vlw&Qx08!&}E!HD3< zk;8EKa5lRwD=R}nD1xR)1gYVbj1;0nYoKXFC4O8bR|bft7QtE!EU5{FeZ>(_S{=e} zkCiuJ*UkBnOspe-d#)`R=#!IEOurMQ_LN0cS68$9A`-O|Ug`BpOpIeDt=!x^1pXXY zzI>S;_?vX#t}pm^h|GhfXkd z>^SJ!wKJSNS;)v>-@b#6z;7%csCFeH(5{h@;hZ0xx(j7>gg|6K1VNYGyLWjy5=aKE zqsr>OzFS;L9T7l1p6l~&&}RenU$>(|Tv{@UwC zX?`*~FbVkdJ<+s-4!#w5HpJ#K%#zmO$KQBs{7OUA=YifR@bQl2`b~iUw@3goWdV-} zOk{xFf@CCu;Tahj*TlsoG{rLuSb5N)LpxR$5D^?cd{k=aNCpJj*AWqrmO$PEy460` z)>w;(!Rj&b*P4M&08~{E$ZKm=riGN3SFoQ!)Df|p#N*ZZG{{5gJprFAyV==?8Ss}a z`-v3&JRI1Wzk2oRB?V;Zs|R%j=UOR9^{s5Y>Td-Izf}T|DGT%(fsqLGgb?1pfB(Lj znVIL}P|7G=M#6g8efso9n{fuq3JxF6!Rs*{8Hm6jS2kFY2sz%gwbu3b6+_0anC>gO z^q=k%xV0GxdVjbhGHVhrn|4B(icePpe^F5h`X|;hzy6LL+pyd^x&FX`13%$*o>g8T zS8~8#$w~8#Exj5LAI7QvmH_ZuCjcV}fFu!wamXMX%Yr1z3R<;lbx}-AY#UlW*}Hci z>~r^LZZygWjvvowx22`!wh_@O_}vJRb}GADBC`Kk>X&s;YENRQr-fIHpkK#;zhlRC z0{x*QM~*DhMf)lpuqzA%P-(bU*#IAYi&4Lk0OW`uL{J*=vI5h!O`A5YI(P0o6t|R( zMlxvKs#PXB-Le=Vv}ne1)&@w2;-Cs>@h4860uAeg16mcV`H;$dvR*BX3YT!0?D8~BLq6LptQ7%kpiW} zd3h%oXzI%bMba?izoy`%o+lxabO%c1Nu;DM0zF2xJOQ3sl?3<$2liuXf1(T{{dR2M ztwk5)7L4>cIsjL3t~|h38Pt4HecuTDMgj;p5hwy8&}RlBV$iNl@H~T#s2TJ`e?b=Z z(%TRr#K$KxGQbW4yQZh7vvipTjPztcx7wAIc)NPu3HU{sMOauUwkKm)Do$*-V+ zo-RaKCr=j8S}g{6y7hP#N*Um>MY$axqbR@E3)X55@I@qG1$twe-&o@}5z(@$ZTXt>Tx^)Y5(zU~mLc93*_-;rFtr1|+@$pHR8KmOjl_^LL?bx{olr3lr zN{AdtN1$`-w9APT1wKDA{fW!UD%hxU{omUIYVFKKeRsgKA=fk2NMP4?>``Vy7mDgA z(lLjJ5=?J^6tgN6qeg|F;6c!fl#|Xa_Q~RSjpjJzOpLbhrcz&(ns{Ynh{YC;f zbwptF6If*jvRC$8B!%V({AM_^+#SiFDP4&eaFzghAO`3f78VZpdFIF?;|y**GkI~UrN|BuIqZgnA z34u;oApvi*F5Rxx0lroe{52LpHv-;R*Ng1Bji7HNfYTx?00R+N5kiOn=)8|ZInU)? z_5ZH%D*AbO(NpDIYdOH@kwA^bwc6rywVX)LtLq!1eIo&!F(rZwA%uvcyv5^g2!KZd zAPIgM0;uJHxkf+^wKfsH5%i4&@T(vKQJEk^3Pyqmkp#X>2B27g-;M-ih@e(bw_AbT zu5H%{{6+%!Rh0=~K?po@P;3A%8~J520NG4{ml=p2Q!VOv)uJ3}yfhNPZz4MY3jixN z8H7{dD;CjJmIzdf2yS(S#wg!N0F4)GLg0~t=)Tp*cEAUhL}2x?lc?SZ_(lR~yhs34 zkt`>`1?ZiS0oX)wyA-|=@Qnn}cnJbA$bemO0erj9sV=@px|ja|0000u&X?-dpO{*4hQyB|riMNNfVJ7z{Rx4YuPY@z^t-WRl6``;tj!^38n7 zmtVe#ofz;kHjEu?j|U7GFgu7vpbeo7?R$4iYDv9l>3w@S=hk~w@7;IrefPbp>TaPf zyQos_UG@I|bI$*4%(iV9J&m45PnYJ&8vP1JPot+x_moTjVT_G_6GL}mihp}Aqn<6v zZJ_wK>a*CA>)uP)y-c_0_9aFU-%+ zFDoc0sLRXCn?f%(X3Us*Na$rr7tgXL<9U+d0dc;+zkjdy-@U!Pd+6%Gz`*$n7cT6^ z*S24M_0{zr&<_gNb+^d~1BC0NWH6KmCb7V%2QZ|dR{`FVov*+C`l%Ha6?G*gCD-NV z<`S5vlOSu-IkY^>wr7; z;hLLU`DJHk2Xu9H@ar8NUC`3fjQ{TB*PEJ7LSJ7Wba!|1+wi!#fMl@;&&QD*8ap~V zb|XPFy!-CEpLz&@&I7`~!^puN^$v#40~jTMp#{AT>^Un}uAEw3UA?HNsAvU(eLevl zEqrNtc_oY+Hy$c0D`3WqnNU_%2DP;{fRBNal45>Ir`Fc9$v3C#DUjRzdRJE$g1j42 z!mrJrTUJ)$BLup?tgJLi7Ukt7oJi<&=1dEJY<+zLoIigK8X6jqK$_vysV2B^;XL&B z_YqNSCeNX}yL)$QYwND}-h1y;4-pIqq%aUd3NjC2ln6!%;GzP30PJ+RX3d(#<>lqq zBgn5su+K+VesV=c70jGD6Q)d=!p}7|H86H;5eMqov*!_9{m|Y{mbyE6CQ!Q|7cX6~ z)1~Zw-Q&eeA_SKh%F9d9+bDwK;zFpXDCGok`gAiV2=W&8?c0y!P>-Lv6+d(D1tf$` zNI;uTpFX|*{rBJh)I$UV0x1k=JcUs*7$txa2YeII6X0{^&!1n3=erQ-D-bZZ=jRuc z*40geY15{`WtYu?>C>kppo;j55!lhKMyuMH1iTKMreIcs-%f@G^5>~uD{-BOfC!EK8W%A{0S2#!Q8oXVcxuX9QfpZcXV{Z zxpNl~^cOhjySjQjU<`|{^lmLbk_&zI7=NoUlaTMcl9(@R6V^}kh z6G2VQI3C(GHJ#yPuyyM;yxs^sJv}EdRCpUbgXh+*TSsKjryv9!8H^IaC;?;*_+B9| zY3Ua&T14*sml5<2;Tb_c8$rK>pUL`lc6Oor+ls(%=N4ZEu(&b0Irvp1pn9$JNI>sJ z01`l~5@;Dg~5!BU9!Z6`D_Y6M%_+yt0 z*0!{?ynru0+_GIn7$5r zd;1XdZP38GDQwqwVR zqYe@DNi+jDlQ2pIqXb|a<9mUhi*EiZ*UewBU=iGP*Ik&ho5rm@Rndt;Uex%Qta?dF z5eGe0^SM=gDNrU(-Q8Vq;>1ZjpWxRIA3n@)cQ5ntGt{)VJAVy>y0d2$e>DT9xSdY* zWywlnI6ZbO^k67K$H=s4)A{G8>;Hc$PbVc{ar)~lFn)Xu zOr1KFS1XF10)1~J^qo3&nv+5_5=iMMm6heNV8I+t2z&PI=R~k;*G^6ZSef|5+O=z4 zBIwl+!QiEyHMmp>KvlTowfGM3$0G2rNdljWbI8pn%O6#s%gM2L>`vkw0iXWuJ)s4| zSobgoJ6Uwr*~zW>6a@TujJXNy9ORPUieb#m#*Cpi0fym1^7i6lDZuIRKxY*A!0}u} z(A)H0g9$@P*KN8^ng%^bcs(Kl@)YRYh=gz&-XMn%`CWo9mwAxt)^$o;fkNmpwhyGUy|P2J`}m5a`^C=ejx~ zggUg`bMX5p^^ifILk7+KQdOxaOHtB^L_;Ehg$pkW5W)Wa`~Ta94I6eiM9>=&8eCFD za48W0)5Q6t>Q`g>?!VxH^w2fetbosb?sL3?t*z}GkMRcv1|t=|kZ?z7Kmt9HfO|sK zn&-HW9z6=@Fpej%&pvYo>Ja=sz~gf`*s-+M3;F;Uh2PCauYuo&)|vpBJRvY`h4VG^JK*dJShCRF19pY&w#+Ey4^N-PLdc9besqZ3&C7|!2lxsPOm4$zWbp`7rM4-|KK9eNyStS6O)lUL{>C&ZDSho9Lalpnyt5@F!*piC| znE7(}Cg3N~ySaMzM9nQ!xrW_sFc;nXg{a&IK#y*)g`fw~r6<6n*vH*wvX&kfK3mGZ z^q`Pgap5!AGS5WzHb@5iZzhr>VbJ}X9a6ASROCcJgwWBE^bGc3M&SrvQg|?L-h7xe zX>#(rCq0IUfIKwQ%*%ucHE`W^SMm_ynP;9ya{dTAEHAu=B=DCQ7Le%BEo2S)HQ54t zlmISHtDpD40}tGT;?ocDpen`dcwkNBZYGuLnii{2+Ky`bT{oYs{mz{`QFwX<#$f7h z(Wz6g0D+%O%TfGJpf4%$gPvZKtUIdvBH)J___oHfyIzSMfh<5l7(@m{enbeCM+CGJ z!9NTT5#V=7p1}ot>>iwjuov|K3ooy|_BuWcCE*n$Y7E@)z%2c0!=go(bB~~*;W+&A zm;alG1vs$er!Tzl!cz_jbSw0M&m;+a)(AjV#Ya}Z8il?m#*Q7k=AnnagthwDpib4x z4zS@3F%0+$VVYkEqiec1WN{ z;ti;(2}cQFgev@8n;uWR+c{`If(i8fTjo~U03&=1ETMc)N}4!{2l#`3Se`U*Cx zY=fl;cmn=-r}~a!c{-p8_&$)hphp*Q}$U zY{B04Q>RY-k3an356?)vfxgdTrr@(c03!HvWLE#-hrf(2#5Xy_cA|! z3>WtDox&Os_}619&vo-T=<&ei*4_mP#qy~758^p#>4ysV2G-k-DE`@opU*ZS65VG< z4=aa=05b%nAtaxVovEL%oIFg8g9yLTVtJqa=r@cz#E`FU{F zRf}*=!%TSXwYT7nH(tfmW83>!K=hYbZhb}|0+lv!(UQQ$P5|D59_F?BSgU^ps{6mh zK^#-Q{q1k_W)@=l)L1R`a15%@qa40lo=Y)4smGf(ZS(`5+w7#{CE)f z#q8 z33&uZafm8SUb^b4<^1<1edpl8qg*TuBLUI|hy;iXe)X$=M8aQ>Uf=1TKlj{oKg0)g zNxXrJJ|wsp2|%Ut=U#Ee71fyD`wy!KL$}>nFf{mlQECcwuSeMwA_AUDbxs2q zo)Hn8!25^@I*`w5cTrQXW(;}A@Og9-~H|xnrgfin~nYq zo3T!Nh(IhLx+qEDVj}>h%76FWcVB_!q>s;@J$wFlzVjU}44plD&MD|g{n#S->F=fn zokTmU&Ex#1pMDxTupwt9R`4yvA|8Iwp$g4`Pi4JiH8FiRh$?lm0xw&@HzWmP;OhXH z0m0D7>(GS=5xxf^0G9~pyLCf@e*6vhW7zNs5AB)r0i2~J>{!&s5F)0%9y~x2w$!CkA8$?q4GyM0u_Ync)dOZSi{V`+3#EF~;NF-dk^h$nAkPftS=U&g^ z6!}0@@(QoNei_z^)W9!(@iaCUZEi;|;0HL7>=}UsR3X8LCxMHF07UR73wto$|6Wy9 z)!$GJ|Eg6h`H$P)-sy4mK{6AM@s?at`RTNK_il{!Uw~3{^>4-gdrIk1oN1x3#8qhe zZjcNH&>I+5;A=n}20B%o&lHwDYJpE|#XntbK=251v4W5nTtTA9iiqG0{_Y>lnF9>N z0V*b0J6ODUE)vLM%mBTKnSz%|7kCnNfyeOfPDx16e=&)J7Xtx^ zBe>lrp1iMp?Q1_q%m2`y{^`HOO*%7ptnZ(vAH1X@9sy1?nvO|Sm;x4-?50txg$FjY9B zi-txz0Wg)=hxz?q;o7Vf|N3A58y>@xxI-hkbS1nL=TmAg!Tv`P{J-H=|E|4zxw@Zp z_niZWTzyjZ1J!!NpzPZQ?z9Q;W5xL~3Vc|^=Y)|0i&5F(wsSS=70CQf6S-koiNTB;-A%T%f05b6Bxk~>v*IYCI8{haQ|Iuh|HHm(z!5rl3({K*@`>3Gj zm%sc4w4ufSBKGFBDh_CP*@J%>a-$1)J;2k@XUb@wl3;M;L>_Hp8MFfSiY7vM%=8IpYNgb9%A z3>+bEpdGb=bu(r_8!|WEoEp?%H*Lw}*@AL9cPS_15cfcuXBU{`ik*UqSo! z?c4tt+rYOe#K93s0waw8wBWz?+G|U{@r`fry?Sm*udb@gb@_4GBs~B8b8r{_*d+9k z!k>d85s&rB>Lc(2R)2^EzKj811L8Qqw=)D(I|6aqF^DtDeUv2_<9Gv9JJ5&5VJ9{j zZN|;aD_7o#U4k?4u(0{ef%^Irpxd0heEDL`63m02|NNI&Jhq-l;7>dx&^gjcfc}Qu zi^1|g`si0mDa}W9-EM7n;$Wg52m95TdkH+f=c@cqKKXMv^697W9qiX9m!DVikuapP z`opN~4;S#`_5g&%`L@CL7bV?iXXy7J%ioP*V4mX*jF~wT7GtKTcKddC9fjm3Y%aRy zn(O(fa~#jJ|m9kKiQ%5@`th6z~7w2j7Q61o-#xhvhCmrG`l8b5ncH2Az?r z?57R!Z-1$D^}cq1W<}Y zNEUDsNF0MM2|&{Dq(K6Qo&`vN{}E2SPh#JXfBYj@ zj)gr0ezN#E=;~3bk9zY5PM;3#%^!-I{ZQZ=9PsG?-?Wh@Q(b?V76mh@4bb$V9wdQ0 zObb%oKnd0f+^33+TuQ=805?8;@idkWD#Smp2QVxoFkD#xS%u#?T+Z<_j(M6-;HOj{ z<@pmIjRh`qa#&LATeof7`akk#&Enaj0c`c&i^tB#=p8J>%E3vK>UbEieS3lgRB328JMg!E z`(H7WaQF}w3w;;IjS&gZlXNMABd4;HNl$0QCT>?9*BOSSzv&?(;Q=OP=GsPiyrZYv(;WY{h2~@3SGi zBGBkLA!1^{w8i-T-0vaI>oLjmRIe9yAMGvZ#*ly(ndIOkskwMwjmOsJ_txT+p?W?I zni_;i9F!R*I#G7u@Zoy+(?9(lu0oqJ^Y+_se*$l&o@9CwjhdpVCV9@Wp986>RzrTW zh6(&W(GMGYeuT5%vAp-Yyuwe%t^|G>+ePd}6M5gj9RJtQRU1z~7?yofbsvF#0Q3H~ zVv&Xt_+ggG@V)?8C7=TV_;$8|FMK{*@;;`(_W~%L@5oksM{&qDlfNO=2M`JLV>W=6 zoYZ1ia2s}te*8X>z%gEAMixFn0^kF`dtANs);hjc?oa;YJGkwrX6AzrKKR!P67UiL z8)_o3h8X+-#sASqAN>!sq~p*2?9Z@fzli5m6Yfb6{Pf+r;Q#PLwEWLM4_~9QUKjkN z^wShs1ix(Lb|~=E^agZRUj)7#1^Aj6I~MQ->uqGA1#cxPahh6oJ3}6rDf#GLuNTF@ ze$)au37{@;8)gVT{@^|A8>r{P^P)v_d98pLhPcNWkpPt#|FeJg4b%`O+=lZiz9W#p z7zqi$P?CT(#NhW%^2vMn;fKG913Ulxd*Az0zAcspclxKe`oZrm z?@aMY8~70|iOi*!iD9-Xwv`kH*_!uC3-}SNx2*|b>Sg7QlqyA1t?!JV4Opl3P z3?u2x(CXKs7C!B0xR2>!Hy zZ|DukTzX@xuhI+*-pk}UwuG2W8-Q$$7h@~F+c1~j)_6ZUJw^xasJ~&HV}e2fP6F6Z zK;gh@fduBxoy{-(3zHMN0Hv_W6S(G@Ww?g!ZkRlI@*kt#wb(-fZYbcAz>tOkxkCv2 z4X>yMPey-Lc@Wr7WYzT7B zwk9Pd%jJi4)}=Gni-lwtA}Q{MCAfBk@5ofWkDGE#_csjtd_)5D*nF%UmWq!5{wc4%~IuYHT5HC`O&@Z*lLyH!wXYE+O@11BPUx zkTpc$_r?9Ruw&-TnNuEq_)FN;mzd)g8a zV?2I4_{@pRFP|mN4AA9?6Q?jcunoTUwMVhUcyi6nH{bl19xtF!p$BB$8?dqreo^$x zCDE_Cdfb}teCNA7-RJgR%fL_Ny>!qK_#c1#G5Fkp{jdPPbQ$=wt?YwD*|!xguGH#B zND+#pU@JTYrg%L&VwzIsYU|Q$I*?@JJ0B^pN8iJMCmfiI#b#Gv;n}Dxx4-WWqx#Hj1*}=zm-AY?`CE(Atvahr3B6yUdmSKueCGZ&m-!Pq~1HK9*c1T>F zt!#>&Wp5w~@V&H2Otjeq`1Gjva<4rci(*v?qZwcZ0nI(ew#1opfmmZ@BZ%phv|n7 z!gctCQ(Zp?KN10@``8d#60OyjfSY+Nr?J%!5e2ot4>_nK13yIB&(wDwTcwXkB+5dg z7-0&X`zRd1W0F#YSJ47lJVw{8dzTxnsZ%F&Z@?=Ex_?tI_8z?T)`wgaT(<053>z;0 zceu=EyoUh1^#M8A4h5_%fgfaXKkj#*a@SpVa>bw2QE>#b`z5(?|C3Mt3@WgOpX&Pw z{4~sgcJI+Po}plV5aN85)fZiTCduXN+;cIkkh$Qp`)mWyWfJ(A_|EMRA~E&ojKFPR z+0*%(`q2~U#hMdZbAAJf*vA`T$Bs|<>&}@oolm3^--|A(K7_-T)YDIueyKVADH5O)ZR4f;KKS5$Xxz6C9@()YG020; zd#T8WhIkB79B6A|d0oCBLH6o@N{GOObNw(7@Cqe zn2Xzp*5G`B#)AiN$><557Nnsf{`6p~Mt~ZJhzJrZ>&vfQy?XWMJp|yAK-Noztt~FSm{ULr!S`-C$~QDl=v26a|Si{P}^nz{8@|xO#!Y- zo7o|tv%^Y(V=DW0NSI;@{B)}HqLdcPvJ2_t`C|b;O_8w&>jKCdC_)mr4GCfM<_*cr zz|^UeJUW2CM&RwYKg3>v$-F)Q$K3zXtXZ>WK(I0 zslmss^n2oo$Ke`ur4}NiS_pnR(G(wWi6hIZuq>8`5Yf$6#PXnV!F6$}Q6Wzp;A^}K zh*?u)6Z_oj^hIE5 zkpKIQH{O8!#s*k@_;5<`#~yzhTQr9vM;zwrhqd*pVsGu_->B|>I>48O9GcG%r`$K2 zd3j?6f0$B)yo(T9hbcXH4Ne{IJ9;G9JveLDH05jyY9FTXc=hT#Ff~~FXf+nfNCd&$ zY-9zj41wRP`t!}bnD%>|T>tB?yB5LUiYdPVpV*hG^c%y!h(x8|tMGY@rwXw#h}wFo zzF*hRpUDHjHi+d_v9jqDpV0U^L@)${0;FsMf2cqgt8XC7p`nSMKw$uB0tJ{Jysn!z z5pUyq0Ij8)II%`b0*QWs4|ymsabjKZt+(DfWT8N&JpgX~vF-83;^MJu9(w2@Ugk@S zJKcES3x10E$@QnW|JT3%6?_5J)EbQYEnMVi<78jo8)~fuDE8O#*eaYei#ASRQ~K!PBh$Kf!ME3ut;=gzJCFQcwN617D^kWN%8 zOyR&+zxovl1+GCo;A%x$Frx)RRtA6%g7Tk-E4%;K%9SgT%_i_vhqzV6J-K<(mtJ}i z#$(KM6~_8G*l5Amac3&sXHeM}C6>2UX+TBz01(pT6CxPu-2GU_1 zHjROayB|puMDKM0Eh52j97`LVx()y?fx(jT_(|?C|4z`Y6eeH>_@5Dj|%*_-as6|a46cjqsIBh#sx(|3I*u?>uEjRzCGv( zpXZ^#gb6jC*G;_#4Zz*Jc{@D#;QgqZRgJ&ro_oFt!3x2Q)&^K<0^ghWC*iLObNT<` zOJ90~&+Kb!JMT;Px!@;dl+u3W`ak{j({LA7_LY+sfWorneLI<4xSe&IY^Nv=Pt{xrRJ5M!o{yKf;DHLTp%)}#pS2qIos zrT{dbA#O@fC7{`0wp7%Hc;&fqe?t>%t4q&i!g|L&#$=?>hg!e@YFYCz6EL=|6;7UP z;GbvGq#96l4X#`FG2C?1N-hXuA>AK)!T>QXXk`MtnXvvj_(7**`|d;3<J|5K+1-%6?qn%ewJQfS<{9S|;-RBQaAz^#MJ& zsj8Bu31ju&!Gn9b5hN`jvDGR_0_)aoMlohf@!7z z3^N4)>Ji;hGdvAG zeteZQ44{U&9Xs{_b%4he#J z=nX8x>xT~Q=Yk;h3J@;*B#?N@=FK~>f^a&nB)bAT3uiy-ApnsChAadK#QtdgSCRF{ zamPIEH!v_LiT!P!^QY@?z4aDMMloa#PV=!);H2p)c3)psm|BZuqCl;Wf-Y8xg@P=Syi{0?DYxUgf#UcmkU z*9*7`{51h-4-QEu1V~!{<;$JQA(d*E}aqoNqFCW5`_kOaXrgi768z zBV`4sL3k?G1{^uEpWjBa1V!+>EyJ!CP*PGle;J;HFd(f-!f`zS*ZO}uVg0K)0f+-W z2=vtKLxn#rEzPiN*G^c4+j?n%Ck^zZKL1SrU}1TF0C~>dWZai*)?b)zpn5$Okp!&1 z;yH2S{4kGXxMKN?f{r=2Tg(rMr1?tR=RVbUmfQ~R?W)&{NOXgE-1j057UO*it(Dn^jOV1QNo4sqh!Ul7!{?WwE#Pc?z;%QAd7h$VWybVOBpK;IokR zV#68BM|bu00C!7lgE(c$B`PceF|m-(CnF4(WCCyGJg5)T-o=QSIf#Y&`Xl^xs;etK zubU!)o}NCeA2@_wz#Uj3T(Nxm^yzg90*Krr5H$oyrvB>c>i*s0#Y?ccVG4>F%~J3u ziheA|`hN>L7St&&Kw6$_gw*pfol4y#Ubzpi2q^=Fh(j zhl@;wE3drrQB4>SsYf7=29UF8(V{AJ?H|1H#+&$T|5V=JPXd(QAnX6~%P+x7RQfB5 z66=%)vQh3UQR3qk|L8IWU?SovmT$5Mb^f7NKVm~%??5l$Fz*q-(EDag zxmRk!fQ%AAK>S;{aN&0*O`41?iD9k3YBm=<--tCop*COrqj1 z!v`TtGtY0Eh;`9%#=K;tQ>QjQq`{z&i!lCihiWeyyP;&Wv8IUtsrV+MLpL3&?A z;9DBc!&4iu5b;1-z`#I1_X6lFBLT_?EL=E`R*h42;1?Y5j{)_(0G25az&Gg^JN_P8 zv0??c{O#?X(v7}FMFo!ae-W<5^j`(4{?sBVF7eFNK`3?46&Bx)FpWp08JNuTi(2lh z>bMJgue5+a9NvHo#L^?q51tf$B^Ey$DMA)gw6x^f0An7}*ZJK2XIo*+xwG60IDPsA z2Ty6~Sg963LOY$WzWN#}y1UsE2DlzTZqzWqG625M`hW3@U;G?SESY@64L9&nU$Uj1 z6!(({(ACum2XLR?t!VWbPDkO^KMMz;ZD85Mru%5`Q=|b+ff^tgg_mGDhRdX8f5a*K z$@tkX$f@Fdskmku|C`40mL?3)fmoF(Fv(U(UauF+;uqpCdg|0kULIUBwwUk5^pim1 zI)wo=VY|4vWZd=FU%yiD03w$MNA>`4!N)^OmtMi+`$Xz5xYX0N{_EGThZ#89tK1p- zIq2Q!lOdNh)Otg-JiqQfF%4)^^=F3(h7kvEgj)S{w)kXPj}bwyap}d_J1}Hkcpoud zSH|8F@M}|gR(SeQ=FT$~GewB!L2QX9FJK}X&E}Rwc8pewdFuk)>-+Z~Mh~GF=FMA( zEya^=@#p}t7KvC9z;7*Bu%L=ke`Nh>!G{d|^f}$tUbNlkcI<`b%D-# z^$NLeO20=zz;Lixi$U;dz9?wJk zjDu&}pbEW!B5V-4(0-P;&QmBLf}bvT?%c=6jF*>JW6SWe+rZx&5Gf3>Oszk?Fpe>L zki7oG{Zhk^trPxYPF_)^`|LFH{5luhbYX`bK^ri6y&-MZ ziSl~hcwGT}ul4q&^gJL|0!Zd+TOq=lsj}}r&(Z*2w4q_jz8BmQ4?aZ z3$^`H>hJL3L$Cx1fMNWf%=>51GMN_SFhgq_X4ykWeklmXWQeAnfo7+y#_}P+4;*Xl=cg-4eV3gX{vZaDh*ma%Ac>rE%4tOfvvYwt_MO&1ayt{Tw#UwPU8d zpBK_mDqSQ2x=LOE?%qJXt$5mest^jM17-9TD+J)~Ki2y{Ja_JVUXz>}_3bbIaa(>j z;*{U3QT(HR|3Mn`k-@4zF$KpWjN7&q);0{>M&rA|ycO8{UUm!++nKQ5aqja1L2{({ zL_ClSWqw^CXi=q>U6UdVxccsAup$b4G5ZiyReymR1dt1m0q+OrWX#0-&z^1JPbt9# zIid%^cqd^aPT!i0c$`SH0&ew$0IDz`mIjb4{l&Dz9mF_P`Pbh+0AkrMUAp7GHgDbt zS76=0MX5jhgED2Abp7|H(|Aqt{HB?LINCruAkNenu2{eq1vHa5S`^SkRY|nan)D$k z(sK}2dmomwSJd-I$`oko29i&+G}#Kz>-9ICglQB8w4CPYbSesxg#mq7SxY299(h?= z+0~kKpp~u$kh5mZnk5AVg_G&c5~IFtN!f2=&j0b_jZlF7&U2_a2)mwaYM z-N$6szlrrW$rQw0<_S?i9OpTi^af;>*e8a`4{D~$%j>)dK^q8`@2cHtDc~Ta+&2v1 zCZeDRMe0n3<~x@ViH+5OB@IWRBnM{)4D|B409RKNN#NMAMji$rHpgKYFh}79MAiTT zJdIUHcg~zS3l|ucaSuS=_>-vn-?(u-EW!F_hWq_|dH*yllQ!L#&aD5Cv0f%3o~bqv zB^#j9JBCcz4+TDG)q9Yt?YgQzA$4fb{KFL1TN{(JG>KEA4S=erK=f1?gqB42O=RMCv@zCJd4=;CM0&m^N+N3PlK@%?4N@&6|g9zwPM$_XlEs*8`vv z)%{1Rv;9n>9`%go4e_q1j2oFGZnpnNkS?x zR0y;}-2a@KnwlFXOqj@L{Ty%OBkRh(tcE=@@LAAH4(s5v)OmRpR?=&t(uc+JR*0S;i{-n6 zXE1ypM;qur*9ucm2WV|;<~NZS5S$#$PzN}U8-&JVcs>qrP{?8j_&O3`kvsq_{#mnN z!9qUqr@OmH0Dt0=a+P$Q-2Wxi>Weka(s5q~imcAfmel|-18Zx%x57q5)hkQVK(|t^UZx5%k5Au#XE$~$?ZXm|j#s3=j zT=g7F13C?1WU~1uwDJ%P1jvvjltOE^O4B7quS1(&hg# z{kI5Xi_%%MX7Y_c^4kA|`hW1?0a#4#e?|$KX+f&=T%3+T#)6agbb~=8*$S05 zAc~LFA20<$-__m0ADTZVKLCC@ojiFOvj9`EW}v7D#roL-__g%_mV~F0`F|Webp51B zQ}`l7B7l_emm&eT^Y7D7cfw^<@rND(U+*1r%Bvl+d^1XlP^AroRSL2Qf?U*{eh`H$ zGFY?kv&5pi@6Er1q#!2@_>$tEfOQt#c90a%sImwq1wN{jqmpFQ$E7wzV3LYI)%Tz% z{$b+8A_W23SPoo{1wlPMT^x6bvS8*b%3^%7@M#E|9ee~#bJ zK=QhHe{N;9^mTJe%YiwAGg9|Q_>Phvmua46_IgYvutYhD2=Sv7c=>g>GvH%qS&z$MH|KI_C;?gbtIdteCOtoqK zR{;z*H%9`#a@3aRK05^5w&wM~h3jjQT; z!XQq3vb(xCcqw;Hw-cDbsTJvOZ4P3Rg?xO;zk6gyuH?_ots0ji2j_ZDDej-dG__S& z43;yL*5rFNHCD!Cp}LRhi6j?018e3k;SEBTmB%Lq5#UoKkRSx|0x%2U>HxVRSpZw- z0dTg!aYNH6{-+l`e*6?9ihh`n1n9%K@&6pG{b$tr+t_Ffd=-GSX+W8{CwYz?19V0u zy&CW{jW+;>_oWYfoz-W7%Dph)58$*K0(5_CO9JqyN)ICF>3YCoGfgzYqJf^7;7a_g zt!JdyeFt|D56m4_d*z|neTwhU9 z#YYqE-hDvo0l2M-ui$js!vjCcbK0>ESv z>&*n0lt4}39>7*^0zCnoE}7J&mxEhp?X{qn5t0V%%nV=B`vEIMI8^C@6;WeLDyR=^tuByh<%0o-!SbvQ1p zf;@pmNB~D5ycWP#g#Z-)&nNeva{md&fq(}I*97=cKrhuM zqI=gyB;k_7`Lq1of9g06-`kaVY94t96&O;C=iY*a*Au#f!Eb2mNkOS6*#6@+9|@pu z!Zd6R&citYgB~r;(U}|_fOZE=nNkOP_wJgeu>Ju_R7n5`?)g)uOyOg{W$u4&ZXSR0 z6FAY7mK&GjmonJd8Tp`=R^e%Y6*}@es_14QKxP7{VWK5VM--HR-)r>;ySq5pUF*)> zdhbgMBd=s`Ju|#b3wltzM@&z^RQrR7MBw|~h%8ZvqciswY~X}op_h=0UIOIPcmETqKx$H;H>CogDJ1&f-~zk1Hx}?st!>n)zhSYww(Bp3^|qsq`-%|4 zC~s%MSe}HrJ_5XB;cX-UB}i2kK9j&t2lOlg*dqX(MI?iyI?sj9Hh%7FIFi&A5@b-r zFB4&d#OljP!4Cg@=~UKv0Zx}-S0HKuIf%W%zCj=O>48+LPnbmlm@9x^DFn(q01^PF z(m${dfOi|EJOD_(DY^f2I&tCz4lqEMsH@AE)ohJxu2cOp;a#Xm#@2w*j_@~`5*nRvOYcb#`@}@wSS^ca6Uk7?qz_ZK%I}?OqqozP)&|81R(Gn^WgfOlK z@+M3cz7N5OF|{_p3B$Qsh-JZ6*?1Tnz`Yxu8UPpkWS~h0Sh8du$0iDawWtGJ@PI!@ z=K=UcK&&ZVR8unnr;e6#4Zya+2Y$MAJpc*;Zld^Klm9o3m5tMO0Q%vd5rssZCAQPh z2GSJ_wcy8?ukZ9J4tfGV2r;}2{Ki&al|D4MEld{(1AP?0vlv;otergh{6X{%dhra0 z_d!?Q7)}PcNCbHkr=Wf?#`nW3wE^3&;ZQw*HDf*!0PrCpUiaTM&{VHeTRRRtfKu8S zFartTu*L&mq6p}10GeN0JAvQW)zuw{HM|}GiH#GmQ^_`r|4l8i&U&j-fvSd_xNf#> z07l!86j%mCK~n0ul}ERpz#jl{RI4wu)+|2oL#@5e{bw-&Z$JVpJ9rlkKE(t3dZGVh zBR_Y4vVn_;W9nu=E|P&&JQkD(rsWSE#-Im46@f*Cl^l~Y4oQw zfl?6=8+@jemX_dP!t+l2;|IU{L$u<7ScXK9kLAAurt!Z<^%v8Crq+>Zyn!eMJ|?Zj zCf3^+{3hN7S$YJ0@6n^dv>Xe!@{!3v7yD-d_z~&>7&8Pr3jui+7=U@Kw3ZyiG~~H9 z=-#;n$NFyJp+er&nF%r|D%Pr;dAL4&44VIbs+e>e;XKQDTydVlfQb_)(0qV#8Vx`j z0$4}@lV{AB$(cq3f5QDwlwY=C)6ZlQ0G(+-u|+bS>K`*3kcrO#F+@Q-1o%e6qyu`g z_H=1#=~<>j029$L%##Rn@ncj5vbZ466cL12eI~izmJk9L6L)+JfM*I1a-DNBOh~z~ z{Ax*8JDeL+!b5WEz@--b6gD9l29OYl1TfxX^_Annq#*!J04l<=NpJtZ-~CU-|A!AB zgh`1wA)fFTq7B%J?>(w07?vWmBgW-n9z?7%QoOw%_b2pWB_)BL=j9=$r8lthk*&S~ z$Ya$5gxnX^6EMO$O9S+h=i8FlT}lcXLg+i$fQvyI`1LX97358ui6UaHFMU{mgmJE{ z2C-*xA#gAdngb65N=nMGGqA8u0)Dd)08dx~JPjHeP6h9~3 zD!z{jZWa&p(wC5dTtNx}Lh$^%xBd|H9y`P}0tykvAQ`ZtvAjAnAHD7VzJ5=yho1y! zXz$#)Gs#0J3Io3^1h7`GUcE5SaY3j8ES-lt)&Kv-&*U5NWT&i1 zvMZZ2?2#>0cA42^hGS*q6B!}vWJHK#AM2do`CixWKX_g5*E#R^bKLji9(@mb)efYV zK7o6XijP2qts94s#ZAsBVJC2R1I_%|$F%MEcM^o3x!9e*IWRCKxsM{Y-625~Ne9<6 zPa(Zu4llfaHPH1l1v z=}^9!{MLiKblIAzcX^KjW!!||rwgPCgfo6O7pQj#BTiRrfnjP`P&V0ZYKpgPqY7X0 ze$A!sT!bzq>+5DMbw!`d({v=do`Vz&BI4rxggC@lgWD!%!fz4iEv^AVG&*a(fk40if*$kEoVip8=&5bS)*6ArwK0Sj}U*_wrV ztU6%(eKIXdd=DTtzKzTY7Wg2fn?W}L{MJy6(xo$b7h4zsYy7ZsETBrG4m-y-rArW9 zlp!(c*IvWUr;EDFmGf@j_MquQaSB|Trg;nD;gS1&quB0Ql&{PEt`+_I)X><4y^vG? zZgBoXoSHksHU8>avSB&UK7J>?wYBU!fLRrQuBOFaiB8eAn4!tY!l@K--2@vK-S`OJ zZl+59)`brFl<}2lcD6KgV#x1G`(t+xoMq@X-Jq<_EUi;$vCu>f&${@Hi^P<18?rlg2OJMev`{8{5|8TO0${JDNq zi0ku7G!G{wVqSM!-TF)qpF~Gyz^YfGvC8-)D;{nramF3?pykYPms>(U0{7UzbFE|h z@hoBDprx5JBy0lg9f_}f5z_$#A26@>@aZer6b{14|{|(>Q|IUcirr` z2U*@^=L0hvJlJXXtk-v48%}2U332+QAji2rzQv*!&*0wEKZ~d~t(jZ^fkS_yq?{!A z*6&8VaY|+zUqt~w{?TtEGD=X&twh4&^#hfJLL}wufsG? z*|O3>x)roB-{>1PIZ^EY8Y1#Vz>${5cx|j%$oKw4?HXa@Y*v;(KvARho5T0mK{I2w zu%60z!FUN2+gshz8srn^Ph!|28!^6c zW-UcntGi}YJFpmtvAQKUu5JqoP!)Ymj9Sxwk_AB$RRAS?S-7X_0SJ1OGv0Z+=;I#X zOS~psDg5eJ)Ue@o3Rbv>xmYtIP_nT*eEp7j=NduNRRGCGaJiQMHVku>K0?@x=4mCL z@^feV;u!qTm{bty-d&ut6FzHB()$-hJqJ7dq{8&_V<$3y z3G+&{;^J>Btu**|Kug1aGO>9P?ce!TSA0)>pvVO0|CCAflOoBn3Jy6X%o$fkac_C- zbNc}5*pBA@pTWXILr7VkXL0cdR2qg8b%)?)=z8G{!3&yMTPJ?NUrrSq$a8Dp-}2wh z&|$GEmv<2>Qyo{KG)s4y=zZlNQSCc;yyLW!f4C}6pQ{m9|lod0V68M5K$`cg?qO%RogVq2SAR%XryigDB~ zzc$JSGG;N3cwmzgB>1a_t^O%;C>QkNxJ_*vEww3dd)LxhjT7`Pi;%RncQ^BOgbSM^ z5To25*}uy4SidHWJ>Bz56~i=JMzD`Wm>oL}hX5)7MnJzNTt{e%Fkc6#f{cBKI`jjp z+<)c`b|A}$(HXgIXSofz9!InNtQQ{4T)QxCXfVaL90X3$Zn zVr)rMamve+Rs{S{4`JzYvMNg01u&TVjzbY9Dx|F6FS{d<*OvbNY8kJ%D+?yjvK=Ca zgTezza#AlL3_4mlGRX-(za3bUcfABHIQdHUj8Z8~0Z>mxX1^00ZMg8wc_ylw=<*U! zbQ$yOOq`c?JNNnX=LI}Xo!>QrZ;pi})Y$dB;?F80mP`mS6Cvjsu>&qH(^jPrS+TUI zS-4FNGlEjX=>vbeO#YY@p_m7Bd~^rb?}WJ?gPrpCj5uimVhK8$JlmTU3{evT$DPg1 z%&MTX1hSL_a8!jjHdY(bUE0jtdkA>O5g>aHl&I`Wt5CW%%cB1^bOhbM!E~qQw6?Mn z?yX(X-k5ts-ei#Lh@_W73 ze4O%m=T{x{G!mF?Qi&;kL>VZw)BA}qC`Qqf-Tz}T^v9Maa%S(BYf`+cV!N=iZ_>o`D#8Q5p#_o;T=P%@V zAaqgK%m7HG9^kY2a%6fQ#+^D)f79HsCI`!J@)gd{8ROjAr!M{nA87f=!dY`7EX4KubupQU_aV?%Bqf)haLPHDE1rwdw%jVV&py!(ED-}jGtH#c+Ky5 z_?3O31?!mv$O)E!jK=u3ssl_0yPtiNdwBNUNCQXC;PkZ+iZ4b^98SR>w|aM7_q@+_ zKC^Ix`-ROG8Sp#d?Jg=Tz2uC01rm)~!@ya*5$vI^8R~Q69#bvZ-ONUf8X}x!Gr|Uj1JaQm&mU zJiqwRN|stt=^bEqf}I1IU7k$k!-;R2CxN!|A#$Sr0}Y++PJA_@SuPq6h|D1W4;)U+ zLJaw{iEEi%9BL46%Yzda-_hlvA%j3}SvU}+lPtMJZ7T0<9NaYnoz61MGvWS55*o6V zB|A|$O-@2HfSvkh1!8??jWl1ZxyXgBWCzZ9QJqo16%QiQYL%u4p=3diKHztPm0@+- z$%J=cQxZQj?Rn`vfi8v#CuUZf={k7V0V^l{7S!9#7uI&~-aWIDmv_Kun+;(8+2Zt? zl}S)RFB;YP{&xmllAs+pI#H?HaBbHi?SFUueL@g~&hzYCU@k@1KBCk#+ZB<(I$knE zVx;9Q=9q?u*-*_9C&zfOU&i)^!|&gX-RaGD^yDTR)0v%|5E1ZJ8pN8%B~FVE%67Rm zb1j<3c}W}n&g~H+)K$U+@Oz%mBfsN{13)TdbbS2frA+`~_^pKi4V6oCtEv-2R?gYm zrw{2vIZ&PL$y!-Y^W%@BCr&raOcTgdW7$g9)zOx)W3|9Yif*R@Gj~fn% zZnkvZ?|xsV=&nO&pp~403m31pM2LV?KVv0p>qSh!gku1!t>PijSOVz^cnn#A2?3DR zkK2yM-zvJO2%vI}U9!ue2%04Ue65I_O$uX-Lb{?stX$uj(}^~h`=!r^N^mgyNBegQ z5ir;8*I3L|e?}UtY+mfw2Kfxy^*f@ z6Vy<=>R7D?a_ARU@5PQ#T_}^ioD@Bw7$iF}rIembOU2}r0pJWO7I|t|`tQE8HQ}<` zdkLZ>*+>}Ow2E=qr@xpn6iDA}hgL_90g{3q4r9)=Wg?j8=59wDi8?a@lNQyAaw z^^`vdIz?OC7*b~HdimL6G;hNc6o^sqODv*!lH#Btz)GP>@y;NBzqkdElY@gWwqWYX zQ}gp8GYD{CFGaf$9@x7&IXO9&LFniJqHj4H@Vgf=b$37c)cmQr-2u>dn!Y?GabbP$ zu&e^Z5HC1*sKk6>CZPQc)8$TIuoOb*Q?PLyzP^5&`g7 z6awclR!NcYwb@@Xc&o)JWJ^P!z^^BAmK7nA+Q*wqm6z0izCef2$ zPr>B6CYgX8oCm@L4hGkT=U2B%iDm>S>!E_u;vusReV!git1V7CV!h*0*asHA5y|k@ zZbveSijP9g(dkf%N3k{W4{hr~=iw@hSR}J#%;op*Z36TQDz0Q{VIi~Wnp_KdYg+y0 zF)A#|73^8tS3WJiT+wp^xKu7D_4ywze`H8lLd#t(ai;o%e4G*cHlaMk$mu9o1Hp@j znpRT6EFh7qU;Gq=WL4j}BPFJ76#z!VJ9GWL%m8+sWa2dyp`>WQ#7A9frx{)k@R);J z;Uw^;qJTy{7=pURQDDrktn}!iS_DoQ`}&}{IoLAVRt}sQ^F)kb)a}acm*{6UF9mZ3 zHjabvNXH6HwjAQ=kTHj_Q$x#uu@w8Fov1XY<00FL+eq3i9;?_wPJ#O>LPalm_s?hB zN7{4XNymBf{ec1_BezofRdzg9m0j+ha#J?t`quU}a(71tB1WLOQ!ui}_VM!8zEkXc zZvXx_mbqnq$-K1?U$Q5P6#&namUfY2P;QA9RfLi>-_m@fZES43X)L|_R#CdaHYSOO zb2CAL0pb5-u6frRz!mxZ=sOMIKgaK|1u9r%39U+ljh;M>o~YHm&r=8U>2!@oILVnZ zpL)eka&d%?gi0voo#a&~_^P7(w7$%R}u3X+mKvK#wF5SuVgl5q}f_FPp7g zQK#MiR8Y|nMv+iB&hspx(RjEKygg?vipv)qJnyRb}@jK)z0ot@yM& z;2w6`XQhg%M7oGEQP`C>f=g4@+$yA&J_ zz79GS?4vZ)C3Pf0fTUw!Dz3O>e%CDU6%lN@c^o&FDM z?gAz!mkUy2Yv1b}S~}4=(JQMexx@H>z8##fv#7-Ac)yQ;{Virt%n+LhL#}S@^%!~= zwR<)$zB!73W*pzthY@@iuM~h=&YmmrQ#=MA``cMD?~%KvTvBUrf!%;VOf2k4$1Ebb zy!!@e=Ga&UtR1)@J~g%W5ulgUsR_8iUW3#)5lR$&p#mze{>S`FyNsnLM^F{Dh@(*7_DRYn8=z^&Kwt;Cc^=jYqc22y&yG$iZLmdmgEV{sPj(r!;$T zp~inN!D8i`l4$%|2~70_6JQ?I4}HC&U7y?j{9$o~L$X4IVKF@IIMDIzBS)Im4QCiY z!_OoNu)X~Gn(H+dapz}6%1KvyY8m&;+ zc01_CyzxEuRP_z}`Q}vrwSWBOxs+EGoj3pSnMzXMaNINjrbI>HIojXy-&mYhere3v z;}9|PCGd8OGd$zb>VjgQhhf?JH_u}iuoYgCr5&71PUt#z3TyK5HoOw`>&AK8wW|YE zqwMfFzy3DHZPQvgxRH_S$w!-M0{#f$`vozN7N@3yagKC!;4iKyB~ilTa8i=~I&h1t zKU`ugwD6`}DG<~6I9OHTwEo$}c&VhEKF=L}ws$w#nxdLxWbXYs|}n>NP!px+qN6M_J-HTGk@gWi10so%+H%&w6hBf#h| z^xWs!Fmh`tkD<5t=OSR8XOb>UbB>8wBRrWWERWJ7c~o@CZ&C9S-6y7N>L{8J6I9yT zbdh|^l{mDDn^&2jOPLPp?(_M#I?+TMbmUapYf5~+yuZ{);}2~%0jv_MK{9{^;4mox zJbFk*n=&C#fKoBf!rx)(bnIPuqCnb%G&zQ8oG3F*NcDzMAQ@Paqcr=s?NtRb{x4$| zKs84WF`r)G^ApgG7nIt9(}kknXaHa}*TZ<&D%9zrp>$gd{eP}L3MjGfSEujJS5qYf zdc?bGvlV+EgOi%VbOI37b3PQD4W^Tn*({n+_3W z2t7SGUGC@2r!veyUn~qjq3v*4Dr3~TX3>M5vr9|}n!D!pY@lQHdnq6vP~^HL=y>sg zlbLabLF*|1?e%MfYo|mFEOv#SQ?CVD@nIF3Tx0|{c;ajFn@dm6j8w^Y8;W4VONr1v zUE$G-1$+@OUAp;#Le(9ot{+HB!LWHUlGlcm?-=E2+;+v!hpi@=(Goa7vKaKMy{&WViU-GGs(V1 zjRWv+v6G?~LkkcSLZw3F_(?|{p=T|Z2poX4L%;=inlJq-Y)DcfoN66ijAk=3`y~mc z==o9-GEAeI&;Q26)arCG{D&};75ee< z`#jo}6)(ME6||HxMgA6x5*7LrVnqAm8;&D${~TE~yT88=B9yQN0^0okePzJURDwte z+JG`zl#A50=?33T3$oaRd$KUj%*42wjm9ufgY9N27vdA_)#*uFqsO;Lp6JrJ@T^yc zkDa`*k>($s)_hT(=ZV;;bEw5EGNHvL0HjMn%uD{sxUj(+ATJAqA*{h{2sv&>ij2qB z{Sh|rN@+bcGC92JN+-ODjBTT2QAPy8z5k=kfqceSHxU=vc?^VhF%2fu7$Wg4>nuk+DSJoOhPZ_M5^^B1c2%L zuzuTPq!L>;{&e@ub+u`oEZzt(|2x63%+unx-odun|#+H}+j9OYl8Nbum9+wIe{m}SV(I=bj<>Uh?7 zE>54poTi+#n(y9VPgCuyiDjPv2Xi!CGTO&vk6X@i{PUO~25ZWMXM`D?*zCi>eca?2 zfX<4;YWQIn*MzVG0$Ay9kbGDGgDJxqRa7_Jj%6qP6NMX_+Y2+4{&B$jNilD9Wno71 zJCFwFNcU|dcVfCC@RbYSwZ?e;=R=9M6911E|uNGxZG?HTP+ z@tKp0$*YC~54;MiBORlr=$C)DR!7wvStF0Hp}81W9l9+FTY#nd~*zDF5COWzk0Nl5Cb91{h7TWXh_`{23#@QSM(GcZN>HzbCV)n zI;cxS4hsVrwp)6Gh|QEjS_B>T&Sh-yW%h2Y(p3g!-*W3~G<%@DTmf|G_w1G$=WfWl zpT+CwvI~)GqH;bIk>;~I+aWY`uj|I*Dk>(M)GC|nIxvbv#pHG1Se*loS(MWvcSG?l z3%4-np^zd#T2izlbMBI@Fps#cZ#jWSA^(RlhX0Su?Mwt-wI|>HUzuV_a>Cr*i zsW4YkOz6NG-C5|buT^4c?Uyz7G&`=jZ%wmO#g8=ZWuHtt>T`go*%{%e=Izem&q;q7 zxw*J+Gt$4NzM#2}?{;HRZe9?gHsLVb{!PT${VtH_*nV2N*92P~f+66j=2=#^d^m;+uUV9CeJ!{Ry=1 zK($?YZ%x-WspapOV+9KX0FGh^9vZ~*`S!}-9VykhZ|wkU?$Kp4==~fO@TuGL-!MbsArjOsN(HVKtLTtVaJ}AmwvwQ4Y#9FIN$j&d^ZWIXHT`qWuL!<@M01l zFyi~EpNeG+8;A(U9UdM^PkWsUM*W*>wHcR5A9x!zho!|N3uZ187Rh) zlEY~;$?@;K>R(h8KPGIEBqOreN0YK%7 z7}S3T<{l3td3di+(n8pI%F@l zsXTqDc3VLhEz&fD@yvWLeO4-d)BH)JQ&AiF8gkJ3jmc;VP9}TTOy)G^dtpIqptqAD z&v3SU^rE?iEh_+-^K9QpS6AG9v4gQ4+aqXD(YXnX`H6hNi7E$wxj32?^nKNkXVtW= z?exi-;Kg+Rk&saG*QhkCW&vJx9a=i=tn-yZC-=1bf2XR6k7y!8>-h{o(EA9p51ofEffpb1Woa#v&QsZye=lCJ8gF5E z9L4f%Fqescb556S?`O#gG`i1(-43jjwmPGf_3y>+Qm}A$LoI*}0;h4Hl8dHX90x?( z7wUHzf#9-;0;tsEItGm{chvi3=<9V72GUG1SrB<%jG}mTt{!{Nz?;c&2YKs^SJyyS zyJX$OSWR8XxuMooj~P#66hW2AlXe!zY&zIAdz+^wHA zs)hBY$^uP{q*!mn+uvS_zu8Jwlnqa#DlO`q3|G}%@2Cz_d>D=-d)VfPdn{u zp{g^%zWJob2?I{9$YdCX)d=5KH_LSstMUZl0d4%~j3-l<$tP7-g2@jirJd(Hr8v0R zA(MBhfJ4j1l^*1Rr19zNsrNpcJJI2{YXWx7&#PX{_4cW?&&a@&S4%wRsirEr|1Ky4 zEEFB$@!Mib;RFy;2Ryiwhd%xLB>ECR-r(cuM@~c>ZjK(Mp5GiGHT=%^?N54cA6uM+ za`d;0Boyb<)xJVs+Sp0j-w&2f;{|X!C?^3A6fIiphtN5cIuq||jZ?pPjVB!0%`>1L zZ;XtMAP+(kxo8dSIjt$I4dkp@aVAv8S&2Zlh;Cw6u6mmsrStF}?ScCDTN!79v*3Is zZQzogu5SO`|9K$4%G3wA6*kC~%Ko_0?Bvr;Kk5qX5d+*wpjf}E21e!MV{QwHiupUI zyLQ%W0=ev}h>FI}9ASjan@rG$5$0M5`UXZf7s$I9Y+NYX0hG^u+_v-Mb$p8kpA8NIp)MZ&`-C*Rcg{cB4^rk z_XB_fuiW@$5o!6~SGF)um*6^(FT0y}0{^QS5&7mQm$O%0M`q6DAwK33*g28=bze)G zOZ70^o&m>^U54AuB$se!9?a_%ssG!)?>qLhLRio5oJ*cLy9aJ8dpjU zR;|M{5Ec~cYY%EB3gD%maZz1r)7hqURAhpBXFFeIC&RfJNgG0>()E^yDH#{fE{vk6 zzwLBwckK;CZSoc$H}UgKd-(X8q2>;kHnUbMvaD z$|d#Poy7x=Jp=3Vzv7d5Y5?mkBLdQQgW)FGy?78Qd%2e!K7#<*6hIHE9=G}BOws56 zE2?AEs^X*qgN2n9Jyf=G%UOCQcD$@pxzzM+>bjXs41SKFp&xwHsk>jxaSmIZQfxkZ|s(^(0AZyD8#n9(%0^|3LWHZUZYU z1cmX1W*nV9q4f5YB`4DCcIBL3L5-~RjB*d!H+A@VVW75 z0y4VdQm>yw`72?~O0x{gH9^3#`oP8{aZ$jBToMnom(@d5EK#emQ z80#Wzq{YJWBsQQG$v+(9q_r&cuH;`?WPoUK2%5U9aq6azJBw?Or_`ZTaPPMtr(E`( znA-Y1Dle<|FqW&MB`dRP(0RX^4hr}C?Z5%}U^QphOJV-uruPZ2Cq_PzR!e&hDLjluVATc1*5RcF zF_rl=$rbW75!bZt{?IRds3`NE2f@pev79Y^vRs+`d0{Lk8toYTL5XSax!CeVMa!3& zlDfKKrS zM!hJUny;{A^{e(tTj{yKTQ72;m==I|%f6BdIJzR(g%j!w+-_MT_86@Ui>0E3MUvN9 zN-HYaZ;3Vm`{?lN+F0YyPB(A$L()ZxC zMN*|eYx?>ZhmHF1nj*nN#?#LWhSM54d4G3QU;G}Dtp4M6FwmWu5yy{--ErHR7S4ty zGmRiAeAjQ>XkWfcu_b=1pxB#j<|WcnuksN+USL&FKs~55^x`z3#(l=o?k4^o)7aam zv}3LsbYcHpg?Y*`K(~bINCwAqW~K22p$Ha+_><{_c}#5EPW>xp(brUyMSD0=1%weV zFeIkkq{Us~|H&c)*|-Lj{%?-Lyu>p0zWR6tzuT=~)!;3Gu`NQJ2fm?DAt#Q*oe4if zeM6~;A=v=@;y)o=LnY=BsK#8P>+9FAPv?xtrj9a>@*8Y=8iS_%dajxaGOs0)c|Dt* zS0X{KQ7|c)uA1xcCvEZS#HSAgqxh00U%Z^Gk~T*c0Xp8;a`&qSHJS zq4B2}u(N4Tw(-zt3e2nsQo9rQJCZ9`Ex7RzPcQog_AOJ^?@mm-D3)HkHJH?&W30;Z z)DbtjQ_oKKn%&q%%`Mr_Dz)ltr^Rj+N~%C!J*FQ}1jMO;T%T&D@Gwus+PoXmkThrH z)?hYcIlfD#IUU9%(YHO7{t_m8`msCr#p?^_TNEnq;PTa?g;DQ{T`B}7(7cAM&bo=r z`u;mF5q9+(%$pVU=X6I9P15j`D(hFq2$0sGqPh1a``TJtPXs$({Z&v#uDuYTo9@v8 zN!bt*-Z>0l7w`=aah)XH#)eU=*C7;^`rtPTDo~TG|7C9;L#|!U za4f7T{57h5y%2j6l*#Ba^tG$*Q`IU(jNYrIjTy?Cua4o~=~3PJa=V_4mc({`YhkbK z{iHLJ$5I1+;lpnl!se<}^i**SvGhzAmkIr91Tg)tVQp(nOV?G}(LU@L7>j zHg1J6K+id*g8k`edH+kw1v>27OM>L-ZYs!>iPt1&(xcZAi7VO~eS&MWm;9(=d?VP{ z@^~a)fMxd}QnUHw&yDp{E)-83+OxV7Nj73QyV@<6C8zC*lscXH3q)u39vaP_x@bWT zf`Iz+$r@W{JDn6t3W9Vr>-PI}GxJs`NRxGaYwM4vCdX4~l~rqYG-v@-;2swy_1{_j z=_yS_N(u=%zTKJHAu1VL1l7rnQx@Yvig}1wTxToW086#hc`1O6Z%a9HIF|fJC|RQ+ zPIto3Ncx8(%dSWsQkj7LQtxGIppEbJc3T_$=5w@k@5OxB;m%0J;Yd0yA_|lZNbdV? zA4#z5BrWZGA8lC+&<#8Xy9YBz^ZDH>z`wB199hoA$5ev{#{RkMEoAC@UW)Tq0K9Lb zw8m*wAJUF)3&D8xo{!yOD8I2#4#(GZ3C4YX#`?`@tz_g-lS0xDn6n!cv`Gj)cQQr) zz3!mJ_?_g|Q<#SVRg|1W>XJ5DA5m=OvX4~FAc!N_LGWj+(qhQ|XKLcq7s`aG(XMfR zqu(PGJwX-fnsT+871Dh8Q`LdVkHNG0)3+c-)GxYF2DxHM&SLR98%r1{5xjsaJM+@I z#ym;``f1mcKhcmdJ(k3r89uKtZQum`8ZsW-xj4hbOZ$RQrMk16m-GPLuu7c~uu4H? zWMm>ra!+a9Pj7wZk4N7$!0-Hb7F>z>m00seXjXzA+Kk0*58ExBI*PM554m#g`7?}_ z)qjdVrdvspz_x9-=0$@_>Sk^ZM-iNLvARM@a5L0IALXq##d)Vk&1mv7FE|EzyS%Kg z>%P|Q5v;SmzK}Be#Di;O$Bs^cU|aOqFGgw#)0GZ<8vY&>Nj9O6mArN9R>;r}tzd{= z8oG7VUPA)dnRPspbvWV#188w4Pk`=cEBjp5#!(Cl05@(LKOcS1dSL72wS&mRR&(2Z z-m@UB9!~LY0(*Gq?nQ$6Y^|kasTp~b$ziIZl{tZ9~5(RbRUfv?&4VX-RJUQ9e8*L zo4|mLDn6_u(kdo=t7;-VCj@`P{x^qtob@|Y8nB9e;C#D4SsPW$g;F@szS&}VO^r^J zsC19Z{<7!MTO}`{75fehU*q1;61vskGr;W9`7wrKYIeb^r1uC@eU@j@Gh)n))q`k& z@{u(4Aj-d$!R32K$P2dvPzKQ!cr0Q2IG-sPs-n_H@@jiN`Fy>R3)u9-j6J!Ib+UTVX%87YCo0fKp}@#3QXpq7A3IAqlUZ!F~I65vl9FCXSbrOPk`>%$`5}Nkq-uBG-`FXerv)9SoydAb@p9_D+B1Xp)$tgEyp)M^yW(G)qN|z-n zI{J+6fv^dOAnqv=s23j*Qe8lVfN~=64g2Kt@rbuU-PMXS>;4}6HE&jXz)Wi0J2E}H zwom#w!<(l?IF6Hg{%QmhCM_u_RdTNPBaAGme*4sA7R<~W*^G$s{C+fEVniVbQg)}Z zC_5{;*ovE28GR-KSSMN9tL>lXbj>sQEjZZ~I*$g|AP(SaumvKhvJ%}La(`!+u{|k{a0c9TqsYMC>qXQB1yoyflowmHU z8Nt-xG+=JXp8l_Y$HUaTA0|D#Y%3W|mA2L229z69*_TyZb|JfER6PcXbpmtr&m6WWZwcO4p2ETPv9 zCQ2)&aPsO;Wq-?vk4iYnXcFthbg<>B%|faCQJ6*BPZe#oYveD zZEEQjn1>?kp|5VH>Wfq|^^2N>kUTmVun&;1AMEM7ojRtlO3h@j?TjPC3y0B!Gw>{0 z>TcbD;$f?9@F{_-WW+Igv2q%3+S5g~xGb<-WO=nFwQav23e5}N4lF-%d$ok-A(?Yv ztbcUZ{{s#@2%rB*ONbcnTxj)udA5T-2K~=&*8Hr2&*`4_#X2xs-5-~Rr)IW=yO}th zEN1a=m;YGmxlH4v%$w4})ITxTUD%PRWom!h`{9)cp@ z>^=Bj2RLz}>qF@Xr6nA9VsL9`yUj=fwxj2kKV7%o&&QMk1i-@rOSaGUtGi?qOn zumH*(DEY-L!>_)8(^hGuhU(dMSVq~cj7ssO&Ov%(h$%7$>_eVZ^cin(~^(t{qi$$Q6%7#g_Vk8LSG!BY;(#3ujkp8cul zq^<Clv^w*mL5v4SIX=UXmFk)}?TyT37F%@_Qv zzh##bG{j2ggdj*T1(Iea!I5i3br;idA;>JYlwBqaX}V%3bjK)Q4zoUL0#OgGZQM!K z!{!>^n0)zzEq|Q%2{o7Up_v_KkQp0lqAJTnr4D_0ox^YwoZZ}5#E6b!bZ>e=l>IHQ z?z(4fGU9HW9pk z3C`E%{X|3WuOWd+w~w6qJR1MillZnedQ6@f^-CJvxZZ7(6&Q5zqk3OB&zq=tDP-u7 z3bw1Zr-Dcx(-Hk)Rg~m7bPo?GYaRG_09exKwMg!7^T+%m@8?lKQ{u9}dvu&`fL;}L z;5eP03y1kEKPQmS1ftrfsv|N*UUPmMvI{g}j^ro*_LvlOayg2T3-DDPi&vK1=<6rS zn|)v0-YV03B)GDcNY)70T9==GE!iy)dcbQnr-)P9X%5Tl{3Pq!&fIjyN?HusuF zXR~Ludvo^2X0FZcBq1N8jZVJ}6$%AG0jtuU{zm>*sj3=#L83QfX)QNT{M!qrTt3sLOfuG;@0RW zbTay^>;Iz1T^^oHJ3k*H?PZdAzMTA#BtoLVI-na_@%)~EUw$;7m3l_5V6uAud-{W7 ze4)sm8?h3~DsxMb9-7MB6nJ%A9K>{Mx=`~oSl}jbkpm*}-?1evQ%?mno*;QVH?mW5 zrD>w^?slh5=2F7(-#`fZVVE{sHTuF$kOU|G`)*Bxe!Iz(Ne7Gx!dia2XV9PnA~|E2 zs>wC=`=BXEX~m1)LH_tMSQLQqyHgnq$MN@VqBiYl7D9TluYz z4m1qA;%;f@-Q`K*wQ`<)X%ka)yjV!5M5R9_&0JU@eX9TcHEvK(7jC=Hw*qG%-m1=a z#g!x~by4BLN%0Th);7E^Qa^bNL!p?)T;E)y@Ze|G7yZ z>-Ro$SHGgKYA|^7ML{3Tz?SL#Fm<-KUwttyb+Z{pOb#waQms27)W`$NIvSsQd4CML{a7LH^^BBAC3rGV^fPH@b#TJ z8oRk>mgnE|?$QsZS{-k~kk}c^&kgixC7le|U+qV&&lzgunj-#ygIElu)EhcAi>(8Qb_gwXpo8*G!c9 zPf+%JoyJ0P+I$*tJl_=X&U=;IUfVUCMZV==pZL$AR|vb}W_=m2=UKm!1Qr7st=^Dh z^1g9Q@6r6C&VFM=qIOS(HK6!JO6_GaW3bV7lv`CxBF2l#|9W;3UVqjunrt=yc;J)C z|FG5%7bi>g4if^1N!LMgLO884=NO1*$H|}0cWQw{ab6+IdHc?BEAWQTIZ*R^Hy=On zwa_255*Jm5QnQB@*tZv;d9z3GbbsHcU*EYf4?BEhPqK$tK176L?+O4syV+n1PbF|} ze}38QO&tWlx<&>YoDhkd#ZzkGB5irfGoZ>Mjx32FY}@&P@-;CL4!g$B{ZP8V6dhKs z8m?8$c~WQ3HnqaA4gUAJpZWYh*3X^bsIvBBM=^jYE5a+E)I}?JzBx2BG#dlql(mf! z9@cHPD!3Jh|Asa^1-fd}BTI<~+rN|?roJ-$ra6FAX}q{vc;ml9+vFQz^1kD@mDFzV zG6@IL(^Rz=1>GzD!IM?MMc}&=aI!ITDWGvxO{z5rIv2P$e;zEPlq*IWbvJnX!&_G3 zdcODG`i~F9iye%h((z4q@8@T2*ULIs7|GQ976;Mf9U)vHwLAGorZP6q`!*)s2-|Ht zy)B?NpI<{=c&SFS93^kD5;LI`V9w>s8}hFM;6qeX?yTQV#4q|a;c)5w-JnKTR1sIb zO_2SkNL0=x`^z+??bwf;08LiqpQ-?A8OP+YIte=;>7vHG@nv=<2S;Q$OT50b6dH{!O$`%_=aP9u+A-nF-ksk; zXI5L2i9ON}9^>zV%%<*5hTrasJYQTJ$%(YI4Q2VhGusnuKNx$;5bI^V$61#`_gWZo z%@>fYFiP^(j3 zRF{bny{e}6YF+MpMI4C}(Y>^j?`J#0(0R$Y>0szz^$j4OtR+J(wd|sm27*tVO+$2bA{Sj^0ZyX! zw{lTc;@ZgvG@Q%up0Lihat>o8uh94hk;}^PWQ}|Swqo|hMh;BRT2{nje^m3s%*wxwRrsobF0`Z24sP1$4>2UlBOnvQKdsCt(hNLO@Su1o`sLYXjIWP zp3+od1!G2bMKo`cs9IQo0ra_g{MFvn3E+!idJic{MF&7l@)YX;{t7$MXv>4giO8o# z46o9pW`sI7CmbI+WgBg4w46E(`U=;mHl#U*zBp6zoYVPFO|4H?@l5@F&}~Bezb|!F z9V|@b_!NK+t8r}jf|Mr92B?V&4BKC%W{cO8Hti@X9tfHdX;3$6B7BmsW= zPnv(@Cs>MNBtaUHFIxA!k5lUaMcmsdqq^59Z97%I=IEV_h5N*$y$y@rGe^~(+wv?+ z)LP9v3cr_(9p>fs|0dmZezvz4=eFH`tg+2_{dzW@gf20@Q<2Cb;Va78@5u>l@Wt zMA;rt?$FAPcD_;bsTBM9ty{1CPg#KO(={pmr_|VOp@N;YXNR-+&Rm~S*t!ubKX5l-rr~2zh&BmOXhUOc-o5#Fa*mX8 z8?R_Q5MnWpYY|*pul>M%7aj2=-~Xi-Hpbp|bMX4op}V?;1yl0VS&vfg(oqnel4V4* z|GS4Q|LpMPErM6G!~f#$?`Gk@fg&o;D)ksXcFSJd4m_Nks$p8W;eVe}&Hrgp+UKl| zU2E|+&kptZp-SkR%e1O*CX0rB+P&`bJ$onP%^M>O0T*2(>3&OqT|5PL^g_#c6SR$6 zjf;e4GYk!V6+<%rCovaw@6IcY*(?OE$YsOf>&c%6$1}a8{|iwIuJp}l7+}(gXWF3` zyltPI59z}&WzF#qhyv|{@xS%Hj& z0pfM97ziO=fU5;K7JnO-ZEUp*5&UCduwooq`$Zh|lO|2}{iuV3gT4`PVMSeTSU?t^ zI{X?NkK-_ymoSaHsSDT8z3i;y?(w+%-3s7~R{x?X?zxl+AdWXs;DiO$7#4gTQ-=4< zoH+-+@|8z11z62VVDH|;p=m?0j=Y3oO+5ILMMWMm_+;xA>?%79ZH>pcM=%eGU`Bg0 zRP=UcPX~w`2E^^eQ@=(F8w2}TAp*acTl|ViQ_!`ajqBmB#)-R=e8(V-b{iZV&{%v` zF_*;&n1D}Z;3({2Mdw>MCa&4*?)L~Tf49WlcVqqj&zjZ$tP_BY1VnAX>kW8`pa44y zr{Vaquc1tE^OaX#g##n*aERcr(j$-;nI!&A>NJT?C64&y{d8yB+F;M#T~um{BIIEx zJl_H{E;Ms8;DpfE8Lbj9n_&Pb#6VDc0WB;a_FKiUpA{kSi=eZx1joD2=Ad7=aFIvF zPkg3+?A0F_7~mE^Ow1F(?^=8kXlaWy?v}vftq+iJo3^iCzy3Ekt>>fw{BDKC53~B8 z8SsCW2_T9$;LR4e^#cXiSTt|ZqD6mHQc}8r3Qg{~V@;9>_U}K;Hx`+soV=Gr1~jK| zEDn;SA|uN!aB)s2n@%7R9C8TZ5U`H(Frn)l%$VaZ<=m7{Cm~Ky{^Ft5pDv zIYrQ56(Q)yLL>g$E*#4NpE%DFTzlrMeIMYUrwKcWnfqX%=!;=j!s4Gjdk$yvzl|fh z-ot?}XAbPyv*-6toZl+}zZdwTyWcM;{GU1CUn&Hk@&+u4Hy{!L_Xw6RU%qVi?AedH zM6hDT3T!aB3_;p}ec}61EbNLz0{-D7g*+`qmjzDQaZ2R|bYa^#5u7}E0#2Mb&M#>l z8W92!K_#9iA}JIO_VMc#{hiUm007ODAXlAFc>!Hk9yI2bKo_ISvCv}WbKvjKDdA!r z0X%V@#6e$KSrz!3$h9Xginc)0brp$*39s~hm6w0H(>qxkG#PD)C2Aw z0q`>5drNsg(^mgdBLJN@AXX9PNkRjDIeq%{x+|}|^3n40@|$PPngeUr+=0tWFGH(y zo|D1RqYaV3FNO~kyAlz&=YoPUl8;W=g{~wa5E&3b(CN^jgZz@nfJni8*37PEKiGwr zWImSwlGAbzjHyaKCIR|{s7b(fFTJ3r>%l{aaqWbFPJkzC?>*MkVU(H#64{m#uIe8< z)_^I!SGc%JuKxc0```3f{9Xy*RlpzkEGqn?1YqtBgb_i$mk6-W=zfe}Z>p`GSbXiZ zE8wP^R`C}-e*6@M2S;!pe5Vokbq7CPQjIzt=w$8OOX(lw`R&@;T5(!(E5D5hf`}sY zOA)r*(1}DjA|g7GCHGqM)Z6)w886t$ttU$_%h#L!{qFZhZa%HrCg9U+V$lv(@NK}| z5WBk2OWKD2{=F09_X<{D26|E86B~Fg8Sr0<1Yi;t$cP|72AD0VL?XCh%9JTzDk&)& zhdT>+cra_$3?3E`5u7-2%Bbnc1O)EmhzLkapxOhvbpP#+u~rd-?l$#fgFq($+I9lu zgE&*4w29QYr7^dTwwE!eR0HZF~M|J*sg z?x|M+{vOZmqN}efzpB@ZnOl`lAG3Mg*23G>~}(f%CL!(a42M+9m_V)G`JZ-~r z-8Y=rzRz>+)tvhTH(%!J4~_zVlmOxqfu$gVJP9G>dB|WAlEDfh19Z!$R#sNS{P_!z z5Ej6U8PmCUaPHg%hYS*=K#F;^SUMf>t4Sc;cs?hGChZh+m!5!6Q+6rmf8xYR4tP}V zF_UqS1WRt|u|a3|ek!y`fL8>5p9Ju|8qmvP{88YK5k zVZsW$TrhU**!eiUxVolh0w6&k8O-E_KsyUMJG(e3kW1Xs(#AoP3LBVV!A$y^LX>!( zghPn2nmjBCIc%kGb?|L`U;()bZ0(=z-=s+*J`J*xZC;?=J2)tPX(JN5t1tKBHF8O)t)TtA(l5PfO z6lRo`md-;`n2G=^D#cxP*sp}kE31$kX7IfSd{BLcCWK<6Epp80c;6eb|>Yp`W` z0g}NuIukKqF9GsEG0-_LZw%o5ydsYZXK(@0StS15atJ+UC$A&`v_+4+_^Jw=KWY4bUF^%U_8Ay zY72E(m{aV2NKCa=BPmpy{6Q|L-G5_6Unf~|x{kE~9Y_eYWrYO1T^@IP&;$5EL+}r1 zfGz@FjO$h9x}%^UC4dp(6@WklIzq4%K$rbHNAg_R+55iYS@n5k*3*`p2PMFlk-&iF z+^@NBP-5w2as8;Z&kzCRj(!d4e~*gWnCHGQLa27aaie;5gv5W%1#Zr6c5ENwRm z{80k9XuSfjXQ0v!MDU8_z(N3TYWYn(08>vu<{7ANvo&$NtqDg)Poo6znG6%afDppY zR>=Jj68}Sp4xl3fTVug>XBf5eqXaNY26{q}k%H>F?%!d+hnPg5`*#$pJ_`6z0vII( zgZo$rLFj Date: Wed, 24 Jun 2020 15:01:44 +0200 Subject: [PATCH 016/190] added functions for getting pype icon and pype splash into resources --- pype/resources/__init__.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pype/resources/__init__.py b/pype/resources/__init__.py index 248614ae9d..ba882a84fb 100644 --- a/pype/resources/__init__.py +++ b/pype/resources/__init__.py @@ -14,3 +14,25 @@ def get_resource(*args): *args ) ) + + +def pype_icon_filepath(debug=None): + if debug is None: + debug = bool(os.getenv("PYPE_DEV")) + + if debug: + icon_file_name = "pype_icon_dev.png" + else: + icon_file_name = "pype_icon.png" + return get_resource("icons", icon_file_name) + + +def pype_splash_filepath(debug=None): + if debug is None: + debug = bool(os.getenv("PYPE_DEV")) + + if debug: + splash_file_name = "pype_splash_dev.png" + else: + splash_file_name = "pype_splash.png" + return get_resource("icons", splash_file_name) From c7bcac6b4660493d6c45dc9fb9798314ebe83354 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 15:05:22 +0200 Subject: [PATCH 017/190] used new resource way of getting pype icon in code --- pype/modules/clockify/widget_message.py | 5 ++-- pype/modules/clockify/widget_settings.py | 6 ++--- pype/modules/ftrack/tray/login_dialog.py | 6 ++--- pype/modules/muster/widget_login.py | 6 ++--- pype/modules/user/widget_user.py | 4 ++-- pype/tools/tray/pype_tray.py | 30 ++++++++++-------------- 6 files changed, 24 insertions(+), 33 deletions(-) diff --git a/pype/modules/clockify/widget_message.py b/pype/modules/clockify/widget_message.py index f919c3f819..9e4fad1df1 100644 --- a/pype/modules/clockify/widget_message.py +++ b/pype/modules/clockify/widget_message.py @@ -1,5 +1,6 @@ from Qt import QtCore, QtGui, QtWidgets from avalon import style +from pype.api import resources class MessageWidget(QtWidgets.QWidget): @@ -19,8 +20,8 @@ class MessageWidget(QtWidgets.QWidget): if parent and hasattr(parent, 'icon'): self.setWindowIcon(parent.icon) else: - from pypeapp.resources import get_resource - self.setWindowIcon(QtGui.QIcon(get_resource('icon.png'))) + icon = QtGui.QIcon(resources.pype_icon_filepath()) + self.setWindowIcon(icon) self.setWindowFlags( QtCore.Qt.WindowCloseButtonHint | diff --git a/pype/modules/clockify/widget_settings.py b/pype/modules/clockify/widget_settings.py index 956bdb1916..7e5ee300bb 100644 --- a/pype/modules/clockify/widget_settings.py +++ b/pype/modules/clockify/widget_settings.py @@ -1,6 +1,7 @@ import os from Qt import QtCore, QtGui, QtWidgets from avalon import style +from pype.api import resources class ClockifySettings(QtWidgets.QWidget): @@ -26,10 +27,7 @@ class ClockifySettings(QtWidgets.QWidget): elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'): self.setWindowIcon(self.parent.parent.icon) else: - pype_setup = os.getenv('PYPE_SETUP_PATH') - items = [pype_setup, "app", "resources", "icon.png"] - fname = os.path.sep.join(items) - icon = QtGui.QIcon(fname) + icon = QtGui.QIcon(resources.pype_icon_filepath()) self.setWindowIcon(icon) self.setWindowFlags( diff --git a/pype/modules/ftrack/tray/login_dialog.py b/pype/modules/ftrack/tray/login_dialog.py index 3b8a366209..e0614513a3 100644 --- a/pype/modules/ftrack/tray/login_dialog.py +++ b/pype/modules/ftrack/tray/login_dialog.py @@ -3,6 +3,7 @@ import requests from avalon import style from pype.modules.ftrack import credentials from . import login_tools +from pype.api import resources from Qt import QtCore, QtGui, QtWidgets @@ -29,10 +30,7 @@ class Login_Dialog_ui(QtWidgets.QWidget): elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'): self.setWindowIcon(self.parent.parent.icon) else: - pype_setup = os.getenv('PYPE_SETUP_PATH') - items = [pype_setup, "app", "resources", "icon.png"] - fname = os.path.sep.join(items) - icon = QtGui.QIcon(fname) + icon = QtGui.QIcon(resources.pype_icon_filepath()) self.setWindowIcon(icon) self.setWindowFlags( diff --git a/pype/modules/muster/widget_login.py b/pype/modules/muster/widget_login.py index 8de0d3136a..f446c13325 100644 --- a/pype/modules/muster/widget_login.py +++ b/pype/modules/muster/widget_login.py @@ -1,6 +1,7 @@ import os from Qt import QtCore, QtGui, QtWidgets from avalon import style +from pype.api import resources class MusterLogin(QtWidgets.QWidget): @@ -23,10 +24,7 @@ class MusterLogin(QtWidgets.QWidget): elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'): self.setWindowIcon(parent.parent.icon) else: - pype_setup = os.getenv('PYPE_SETUP_PATH') - items = [pype_setup, "app", "resources", "icon.png"] - fname = os.path.sep.join(items) - icon = QtGui.QIcon(fname) + icon = QtGui.QIcon(resources.pype_icon_filepath()) self.setWindowIcon(icon) self.setWindowFlags( diff --git a/pype/modules/user/widget_user.py b/pype/modules/user/widget_user.py index 1d43941345..ba211c4737 100644 --- a/pype/modules/user/widget_user.py +++ b/pype/modules/user/widget_user.py @@ -1,6 +1,6 @@ from Qt import QtCore, QtGui, QtWidgets -from pype.resources import get_resource from avalon import style +from pype.api import resources class UserWidget(QtWidgets.QWidget): @@ -14,7 +14,7 @@ class UserWidget(QtWidgets.QWidget): self.module = module # Style - icon = QtGui.QIcon(get_resource("icon.png")) + icon = QtGui.QIcon(resources.pype_icon_filepath()) self.setWindowIcon(icon) self.setWindowTitle("Username Settings") self.setMinimumWidth(self.MIN_WIDTH) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index eec8f61cc4..3c5e1fb612 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -3,8 +3,7 @@ import sys import platform from avalon import style from Qt import QtCore, QtGui, QtWidgets, QtSvg -from pype.resources import get_resource -from pype.api import config, Logger +from pype.api import config, Logger, resources class TrayManager: @@ -29,9 +28,15 @@ class TrayManager: self.main_window = main_window self.log = Logger().get_logger(self.__class__.__name__) - self.icon_run = QtGui.QIcon(get_resource('circle_green.png')) - self.icon_stay = QtGui.QIcon(get_resource('circle_orange.png')) - self.icon_failed = QtGui.QIcon(get_resource('circle_red.png')) + self.icon_run = QtGui.QIcon( + resources.get_resource("icons", "circle_green.png") + ) + self.icon_stay = QtGui.QIcon( + resources.get_resource("icons", "circle_orange.png") + ) + self.icon_failed = QtGui.QIcon( + resources.get_resource("icons", "circle_red.png") + ) self.services_thread = None @@ -333,12 +338,7 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): :type parent: QtWidgets.QMainWindow """ def __init__(self, parent): - if os.getenv("PYPE_DEV"): - icon_file_name = "icon_dev.png" - else: - icon_file_name = "icon.png" - - self.icon = QtGui.QIcon(get_resource(icon_file_name)) + self.icon = QtGui.QIcon(resources.pype_icon_filepath()) QtWidgets.QSystemTrayIcon.__init__(self, self.icon, parent) @@ -402,7 +402,7 @@ class TrayMainWindow(QtWidgets.QMainWindow): self.trayIcon.show() def set_working_widget(self): - image_file = get_resource('working.svg') + image_file = resources.get_resource("icons", "working.svg") img_pix = QtGui.QPixmap(image_file) if image_file.endswith('.svg'): widget = QtSvg.QSvgWidget(image_file) @@ -492,11 +492,7 @@ class PypeTrayApplication(QtWidgets.QApplication): splash_widget.hide() def set_splash(self): - if os.getenv("PYPE_DEV"): - splash_file_name = "splash_dev.png" - else: - splash_file_name = "splash.png" - splash_pix = QtGui.QPixmap(get_resource(splash_file_name)) + splash_pix = QtGui.QPixmap(resources.pype_splash_filepath()) splash = QtWidgets.QSplashScreen(splash_pix) splash.setMask(splash_pix.mask()) splash.setEnabled(False) From 38c919e8f81e3afc16d40a944179fc434d52e0fd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 15:05:30 +0200 Subject: [PATCH 018/190] added resources to api --- pype/api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/api.py b/pype/api.py index 5775bb3ce4..44a31f2626 100644 --- a/pype/api.py +++ b/pype/api.py @@ -12,6 +12,8 @@ from pypeapp.lib.mongo import ( get_default_components ) +from . import resources + from .plugin import ( Extractor, @@ -54,6 +56,8 @@ __all__ = [ "compose_url", "get_default_components", + # Resources + "resources", # plugin classes "Extractor", # ordering From a3b78d7e1d7cf569514d6a91e72bbc752b3f3cfc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 15:05:55 +0200 Subject: [PATCH 019/190] removed forgotten images already moved to subfolder --- pype/resources/circle_green.png | Bin 35899 -> 0 bytes pype/resources/circle_orange.png | Bin 37564 -> 0 bytes pype/resources/circle_red.png | Bin 36200 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 pype/resources/circle_green.png delete mode 100644 pype/resources/circle_orange.png delete mode 100644 pype/resources/circle_red.png diff --git a/pype/resources/circle_green.png b/pype/resources/circle_green.png deleted file mode 100644 index b83369a9e341520d4eb8481ee501b2874f2ce356..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35899 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 diff --git a/pype/resources/circle_orange.png b/pype/resources/circle_orange.png deleted file mode 100644 index 656f318e0c9b4eadc4f6f54def39ab4e01521f5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37564 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 Date: Wed, 24 Jun 2020 15:13:38 +0200 Subject: [PATCH 020/190] movedmoved app icons to ~/pype/resources --- {res => pype/resources}/app_icons/Aport.png | Bin {res => pype/resources}/app_icons/blender.png | Bin .../resources}/app_icons/celaction_local.png | Bin .../resources}/app_icons/celaction_remotel.png | Bin .../resources}/app_icons/clockify-white.png | Bin {res => pype/resources}/app_icons/clockify.png | Bin {res => pype/resources}/app_icons/djvView.png | Bin {res => pype/resources}/app_icons/harmony.png | Bin {res => pype/resources}/app_icons/houdini.png | Bin {res => pype/resources}/app_icons/maya.png | Bin {res => pype/resources}/app_icons/nuke.png | Bin {res => pype/resources}/app_icons/nukex.png | Bin {res => pype/resources}/app_icons/photoshop.png | Bin {res => pype/resources}/app_icons/premiere.png | Bin {res => pype/resources}/app_icons/python.png | Bin {res => pype/resources}/app_icons/resolve.png | Bin {res => pype/resources}/app_icons/storyboardpro.png | Bin {res => pype/resources}/app_icons/ue4.png | Bin 18 files changed, 0 insertions(+), 0 deletions(-) rename {res => pype/resources}/app_icons/Aport.png (100%) rename {res => pype/resources}/app_icons/blender.png (100%) rename {res => pype/resources}/app_icons/celaction_local.png (100%) rename {res => pype/resources}/app_icons/celaction_remotel.png (100%) rename {res => pype/resources}/app_icons/clockify-white.png (100%) rename {res => pype/resources}/app_icons/clockify.png (100%) rename {res => pype/resources}/app_icons/djvView.png (100%) rename {res => pype/resources}/app_icons/harmony.png (100%) rename {res => pype/resources}/app_icons/houdini.png (100%) rename {res => pype/resources}/app_icons/maya.png (100%) rename {res => pype/resources}/app_icons/nuke.png (100%) rename {res => pype/resources}/app_icons/nukex.png (100%) rename {res => pype/resources}/app_icons/photoshop.png (100%) rename {res => pype/resources}/app_icons/premiere.png (100%) rename {res => pype/resources}/app_icons/python.png (100%) rename {res => pype/resources}/app_icons/resolve.png (100%) rename {res => pype/resources}/app_icons/storyboardpro.png (100%) rename {res => pype/resources}/app_icons/ue4.png (100%) diff --git a/res/app_icons/Aport.png b/pype/resources/app_icons/Aport.png similarity index 100% rename from res/app_icons/Aport.png rename to pype/resources/app_icons/Aport.png diff --git a/res/app_icons/blender.png b/pype/resources/app_icons/blender.png similarity index 100% rename from res/app_icons/blender.png rename to pype/resources/app_icons/blender.png diff --git a/res/app_icons/celaction_local.png b/pype/resources/app_icons/celaction_local.png similarity index 100% rename from res/app_icons/celaction_local.png rename to pype/resources/app_icons/celaction_local.png diff --git a/res/app_icons/celaction_remotel.png b/pype/resources/app_icons/celaction_remotel.png similarity index 100% rename from res/app_icons/celaction_remotel.png rename to pype/resources/app_icons/celaction_remotel.png diff --git a/res/app_icons/clockify-white.png b/pype/resources/app_icons/clockify-white.png similarity index 100% rename from res/app_icons/clockify-white.png rename to pype/resources/app_icons/clockify-white.png diff --git a/res/app_icons/clockify.png b/pype/resources/app_icons/clockify.png similarity index 100% rename from res/app_icons/clockify.png rename to pype/resources/app_icons/clockify.png diff --git a/res/app_icons/djvView.png b/pype/resources/app_icons/djvView.png similarity index 100% rename from res/app_icons/djvView.png rename to pype/resources/app_icons/djvView.png diff --git a/res/app_icons/harmony.png b/pype/resources/app_icons/harmony.png similarity index 100% rename from res/app_icons/harmony.png rename to pype/resources/app_icons/harmony.png diff --git a/res/app_icons/houdini.png b/pype/resources/app_icons/houdini.png similarity index 100% rename from res/app_icons/houdini.png rename to pype/resources/app_icons/houdini.png diff --git a/res/app_icons/maya.png b/pype/resources/app_icons/maya.png similarity index 100% rename from res/app_icons/maya.png rename to pype/resources/app_icons/maya.png diff --git a/res/app_icons/nuke.png b/pype/resources/app_icons/nuke.png similarity index 100% rename from res/app_icons/nuke.png rename to pype/resources/app_icons/nuke.png diff --git a/res/app_icons/nukex.png b/pype/resources/app_icons/nukex.png similarity index 100% rename from res/app_icons/nukex.png rename to pype/resources/app_icons/nukex.png diff --git a/res/app_icons/photoshop.png b/pype/resources/app_icons/photoshop.png similarity index 100% rename from res/app_icons/photoshop.png rename to pype/resources/app_icons/photoshop.png diff --git a/res/app_icons/premiere.png b/pype/resources/app_icons/premiere.png similarity index 100% rename from res/app_icons/premiere.png rename to pype/resources/app_icons/premiere.png diff --git a/res/app_icons/python.png b/pype/resources/app_icons/python.png similarity index 100% rename from res/app_icons/python.png rename to pype/resources/app_icons/python.png diff --git a/res/app_icons/resolve.png b/pype/resources/app_icons/resolve.png similarity index 100% rename from res/app_icons/resolve.png rename to pype/resources/app_icons/resolve.png diff --git a/res/app_icons/storyboardpro.png b/pype/resources/app_icons/storyboardpro.png similarity index 100% rename from res/app_icons/storyboardpro.png rename to pype/resources/app_icons/storyboardpro.png diff --git a/res/app_icons/ue4.png b/pype/resources/app_icons/ue4.png similarity index 100% rename from res/app_icons/ue4.png rename to pype/resources/app_icons/ue4.png From 7eb36b417359682d7310ea57808461e7cdee895d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 15:14:10 +0200 Subject: [PATCH 021/190] moved ftrack resource files to ~/pype/resources --- .../ftrack/action_icons/ActionAskWhereIRun.svg | 0 .../ftrack/action_icons/AssetsRemover.svg | 0 .../ftrack/action_icons/ComponentOpen.svg | 0 .../ftrack/action_icons/CreateFolders.svg | 0 .../ftrack/action_icons/CreateProjectFolders.svg | 0 .../resources}/ftrack/action_icons/DeleteAsset.svg | 0 .../resources}/ftrack/action_icons/Delivery.svg | 0 .../ftrack/action_icons/MultipleNotes.svg | 0 .../ftrack/action_icons/PrepareProject.svg | 0 .../resources}/ftrack/action_icons/PypeAdmin.svg | 0 .../resources}/ftrack/action_icons/PypeDoctor.svg | 0 .../resources}/ftrack/action_icons/PypeUpdate.svg | 0 {res => pype/resources}/ftrack/action_icons/RV.png | Bin .../resources}/ftrack/action_icons/SeedProject.svg | 0 .../ftrack/action_icons/SyncHierarchicalAttrs.svg | 0 .../resources}/ftrack/action_icons/SyncToAvalon.svg | 0 .../resources}/ftrack/action_icons/TestAction.svg | 0 .../resources}/ftrack/action_icons/Thumbnail.svg | 0 {res => pype/resources}/ftrack/sign_in_message.html | 0 19 files changed, 0 insertions(+), 0 deletions(-) rename {res => pype/resources}/ftrack/action_icons/ActionAskWhereIRun.svg (100%) rename {res => pype/resources}/ftrack/action_icons/AssetsRemover.svg (100%) rename {res => pype/resources}/ftrack/action_icons/ComponentOpen.svg (100%) rename {res => pype/resources}/ftrack/action_icons/CreateFolders.svg (100%) rename {res => pype/resources}/ftrack/action_icons/CreateProjectFolders.svg (100%) rename {res => pype/resources}/ftrack/action_icons/DeleteAsset.svg (100%) rename {res => pype/resources}/ftrack/action_icons/Delivery.svg (100%) rename {res => pype/resources}/ftrack/action_icons/MultipleNotes.svg (100%) rename {res => pype/resources}/ftrack/action_icons/PrepareProject.svg (100%) rename {res => pype/resources}/ftrack/action_icons/PypeAdmin.svg (100%) rename {res => pype/resources}/ftrack/action_icons/PypeDoctor.svg (100%) rename {res => pype/resources}/ftrack/action_icons/PypeUpdate.svg (100%) rename {res => pype/resources}/ftrack/action_icons/RV.png (100%) rename {res => pype/resources}/ftrack/action_icons/SeedProject.svg (100%) rename {res => pype/resources}/ftrack/action_icons/SyncHierarchicalAttrs.svg (100%) rename {res => pype/resources}/ftrack/action_icons/SyncToAvalon.svg (100%) rename {res => pype/resources}/ftrack/action_icons/TestAction.svg (100%) rename {res => pype/resources}/ftrack/action_icons/Thumbnail.svg (100%) rename {res => pype/resources}/ftrack/sign_in_message.html (100%) diff --git a/res/ftrack/action_icons/ActionAskWhereIRun.svg b/pype/resources/ftrack/action_icons/ActionAskWhereIRun.svg similarity index 100% rename from res/ftrack/action_icons/ActionAskWhereIRun.svg rename to pype/resources/ftrack/action_icons/ActionAskWhereIRun.svg diff --git a/res/ftrack/action_icons/AssetsRemover.svg b/pype/resources/ftrack/action_icons/AssetsRemover.svg similarity index 100% rename from res/ftrack/action_icons/AssetsRemover.svg rename to pype/resources/ftrack/action_icons/AssetsRemover.svg diff --git a/res/ftrack/action_icons/ComponentOpen.svg b/pype/resources/ftrack/action_icons/ComponentOpen.svg similarity index 100% rename from res/ftrack/action_icons/ComponentOpen.svg rename to pype/resources/ftrack/action_icons/ComponentOpen.svg diff --git a/res/ftrack/action_icons/CreateFolders.svg b/pype/resources/ftrack/action_icons/CreateFolders.svg similarity index 100% rename from res/ftrack/action_icons/CreateFolders.svg rename to pype/resources/ftrack/action_icons/CreateFolders.svg diff --git a/res/ftrack/action_icons/CreateProjectFolders.svg b/pype/resources/ftrack/action_icons/CreateProjectFolders.svg similarity index 100% rename from res/ftrack/action_icons/CreateProjectFolders.svg rename to pype/resources/ftrack/action_icons/CreateProjectFolders.svg diff --git a/res/ftrack/action_icons/DeleteAsset.svg b/pype/resources/ftrack/action_icons/DeleteAsset.svg similarity index 100% rename from res/ftrack/action_icons/DeleteAsset.svg rename to pype/resources/ftrack/action_icons/DeleteAsset.svg diff --git a/res/ftrack/action_icons/Delivery.svg b/pype/resources/ftrack/action_icons/Delivery.svg similarity index 100% rename from res/ftrack/action_icons/Delivery.svg rename to pype/resources/ftrack/action_icons/Delivery.svg diff --git a/res/ftrack/action_icons/MultipleNotes.svg b/pype/resources/ftrack/action_icons/MultipleNotes.svg similarity index 100% rename from res/ftrack/action_icons/MultipleNotes.svg rename to pype/resources/ftrack/action_icons/MultipleNotes.svg diff --git a/res/ftrack/action_icons/PrepareProject.svg b/pype/resources/ftrack/action_icons/PrepareProject.svg similarity index 100% rename from res/ftrack/action_icons/PrepareProject.svg rename to pype/resources/ftrack/action_icons/PrepareProject.svg diff --git a/res/ftrack/action_icons/PypeAdmin.svg b/pype/resources/ftrack/action_icons/PypeAdmin.svg similarity index 100% rename from res/ftrack/action_icons/PypeAdmin.svg rename to pype/resources/ftrack/action_icons/PypeAdmin.svg diff --git a/res/ftrack/action_icons/PypeDoctor.svg b/pype/resources/ftrack/action_icons/PypeDoctor.svg similarity index 100% rename from res/ftrack/action_icons/PypeDoctor.svg rename to pype/resources/ftrack/action_icons/PypeDoctor.svg diff --git a/res/ftrack/action_icons/PypeUpdate.svg b/pype/resources/ftrack/action_icons/PypeUpdate.svg similarity index 100% rename from res/ftrack/action_icons/PypeUpdate.svg rename to pype/resources/ftrack/action_icons/PypeUpdate.svg diff --git a/res/ftrack/action_icons/RV.png b/pype/resources/ftrack/action_icons/RV.png similarity index 100% rename from res/ftrack/action_icons/RV.png rename to pype/resources/ftrack/action_icons/RV.png diff --git a/res/ftrack/action_icons/SeedProject.svg b/pype/resources/ftrack/action_icons/SeedProject.svg similarity index 100% rename from res/ftrack/action_icons/SeedProject.svg rename to pype/resources/ftrack/action_icons/SeedProject.svg diff --git a/res/ftrack/action_icons/SyncHierarchicalAttrs.svg b/pype/resources/ftrack/action_icons/SyncHierarchicalAttrs.svg similarity index 100% rename from res/ftrack/action_icons/SyncHierarchicalAttrs.svg rename to pype/resources/ftrack/action_icons/SyncHierarchicalAttrs.svg diff --git a/res/ftrack/action_icons/SyncToAvalon.svg b/pype/resources/ftrack/action_icons/SyncToAvalon.svg similarity index 100% rename from res/ftrack/action_icons/SyncToAvalon.svg rename to pype/resources/ftrack/action_icons/SyncToAvalon.svg diff --git a/res/ftrack/action_icons/TestAction.svg b/pype/resources/ftrack/action_icons/TestAction.svg similarity index 100% rename from res/ftrack/action_icons/TestAction.svg rename to pype/resources/ftrack/action_icons/TestAction.svg diff --git a/res/ftrack/action_icons/Thumbnail.svg b/pype/resources/ftrack/action_icons/Thumbnail.svg similarity index 100% rename from res/ftrack/action_icons/Thumbnail.svg rename to pype/resources/ftrack/action_icons/Thumbnail.svg diff --git a/res/ftrack/sign_in_message.html b/pype/resources/ftrack/sign_in_message.html similarity index 100% rename from res/ftrack/sign_in_message.html rename to pype/resources/ftrack/sign_in_message.html From 5a2ae24d9e9458d2b50fbb504e83b865b4a3c7ef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 15:14:48 +0200 Subject: [PATCH 022/190] rest api statics path changed to ~/pype/resources --- pype/modules/rest_api/rest_api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/modules/rest_api/rest_api.py b/pype/modules/rest_api/rest_api.py index 5f0969a5a2..1c4db9f706 100644 --- a/pype/modules/rest_api/rest_api.py +++ b/pype/modules/rest_api/rest_api.py @@ -102,7 +102,11 @@ class RestApiServer: port = self.find_port() self.rest_api_thread = RestApiThread(self, port) - statics_dir = os.path.sep.join([os.environ["PYPE_MODULE_ROOT"], "res"]) + statics_dir = os.path.join( + os.environ["PYPE_MODULE_ROOT"], + "pype", + "resources" + ) self.register_statics("/res", statics_dir) os.environ["PYPE_STATICS_SERVER"] = "{}/res".format( os.environ["PYPE_REST_API_URL"] From 880c91b06ecd003174c56c5c8d5efe3928b6e9ec Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 15:15:20 +0200 Subject: [PATCH 023/190] modified ftrack message file getting --- pype/modules/ftrack/tray/login_tools.py | 33 +++++++++++-------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/pype/modules/ftrack/tray/login_tools.py b/pype/modules/ftrack/tray/login_tools.py index b259f2d2ed..02982294f2 100644 --- a/pype/modules/ftrack/tray/login_tools.py +++ b/pype/modules/ftrack/tray/login_tools.py @@ -1,16 +1,16 @@ from http.server import BaseHTTPRequestHandler, HTTPServer from urllib import parse -import os import webbrowser import functools -import pype -import inspect from Qt import QtCore +from pype.api import resources class LoginServerHandler(BaseHTTPRequestHandler): '''Login server handler.''' + message_filepath = resources.get_resource("ftrack", "sign_in_message.html") + def __init__(self, login_callback, *args, **kw): '''Initialise handler.''' self.login_callback = login_callback @@ -28,23 +28,21 @@ class LoginServerHandler(BaseHTTPRequestHandler): login_credentials = parse.parse_qs(query) api_user = login_credentials['api_user'][0] api_key = login_credentials['api_key'][0] - # get path to resources - path_items = os.path.dirname( - inspect.getfile(pype) - ).split(os.path.sep) - del path_items[-1] - path_items.extend(['res', 'ftrack', 'sign_in_message.html']) - message_filepath = os.path.sep.join(path_items) - message_file = open(message_filepath, 'r') - sign_in_message = message_file.read() - message_file.close() + + with open(self.message_filepath, "r") as message_file: + sign_in_message = message_file.read() + # formatting html code for python - replacement = [('{', '{{'), ('}', '}}'), ('{{}}', '{}')] - for r in (replacement): - sign_in_message = sign_in_message.replace(*r) + replacements = ( + ("{", "{{"), + ("}", "}}"), + ("{{}}", "{}") + ) + for replacement in (replacements): + sign_in_message = sign_in_message.replace(*replacement) message = sign_in_message.format(api_user) else: - message = '

Failed to sign in

' + message = "

Failed to sign in

" self.send_response(200) self.end_headers() @@ -74,7 +72,6 @@ class LoginServerThread(QtCore.QThread): def run(self): '''Listen for events.''' - # self._server = BaseHTTPServer.HTTPServer( self._server = HTTPServer( ('localhost', 0), functools.partial( From a72c9a0e6fd400882a31fba85a7e217abfc7b0e9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 15:16:52 +0200 Subject: [PATCH 024/190] added resources to pype.api --- pype/api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/api.py b/pype/api.py index 5775bb3ce4..44a31f2626 100644 --- a/pype/api.py +++ b/pype/api.py @@ -12,6 +12,8 @@ from pypeapp.lib.mongo import ( get_default_components ) +from . import resources + from .plugin import ( Extractor, @@ -54,6 +56,8 @@ __all__ = [ "compose_url", "get_default_components", + # Resources + "resources", # plugin classes "Extractor", # ordering From eb1a534adbc23d23a19d7a3eed07c77ef4baa45f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 15:54:38 +0200 Subject: [PATCH 025/190] move message widget to center of screen --- pype/widgets/message_window.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pype/widgets/message_window.py b/pype/widgets/message_window.py index 3532d2df44..41c709b933 100644 --- a/pype/widgets/message_window.py +++ b/pype/widgets/message_window.py @@ -52,6 +52,19 @@ def message(title=None, message=None, level="info", parent=None): app = parent if not app: app = QtWidgets.QApplication(sys.argv) + ex = Window(app, title, message, level) ex.show() + + # Move widget to center of screen + try: + desktop_rect = QtWidgets.QApplication.desktop().availableGeometry(ex) + center = desktop_rect.center() + ex.move( + center.x() - (ex.width() * 0.5), + center.y() - (ex.height() * 0.5) + ) + except Exception: + # skip all possible issues that may happen feature is not crutial + log.warning("Couldn't center message.", exc_info=True) # sys.exit(app.exec_()) From b1ee15b0b52904759458de4762e8483451f52948 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 Jun 2020 16:26:35 +0100 Subject: [PATCH 026/190] Model support for namespaces --- pype/hosts/blender/plugin.py | 22 ++++++++- pype/plugins/blender/load/load_model.py | 65 +++++++++++++------------ 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/pype/hosts/blender/plugin.py b/pype/hosts/blender/plugin.py index 77fce90d65..5a2596c9d8 100644 --- a/pype/hosts/blender/plugin.py +++ b/pype/hosts/blender/plugin.py @@ -5,7 +5,7 @@ from typing import Dict, List, Optional import bpy -from avalon import api +from avalon import api, blender VALID_EXTENSIONS = [".blend"] @@ -20,6 +20,26 @@ def asset_name( return name +def asset_namespace( + asset: str, subset: str +) -> str: + """Return a unique namespace based on the asset name.""" + avalon_containers = bpy.data.collections.get( + blender.pipeline.AVALON_CONTAINERS + ) + if avalon_containers is None: + return "1" + collections_names = [ + c.name for c in avalon_containers.children + ] + count = 1 + name = f"{asset_name(asset, subset, str(count))}_CON" + while name in collections_names: + count += 1 + name = f"{asset_name(asset, subset, str(count))}_CON" + return str(count) + + def create_blender_context(active: Optional[bpy.types.Object] = None, selected: Optional[bpy.types.Object] = None,): """Create a new Blender context. If an object is passed as diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index 4a8f43cd48..b16f5a40b1 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -7,12 +7,12 @@ from typing import Dict, List, Optional from avalon import api, blender import bpy -import pype.hosts.blender.plugin +import pype.hosts.blender.plugin as plugin logger = logging.getLogger("pype").getChild("blender").getChild("load_model") -class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): +class BlendModelLoader(plugin.AssetLoader): """Load models from a .blend file. Because they come from a .blend file we can simply link the collection that @@ -30,16 +30,20 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): icon = "code-fork" color = "orange" - def _remove(self, objects, lib_container): - + def _remove(self, objects, container): for obj in objects: - + for material_slot in obj.material_slots: + bpy.data.materials.remove(material_slot.material) bpy.data.meshes.remove(obj.data) - bpy.data.collections.remove(bpy.data.collections[lib_container]) + bpy.data.collections.remove(container) + + def prepare_data(self, data, container_name): + name = data.name + data = data.make_local() + data.name = f"{name}:{container_name}" def _process(self, libpath, lib_container, container_name): - relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( libpath, link=True, relative=relative @@ -51,33 +55,26 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): scene.collection.children.link(bpy.data.collections[lib_container]) model_container = scene.collection.children[lib_container].make_local() - - objects_list = [] + model_container.name = container_name for obj in model_container.objects: - - obj = obj.make_local() - - obj.data.make_local() + self.prepare_data(obj, container_name) + self.prepare_data(obj.data, container_name) for material_slot in obj.material_slots: - - material_slot.material.make_local() + self.prepare_data(material_slot.material, container_name) if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[blender.pipeline.AVALON_PROPERTY] = dict() avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) - objects_list.append(obj) - model_container.pop(blender.pipeline.AVALON_PROPERTY) bpy.ops.object.select_all(action='DESELECT') - return objects_list + return model_container def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, @@ -94,13 +91,18 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): libpath = self.fname asset = context["asset"]["name"] subset = context["subset"]["name"] - lib_container = pype.hosts.blender.plugin.asset_name(asset, subset) - container_name = pype.hosts.blender.plugin.asset_name( + + lib_container = plugin.asset_name( + asset, subset + ) + namespace = namespace or plugin.asset_namespace( + asset, subset + ) + container_name = plugin.asset_name( asset, subset, namespace ) collection = bpy.data.collections.new(lib_container) - collection.name = container_name blender.pipeline.containerise_existing( collection, name, @@ -115,11 +117,13 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - objects_list = self._process( + obj_container = self._process( libpath, lib_container, container_name) + container_metadata["obj_container"] = obj_container + # Save the list of objects in the metadata container - container_metadata["objects"] = objects_list + container_metadata["objects"] = obj_container.all_objects nodes = list(collection.objects) nodes.append(collection) @@ -162,7 +166,7 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): assert libpath.is_file(), ( f"The file doesn't exist: {libpath}" ) - assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, ( + assert extension in plugin.VALID_EXTENSIONS, ( f"Unsupported file: {libpath}" ) @@ -171,6 +175,7 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): collection_libpath = collection_metadata["libpath"] objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] + obj_container = collection_metadata["obj_container"] normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) @@ -187,7 +192,7 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): logger.info("Library already loaded, not updating...") return - self._remove(objects, lib_container) + self._remove(objects, obj_container) objects_list = self._process( str(libpath), lib_container, collection.name) @@ -222,16 +227,16 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] - lib_container = collection_metadata["lib_container"] + obj_container = collection_metadata["obj_container"] - self._remove(objects, lib_container) + self._remove(objects, obj_container) bpy.data.collections.remove(collection) return True -class CacheModelLoader(pype.hosts.blender.plugin.AssetLoader): +class CacheModelLoader(plugin.AssetLoader): """Load cache models. Stores the imported asset in a collection named after the asset. @@ -267,7 +272,7 @@ class CacheModelLoader(pype.hosts.blender.plugin.AssetLoader): subset = context["subset"]["name"] # TODO (jasper): evaluate use of namespace which is 'alien' to Blender. lib_container = container_name = ( - pype.hosts.blender.plugin.asset_name(asset, subset, namespace) + plugin.asset_name(asset, subset, namespace) ) relative = bpy.context.preferences.filepaths.use_relative_paths From c558f46dd3dc165577d02943e8988926cfa18b58 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 17:27:05 +0200 Subject: [PATCH 027/190] extensions moved from presets to standalone publisher widgets --- .../widgets/widget_drop_frame.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pype/modules/standalonepublish/widgets/widget_drop_frame.py b/pype/modules/standalonepublish/widgets/widget_drop_frame.py index 80e67aa69a..76904af0cb 100644 --- a/pype/modules/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/modules/standalonepublish/widgets/widget_drop_frame.py @@ -10,6 +10,34 @@ from . import DropEmpty, ComponentsList, ComponentItem class DropDataFrame(QtWidgets.QFrame): + image_extensions = [ + ".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave", ".cal", + ".cin", ".cpc", ".cpt", ".dds", ".dpx", ".ecw", ".exr", ".fits", + ".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc", ".icer", + ".icns", ".ico", ".cur", ".ics", ".ilbm", ".jbig", ".jbig2", + ".jng", ".jpeg", ".jpeg-ls", ".jpeg", ".2000", ".jpg", ".xr", + ".jpeg", ".xt", ".jpeg-hdr", ".kra", ".mng", ".miff", ".nrrd", + ".ora", ".pam", ".pbm", ".pgm", ".ppm", ".pnm", ".pcx", ".pgf", + ".pictor", ".png", ".psd", ".psb", ".psp", ".qtvr", ".ras", + ".rgbe", ".logluv", ".tiff", ".sgi", ".tga", ".tiff", ".tiff/ep", + ".tiff/it", ".ufo", ".ufp", ".wbmp", ".webp", ".xbm", ".xcf", + ".xpm", ".xwd" + ] + video_extensions = [ + ".3g2", ".3gp", ".amv", ".asf", ".avi", ".drc", ".f4a", ".f4b", + ".f4p", ".f4v", ".flv", ".gif", ".gifv", ".m2v", ".m4p", ".m4v", + ".mkv", ".mng", ".mov", ".mp2", ".mp4", ".mpe", ".mpeg", ".mpg", + ".mpv", ".mxf", ".nsv", ".ogg", ".ogv", ".qt", ".rm", ".rmvb", + ".roq", ".svi", ".vob", ".webm", ".wmv", ".yuv" + ] + extensions = { + "nuke": [".nk"], + "maya": [".ma", ".mb"], + "houdini": [".hip"], + "image_file": image_extensions, + "video_file": video_extensions + } + def __init__(self, parent): super().__init__() self.parent_widget = parent From 9859a0032d06399dae79a8a417b45892edb237c6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 24 Jun 2020 17:27:26 +0200 Subject: [PATCH 028/190] skip using presets attribute --- .../widgets/widget_drop_frame.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pype/modules/standalonepublish/widgets/widget_drop_frame.py b/pype/modules/standalonepublish/widgets/widget_drop_frame.py index 76904af0cb..c91e906f45 100644 --- a/pype/modules/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/modules/standalonepublish/widgets/widget_drop_frame.py @@ -41,7 +41,6 @@ class DropDataFrame(QtWidgets.QFrame): def __init__(self, parent): super().__init__() self.parent_widget = parent - self.presets = config.get_presets()['standalone_publish'] self.setAcceptDrops(True) layout = QtWidgets.QVBoxLayout(self) @@ -54,7 +53,9 @@ class DropDataFrame(QtWidgets.QFrame): QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.drop_widget.sizePolicy().hasHeightForWidth()) + sizePolicy.setHeightForWidth( + self.drop_widget.sizePolicy().hasHeightForWidth() + ) self.drop_widget.setSizePolicy(sizePolicy) layout.addWidget(self.drop_widget) @@ -283,8 +284,8 @@ class DropDataFrame(QtWidgets.QFrame): file_info = data['file_info'] if ( - ext in self.presets['extensions']['image_file'] or - ext in self.presets['extensions']['video_file'] + ext in self.image_extensions + or ext in self.video_extensions ): probe_data = self.load_data_with_probe(filepath) if 'fps' not in data: @@ -321,7 +322,7 @@ class DropDataFrame(QtWidgets.QFrame): data[key] = value icon = 'default' - for ico, exts in self.presets['extensions'].items(): + for ico, exts in self.extensions.items(): if ext in exts: icon = ico break @@ -332,17 +333,16 @@ class DropDataFrame(QtWidgets.QFrame): icon += 's' data['icon'] = icon data['thumb'] = ( - ext in self.presets['extensions']['image_file'] or - ext in self.presets['extensions']['video_file'] + ext in self.image_extensions + or ext in self.video_extensions ) data['prev'] = ( - ext in self.presets['extensions']['video_file'] or - (new_is_seq and ext in self.presets['extensions']['image_file']) + ext in self.video_extensions + or (new_is_seq and ext in self.image_extensions) ) actions = [] - found = False for item in self.components_list.widgets(): if data['ext'] != item.in_data['ext']: From a0096d6cdf062c81fb1c4c9dc13e27aa49da73f6 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 25 Jun 2020 11:14:06 +0100 Subject: [PATCH 029/190] Rig support for namespaces --- pype/plugins/blender/load/load_model.py | 21 ++++----- pype/plugins/blender/load/load_rig.py | 60 +++++++++++++------------ 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index b16f5a40b1..6f34b40e70 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -17,10 +17,6 @@ class BlendModelLoader(plugin.AssetLoader): Because they come from a .blend file we can simply link the collection that contains the model. There is no further need to 'containerise' it. - - Warning: - Loading the same asset more then once is not properly supported at the - moment. """ families = ["model"] @@ -102,16 +98,16 @@ class BlendModelLoader(plugin.AssetLoader): asset, subset, namespace ) - collection = bpy.data.collections.new(lib_container) + container = bpy.data.collections.new(lib_container) blender.pipeline.containerise_existing( - collection, + container, name, namespace, context, self.__class__.__name__, ) - container_metadata = collection.get( + container_metadata = container.get( blender.pipeline.AVALON_PROPERTY) container_metadata["libpath"] = libpath @@ -125,8 +121,8 @@ class BlendModelLoader(plugin.AssetLoader): # Save the list of objects in the metadata container container_metadata["objects"] = obj_container.all_objects - nodes = list(collection.objects) - nodes.append(collection) + nodes = list(container.objects) + nodes.append(container) self[:] = nodes return nodes @@ -148,7 +144,7 @@ class BlendModelLoader(plugin.AssetLoader): libpath = Path(api.get_representation_path(representation)) extension = libpath.suffix.lower() - logger.debug( + logger.info( "Container: %s\nRepresentation: %s", pformat(container, indent=2), pformat(representation, indent=2), @@ -194,11 +190,12 @@ class BlendModelLoader(plugin.AssetLoader): self._remove(objects, obj_container) - objects_list = self._process( + obj_container = self._process( str(libpath), lib_container, collection.name) # Save the list of objects in the metadata container - collection_metadata["objects"] = objects_list + collection_metadata["obj_container"] = obj_container + collection_metadata["objects"] = obj_container.all_objects collection_metadata["libpath"] = str(libpath) collection_metadata["representation"] = str(representation["_id"]) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 3e53ff0363..41343e9c3a 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -7,20 +7,16 @@ from typing import Dict, List, Optional from avalon import api, blender import bpy -import pype.hosts.blender.plugin +import pype.hosts.blender.plugin as plugin -logger = logging.getLogger("pype").getChild("blender").getChild("load_model") +logger = logging.getLogger("pype").getChild("blender").getChild("load_rig") -class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): +class BlendRigLoader(plugin.AssetLoader): """Load rigs from a .blend file. Because they come from a .blend file we can simply link the collection that contains the model. There is no further need to 'containerise' it. - - Warning: - Loading the same asset more then once is not properly supported at the - moment. """ families = ["rig"] @@ -33,7 +29,6 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): def _remove(self, objects, lib_container): for obj in objects: - if obj.type == 'ARMATURE': bpy.data.armatures.remove(obj.data) elif obj.type == 'MESH': @@ -44,8 +39,12 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): bpy.data.collections.remove(bpy.data.collections[lib_container]) - def _process(self, libpath, lib_container, container_name, action): + def prepare_data(self, data, container_name): + name = data.name + data = data.make_local() + data.name = f"{name}:{container_name}" + def _process(self, libpath, lib_container, container_name, action): relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( libpath, link=True, relative=relative @@ -57,6 +56,7 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): scene.collection.children.link(bpy.data.collections[lib_container]) rig_container = scene.collection.children[lib_container].make_local() + rig_container.name = container_name meshes = [] armatures = [ @@ -65,15 +65,15 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): objects_list = [] for child in rig_container.children: - child.make_local() + self.prepare_data(child, container_name) meshes.extend( child.objects ) # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. for obj in meshes + armatures: - obj = obj.make_local() - obj.data.make_local() + self.prepare_data(obj, container_name) + self.prepare_data(obj.data, container_name) if not obj.get(blender.pipeline.AVALON_PROPERTY): obj[blender.pipeline.AVALON_PROPERTY] = dict() @@ -84,13 +84,11 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): if obj.type == 'ARMATURE' and action is not None: obj.animation_data.action = action - objects_list.append(obj) - rig_container.pop(blender.pipeline.AVALON_PROPERTY) bpy.ops.object.select_all(action='DESELECT') - return objects_list + return rig_container def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, @@ -107,13 +105,17 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): libpath = self.fname asset = context["asset"]["name"] subset = context["subset"]["name"] - lib_container = pype.hosts.blender.plugin.asset_name(asset, subset) - container_name = pype.hosts.blender.plugin.asset_name( + lib_container = plugin.asset_name( + asset, subset + ) + namespace = namespace or plugin.asset_namespace( + asset, subset + ) + container_name = plugin.asset_name( asset, subset, namespace ) container = bpy.data.collections.new(lib_container) - container.name = container_name blender.pipeline.containerise_existing( container, name, @@ -128,11 +130,13 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - objects_list = self._process( + obj_container = self._process( libpath, lib_container, container_name, None) + container_metadata["obj_container"] = obj_container + # Save the list of objects in the metadata container - container_metadata["objects"] = objects_list + container_metadata["objects"] = obj_container.all_objects nodes = list(container.objects) nodes.append(container) @@ -151,11 +155,9 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): Warning: No nested collections are supported at the moment! """ - collection = bpy.data.collections.get( container["objectName"] ) - libpath = Path(api.get_representation_path(representation)) extension = libpath.suffix.lower() @@ -177,7 +179,7 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): assert libpath.is_file(), ( f"The file doesn't exist: {libpath}" ) - assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, ( + assert extension in plugin.VALID_EXTENSIONS, ( f"Unsupported file: {libpath}" ) @@ -186,6 +188,7 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): collection_libpath = collection_metadata["libpath"] objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] + obj_container = collection_metadata["obj_container"] normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) @@ -208,13 +211,14 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): action = armatures[0].animation_data.action - self._remove(objects, lib_container) + self._remove(objects, obj_container) - objects_list = self._process( + obj_container = self._process( str(libpath), lib_container, collection.name, action) # Save the list of objects in the metadata container - collection_metadata["objects"] = objects_list + collection_metadata["obj_container"] = obj_container + collection_metadata["objects"] = obj_container.all_objects collection_metadata["libpath"] = str(libpath) collection_metadata["representation"] = str(representation["_id"]) @@ -246,9 +250,9 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] - lib_container = collection_metadata["lib_container"] + obj_container = collection_metadata["obj_container"] - self._remove(objects, lib_container) + self._remove(objects, obj_container) bpy.data.collections.remove(collection) From 7f30c245b957c947cfe2c9b6b0816e511df99572 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Jun 2020 17:22:09 +0200 Subject: [PATCH 030/190] use HOST_WORKFILE_EXTENSIONS variable from avalon for nukestudio extensions --- pype/hosts/nukestudio/workio.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/hosts/nukestudio/workio.py b/pype/hosts/nukestudio/workio.py index eee6654a4c..2cf898aa33 100644 --- a/pype/hosts/nukestudio/workio.py +++ b/pype/hosts/nukestudio/workio.py @@ -6,8 +6,9 @@ from pype.api import Logger log = Logger().get_logger(__name__, "nukestudio") + def file_extensions(): - return [".hrox"] + return api.HOST_WORKFILE_EXTENSIONS["nukestudio"] def has_unsaved_changes(): From f19ffc13e619302f389cf6166d2a12aa68eca83d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Jun 2020 17:22:57 +0200 Subject: [PATCH 031/190] ftrack app launcher sets AVALON_LAST_WORKFILE variable where path to last workfile is stored --- pype/modules/ftrack/lib/ftrack_app_handler.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/pype/modules/ftrack/lib/ftrack_app_handler.py b/pype/modules/ftrack/lib/ftrack_app_handler.py index 00bd13fd73..df6420a5f7 100644 --- a/pype/modules/ftrack/lib/ftrack_app_handler.py +++ b/pype/modules/ftrack/lib/ftrack_app_handler.py @@ -4,9 +4,11 @@ import copy import platform import avalon.lib import acre +import getpass from pype import lib as pypelib from pype.api import config, Anatomy from .ftrack_action_handler import BaseAction +from avalon.api import last_workfile, HOST_WORKFILE_EXTENSIONS class AppAction(BaseAction): @@ -152,10 +154,11 @@ class AppAction(BaseAction): hierarchy = "" asset_doc_parents = asset_document["data"].get("parents") - if len(asset_doc_parents) > 0: + if asset_doc_parents: hierarchy = os.path.join(*asset_doc_parents) application = avalon.lib.get_application(self.identifier) + host_name = application["application_dir"] data = { "project": { "name": entity["project"]["full_name"], @@ -163,7 +166,7 @@ class AppAction(BaseAction): }, "task": entity["name"], "asset": asset_name, - "app": application["application_dir"], + "app": host_name, "hierarchy": hierarchy } @@ -187,6 +190,21 @@ class AppAction(BaseAction): except FileExistsError: pass + last_workfile_path = None + extensions = HOST_WORKFILE_EXTENSIONS.get(host_name) + if extensions: + # Find last workfile + file_template = anatomy.templates["work"]["file"] + data.update({ + "version": 1, + "user": getpass.getuser(), + "ext": extensions[0] + }) + + last_workfile_path = last_workfile( + workdir, file_template, data, extensions, True + ) + # set environments for Avalon prep_env = copy.deepcopy(os.environ) prep_env.update({ @@ -198,6 +216,8 @@ class AppAction(BaseAction): "AVALON_HIERARCHY": hierarchy, "AVALON_WORKDIR": workdir }) + if last_workfile_path: + prep_env["AVALON_LAST_WORKFILE"] = last_workfile_path prep_env.update(anatomy.roots_obj.root_environments()) # collect all parents from the task From 7ca6e8f751f16f6005f444060b2a2108f4e5a662 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 26 Jun 2020 14:52:46 +0200 Subject: [PATCH 032/190] fix setData return type in model items --- pype/tools/pyblish_pype/model.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index 203b512d12..9086003258 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -319,7 +319,7 @@ class PluginItem(QtGui.QStandardItem): return False self.plugin.active = value self.emitDataChanged() - return True + return elif role == Roles.PluginActionProgressRole: if isinstance(value, list): @@ -652,14 +652,14 @@ class InstanceItem(QtGui.QStandardItem): def setData(self, value, role=(QtCore.Qt.UserRole + 1)): if role == QtCore.Qt.CheckStateRole: if not self.data(Roles.IsEnabledRole): - return False + return self.instance.data["publish"] = value self.emitDataChanged() - return True + return if role == Roles.IsEnabledRole: if not self.instance.optional: - return False + return if role == Roles.PublishFlagsRole: if isinstance(value, list): @@ -692,12 +692,12 @@ class InstanceItem(QtGui.QStandardItem): self.instance._publish_states = value self.emitDataChanged() - return True + return if role == Roles.LogRecordsRole: self.instance._logs = value self.emitDataChanged() - return True + return return super(InstanceItem, self).setData(value, role) From c770d1f7eeb7b164d78bc498fe69af735edb717f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 26 Jun 2020 14:53:31 +0200 Subject: [PATCH 033/190] PluginDelegate stays same as OverviewGroupSection --- pype/tools/pyblish_pype/delegate.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/pype/tools/pyblish_pype/delegate.py b/pype/tools/pyblish_pype/delegate.py index e88835b81a..11e12e0c5a 100644 --- a/pype/tools/pyblish_pype/delegate.py +++ b/pype/tools/pyblish_pype/delegate.py @@ -279,14 +279,14 @@ class InstanceItemDelegate(QtWidgets.QStyledItemDelegate): return QtCore.QSize(option.rect.width(), 20) -class OverviewGroupSection(QtWidgets.QStyledItemDelegate): - """Generic delegate for section header""" +class PluginDelegate(QtWidgets.QStyledItemDelegate): + """Generic delegate for plugin header""" item_class = None def __init__(self, parent): - super(OverviewGroupSection, self).__init__(parent) - self.item_delegate = self.item_class(parent) + super(PluginDelegate, self).__init__(parent) + self.item_delegate = PluginItemDelegate(parent) def paint(self, painter, option, index): if index.data(Roles.TypeRole) in ( @@ -343,7 +343,7 @@ class OverviewGroupSection(QtWidgets.QStyledItemDelegate): painter.setFont(fonts["awesome6"]) painter.setPen(QtGui.QPen(colors["idle"])) - painter.drawText(expander_rect, expander_icon) + painter.drawText(expander_rect, QtCore.Qt.AlignCenter, expander_icon) # Draw label painter.setFont(fonts["h5"]) @@ -362,11 +362,6 @@ class OverviewGroupSection(QtWidgets.QStyledItemDelegate): return QtCore.QSize(option.rect.width(), 20) -class PluginDelegate(OverviewGroupSection): - """Generic delegate for model items in proxy tree view""" - item_class = PluginItemDelegate - - class InstanceDelegate(OverviewGroupSection): """Generic delegate for model items in proxy tree view""" item_class = InstanceItemDelegate From 3e5a49cd73a54b4dacb9ec49819df58bbe389034 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 26 Jun 2020 14:53:59 +0200 Subject: [PATCH 034/190] InstanceDelegate can paint 2 parts, expand button and label itself --- pype/tools/pyblish_pype/delegate.py | 138 +++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 5 deletions(-) diff --git a/pype/tools/pyblish_pype/delegate.py b/pype/tools/pyblish_pype/delegate.py index 11e12e0c5a..2f1e9266ea 100644 --- a/pype/tools/pyblish_pype/delegate.py +++ b/pype/tools/pyblish_pype/delegate.py @@ -279,6 +279,139 @@ class InstanceItemDelegate(QtWidgets.QStyledItemDelegate): return QtCore.QSize(option.rect.width(), 20) +class InstanceDelegate(QtWidgets.QStyledItemDelegate): + """Generic delegate for instance header""" + + def __init__(self, parent): + super(InstanceDelegate, self).__init__(parent) + self.item_delegate = InstanceItemDelegate(parent) + + def paint(self, painter, option, index): + if index.data(Roles.TypeRole) in ( + model.InstanceType, model.PluginType + ): + self.item_delegate.paint(painter, option, index) + return + + self.group_item_paint(painter, option, index) + + def group_item_paint(self, painter, option, index): + """Paint text + _ + My label + """ + body_rect = QtCore.QRectF(option.rect) + bg_rect = QtCore.QRectF( + body_rect.left(), body_rect.top() + 1, + body_rect.width() - 5, body_rect.height() - 2 + ) + + expander_rect = QtCore.QRectF(bg_rect) + expander_rect.setWidth(EXPANDER_WIDTH) + + remainder_rect = QtCore.QRectF( + expander_rect.x() + expander_rect.width(), + expander_rect.y(), + bg_rect.width() - expander_rect.width(), + expander_rect.height() + ) + + radius = 8.0 + width = float(expander_rect.width()) + height = float(expander_rect.height()) + x_pos = expander_rect.x() + y_pos = expander_rect.y() + + expander_path = QtGui.QPainterPath() + expander_path.moveTo(x_pos + width - radius, y_pos) + expander_path.lineTo(x_pos + width, y_pos) + expander_path.lineTo(x_pos + width, y_pos + height) + expander_path.lineTo(x_pos + width - radius, y_pos + height) + expander_path.arcTo(x_pos, y_pos, radius, height, 270.0, -180.0) + expander_path.closeSubpath() + + width = float(remainder_rect.width()) + height = float(remainder_rect.height()) + x_pos = remainder_rect.x() + y_pos = remainder_rect.y() + + remainder_path = QtGui.QPainterPath() + remainder_path.moveTo(x_pos, y_pos) + remainder_path.lineTo(x_pos + width - radius, y_pos) + remainder_path.arcTo( + x_pos + width - radius, y_pos, + radius, height, + 90.0, -180.0 + ) + remainder_path.lineTo(x_pos, y_pos + height) + remainder_path.lineTo(x_pos, y_pos) + remainder_path.closeSubpath() + + mouse_pos = option.widget.mapFromGlobal(QtGui.QCursor.pos()) + painted_expander = False + painted_remainder = False + if option.state & QtWidgets.QStyle.State_Selected: + if expander_rect.contains(mouse_pos): + painter.fillPath(expander_path, colors["expander-selected"]) + painted_expander = True + else: + painter.fillPath(remainder_path, colors["group-selected"]) + painted_remainder = True + + elif option.state & QtWidgets.QStyle.State_MouseOver: + if expander_rect.contains(mouse_pos): + painter.fillPath(expander_path, colors["expander-hover"]) + painted_expander = True + else: + painter.fillPath(remainder_path, colors["group-hover"]) + painted_remainder = True + + if not painted_expander: + painter.fillPath(expander_path, colors["expander-bg"]) + + if not painted_remainder: + painter.fillPath(remainder_path, colors["group"]) + + text_height = font_metrics["awesome6"].height() + adjust_value = (expander_rect.height() - text_height) / 2 + expander_rect.adjust( + adjust_value + 1.5, adjust_value - 0.5, + -adjust_value + 1.5, -adjust_value - 0.5 + ) + + offset = (remainder_rect.height() - font_metrics["h5"].height()) / 2 + label_rect = QtCore.QRectF(remainder_rect.adjusted( + 5, offset - 1, 0, 0 + )) + + expander_icon = icons["plus-sign"] + + expanded = self.parent().isExpanded(index) + if expanded: + expander_icon = icons["minus-sign"] + label = index.data(QtCore.Qt.DisplayRole) + label = font_metrics["h5"].elidedText( + label, QtCore.Qt.ElideRight, label_rect.width() + ) + + # Maintain reference to state, so we can restore it once we're done + painter.save() + + painter.setFont(fonts["awesome6"]) + painter.setPen(QtGui.QPen(colors["idle"])) + painter.drawText(expander_rect, QtCore.Qt.AlignCenter, expander_icon) + + # Draw label + painter.setFont(fonts["h5"]) + painter.drawText(label_rect, label) + + # Ok, we're done, tidy up. + painter.restore() + + def sizeHint(self, option, index): + return QtCore.QSize(option.rect.width(), 20) + + class PluginDelegate(QtWidgets.QStyledItemDelegate): """Generic delegate for plugin header""" @@ -362,11 +495,6 @@ class PluginDelegate(QtWidgets.QStyledItemDelegate): return QtCore.QSize(option.rect.width(), 20) -class InstanceDelegate(OverviewGroupSection): - """Generic delegate for model items in proxy tree view""" - item_class = InstanceItemDelegate - - class ArtistDelegate(QtWidgets.QStyledItemDelegate): """Delegate used on Artist page""" From d8b5c9ab10834bfc87d605ed7510c27836d9a1b0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 26 Jun 2020 14:54:17 +0200 Subject: [PATCH 035/190] expander width is defined in constants --- pype/tools/pyblish_pype/constants.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/tools/pyblish_pype/constants.py b/pype/tools/pyblish_pype/constants.py index 5395d1fd0a..03536fb829 100644 --- a/pype/tools/pyblish_pype/constants.py +++ b/pype/tools/pyblish_pype/constants.py @@ -1,5 +1,7 @@ from Qt import QtCore +EXPANDER_WIDTH = 20 + def flags(*args, **kwargs): type_name = kwargs.pop("type_name", "Flags") From 977245f07d3197461aa2e61a2584e62d84aee2c9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 26 Jun 2020 14:55:32 +0200 Subject: [PATCH 036/190] Created custom plugin view --- pype/tools/pyblish_pype/view.py | 36 ++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/pype/tools/pyblish_pype/view.py b/pype/tools/pyblish_pype/view.py index 450f56421c..0d129c6dff 100644 --- a/pype/tools/pyblish_pype/view.py +++ b/pype/tools/pyblish_pype/view.py @@ -84,8 +84,6 @@ class OverviewView(QtWidgets.QTreeView): self.setRootIsDecorated(False) self.setIndentation(0) - self.clicked.connect(self.item_expand) - def event(self, event): if not event.type() == QtCore.QEvent.KeyPress: return super(OverviewView, self).event(event) @@ -113,6 +111,24 @@ class OverviewView(QtWidgets.QTreeView): def focusOutEvent(self, event): self.selectionModel().clear() + def mouseReleaseEvent(self, event): + if event.button() in (QtCore.Qt.LeftButton, QtCore.Qt.RightButton): + # Deselect all group labels + indexes = self.selectionModel().selectedIndexes() + for index in indexes: + if index.data(Roles.TypeRole) == model.GroupType: + self.selectionModel().select( + index, QtCore.QItemSelectionModel.Deselect + ) + + return super(OverviewView, self).mouseReleaseEvent(event) + + +class PluginView(OverviewView): + def __init__(self, *args, **kwargs): + super(PluginView, self).__init__(*args, **kwargs) + self.clicked.connect(self.item_expand) + def item_expand(self, index): if index.data(Roles.TypeRole) == model.GroupType: if self.isExpanded(index): @@ -125,23 +141,19 @@ class OverviewView(QtWidgets.QTreeView): indexes = self.selectionModel().selectedIndexes() if len(indexes) == 1: index = indexes[0] - # If instance or Plugin - if index.data(Roles.TypeRole) in ( - model.InstanceType, model.PluginType + pos_index = self.indexAt(event.pos()) + # If instance or Plugin and is selected + if ( + index == pos_index + and index.data(Roles.TypeRole) == model.PluginType ): if event.pos().x() < 20: self.toggled.emit(index, None) elif event.pos().x() > self.width() - 20: self.show_perspective.emit(index) - # Deselect all group labels - for index in indexes: - if index.data(Roles.TypeRole) == model.GroupType: - self.selectionModel().select( - index, QtCore.QItemSelectionModel.Deselect - ) + return super(PluginView, self).mouseReleaseEvent(event) - return super(OverviewView, self).mouseReleaseEvent(event) class TerminalView(QtWidgets.QTreeView): From 49fe164c12ae52c11815d4a06d6fad9ed2b7e841 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 26 Jun 2020 14:55:46 +0200 Subject: [PATCH 037/190] implemented view for instances --- pype/tools/pyblish_pype/view.py | 68 ++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/pype/tools/pyblish_pype/view.py b/pype/tools/pyblish_pype/view.py index 0d129c6dff..e4bc3a483e 100644 --- a/pype/tools/pyblish_pype/view.py +++ b/pype/tools/pyblish_pype/view.py @@ -1,6 +1,6 @@ from Qt import QtCore, QtWidgets from . import model -from .constants import Roles +from .constants import Roles, EXPANDER_WIDTH # Imported when used widgets = None @@ -155,6 +155,72 @@ class PluginView(OverviewView): return super(PluginView, self).mouseReleaseEvent(event) +class InstanceView(OverviewView): + def __init__(self, parent=None): + super(InstanceView, self).__init__(parent) + self.viewport().setMouseTracking(True) + + def mouseMoveEvent(self, event): + index = self.indexAt(event.pos()) + if index.data(Roles.TypeRole) == model.GroupType: + self.update(index) + + def item_expand(self, index, expand=None): + if expand is None: + expand = not self.isExpanded(index) + + if expand: + self.expand(index) + else: + self.collapse(index) + + def group_toggle(self, index): + model = index.model() + + chilren_indexes_checked = [] + chilren_indexes_unchecked = [] + for idx in range(model.rowCount(index)): + child_index = model.index(idx, 0, index) + if not child_index.data(Roles.IsEnabledRole): + continue + + if child_index.data(QtCore.Qt.CheckStateRole): + chilren_indexes_checked.append(child_index) + else: + chilren_indexes_unchecked.append(child_index) + + if chilren_indexes_checked: + to_change_indexes = chilren_indexes_checked + new_state = False + else: + to_change_indexes = chilren_indexes_unchecked + new_state = True + + for index in to_change_indexes: + model.setData(index, new_state, QtCore.Qt.CheckStateRole) + self.toggled.emit(index, new_state) + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + indexes = self.selectionModel().selectedIndexes() + if len(indexes) == 1: + index = indexes[0] + pos_index = self.indexAt(event.pos()) + if index == pos_index: + # If instance or Plugin + if index.data(Roles.TypeRole) == model.InstanceType: + if event.pos().x() < 20: + self.toggled.emit(index, None) + elif event.pos().x() > self.width() - 20: + self.show_perspective.emit(index) + else: + if event.pos().x() < EXPANDER_WIDTH: + self.item_expand(index) + else: + self.group_toggle(index) + self.item_expand(index, True) + return super(InstanceView, self).mouseReleaseEvent(event) + class TerminalView(QtWidgets.QTreeView): # An item is requesting to be toggled, with optional forced-state From faba238fb06972d22154a9ade3c6f88898033adb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 26 Jun 2020 14:56:09 +0200 Subject: [PATCH 038/190] added colors for instance delegate --- pype/tools/pyblish_pype/delegate.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pype/tools/pyblish_pype/delegate.py b/pype/tools/pyblish_pype/delegate.py index 2f1e9266ea..641158295b 100644 --- a/pype/tools/pyblish_pype/delegate.py +++ b/pype/tools/pyblish_pype/delegate.py @@ -5,7 +5,7 @@ from Qt import QtWidgets, QtGui, QtCore from . import model from .awesome import tags as awesome from .constants import ( - PluginStates, InstanceStates, PluginActionStates, Roles + PluginStates, InstanceStates, PluginActionStates, Roles, EXPANDER_WIDTH ) colors = { @@ -19,7 +19,12 @@ colors = { "hover": QtGui.QColor(255, 255, 255, 10), "selected": QtGui.QColor(255, 255, 255, 20), "outline": QtGui.QColor("#333"), - "group": QtGui.QColor("#333") + "group": QtGui.QColor("#333"), + "group-hover": QtGui.QColor("#3c3c3c"), + "group-selected": QtGui.QColor("#555555"), + "expander-bg": QtGui.QColor("#222"), + "expander-hover": QtGui.QColor("#2d6c9f"), + "expander-selected": QtGui.QColor("#3784c5"), } scale_factors = {"darwin": 1.5} From ec6f10c4aadd2bc61326020b52de856ee058ece9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 26 Jun 2020 14:56:23 +0200 Subject: [PATCH 039/190] there are used instance and plugin views in main app --- pype/tools/pyblish_pype/window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 3c7808496c..7d79e0e26c 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -160,14 +160,14 @@ class Window(QtWidgets.QDialog): # TODO add parent overview_page = QtWidgets.QWidget() - overview_instance_view = view.OverviewView(parent=overview_page) + overview_instance_view = view.InstanceView(parent=overview_page) overview_instance_delegate = delegate.InstanceDelegate( parent=overview_instance_view ) overview_instance_view.setItemDelegate(overview_instance_delegate) overview_instance_view.setModel(instance_model) - overview_plugin_view = view.OverviewView(parent=overview_page) + overview_plugin_view = view.PluginView(parent=overview_page) overview_plugin_delegate = delegate.PluginDelegate( parent=overview_plugin_view ) From 546eb1e73472a229e2d3eccbcbe399a9a5c6127c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 26 Jun 2020 15:56:30 +0200 Subject: [PATCH 040/190] tray can set attributes for modules --- pype/tools/tray/pype_tray.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index eec8f61cc4..5b7188495f 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -68,6 +68,7 @@ class TrayManager: items = [] # Get booleans is module should be used usages = self.items.get("item_usage") or {} + attributes = self.items.get("attributes") or {} for item in self.items.get("item_import", []): import_path = item.get("import_path") title = item.get("title") @@ -76,12 +77,19 @@ class TrayManager: if item_usage is None: item_usage = usages.get(import_path, True) - if item_usage: - items.append(item) - else: + if not item_usage: if not title: title = import_path self.log.debug("{} - Module ignored".format(title)) + continue + + _attributes = attributes.get(title) + if _attributes is None: + _attributes = attributes.get(import_path) + + if _attributes: + item["attributes"] = _attributes + items.append(item) if items: self.process_items(items, self.tray_widget.menu) @@ -158,11 +166,29 @@ class TrayManager: import_path = item.get('import_path', None) title = item.get('title', import_path) fromlist = item.get('fromlist', []) + attributes = item.get("attributes", {}) try: module = __import__( "{}".format(import_path), fromlist=fromlist ) + klass = getattr(module, "CLASS_DEFINIION", None) + if not klass and attributes: + self.log.error(( + "There are defined attributes for module \"{}\" but" + "module does not have defined \"CLASS_DEFINIION\"." + ).format(import_path)) + + elif klass and attributes: + for key, value in attributes.items(): + if hasattr(klass, key): + setattr(klass, key, value) + else: + self.log.error(( + "Module \"{}\" does not have attribute \"{}\"." + " Check your settings please." + ).format(import_path, key)) + obj = module.tray_init(self.tray_widget, self.main_window) name = obj.__class__.__name__ if hasattr(obj, 'tray_menu'): From 2afe643284cd6755987844ca9dcb1fb09e87f067 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 26 Jun 2020 16:00:25 +0200 Subject: [PATCH 041/190] rest api and timers manager got CLASS_DEFINITION variable in module definition --- pype/modules/rest_api/__init__.py | 2 ++ pype/modules/timers_manager/__init__.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/pype/modules/rest_api/__init__.py b/pype/modules/rest_api/__init__.py index fbeec00c88..55253bc58b 100644 --- a/pype/modules/rest_api/__init__.py +++ b/pype/modules/rest_api/__init__.py @@ -2,6 +2,8 @@ from .rest_api import RestApiServer from .base_class import RestApi, abort, route, register_statics from .lib import RestMethods, CallbackResult +CLASS_DEFINIION = RestApiServer + def tray_init(tray_widget, main_widget): return RestApiServer() diff --git a/pype/modules/timers_manager/__init__.py b/pype/modules/timers_manager/__init__.py index a6c4535f3d..a8a478d7ae 100644 --- a/pype/modules/timers_manager/__init__.py +++ b/pype/modules/timers_manager/__init__.py @@ -1,6 +1,8 @@ from .timers_manager import TimersManager from .widget_user_idle import WidgetUserIdle +CLASS_DEFINIION = TimersManager + def tray_init(tray_widget, main_widget): return TimersManager(tray_widget, main_widget) From 79542b6d9487c4cde7711eed8e85443f2e0d01bf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 26 Jun 2020 16:01:11 +0200 Subject: [PATCH 042/190] rest api and timers manager does not load presets but are based on class attributes --- pype/modules/rest_api/rest_api.py | 18 ++++-------- pype/modules/timers_manager/timers_manager.py | 28 ++++++++++--------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/pype/modules/rest_api/rest_api.py b/pype/modules/rest_api/rest_api.py index 5f0969a5a2..81763e8b9f 100644 --- a/pype/modules/rest_api/rest_api.py +++ b/pype/modules/rest_api/rest_api.py @@ -6,7 +6,7 @@ from socketserver import ThreadingMixIn from http.server import HTTPServer from .lib import RestApiFactory, Handler from .base_class import route, register_statics -from pype.api import config, Logger +from pype.api import Logger log = Logger().get_logger("RestApiServer") @@ -85,20 +85,14 @@ class RestApiServer: Callback may return many types. For more information read docstring of `_handle_callback_result` defined in handler. """ + default_port = 8011 + exclude_ports = [] + def __init__(self): self.qaction = None self.failed_icon = None self._is_running = False - try: - self.presets = config.get_presets()["services"]["rest_api"] - except Exception: - self.presets = {"default_port": 8011, "exclude_ports": []} - log.debug(( - "There are not set presets for RestApiModule." - " Using defaults \"{}\"" - ).format(str(self.presets))) - port = self.find_port() self.rest_api_thread = RestApiThread(self, port) @@ -126,8 +120,8 @@ class RestApiServer: RestApiFactory.register_obj(obj) def find_port(self): - start_port = self.presets["default_port"] - exclude_ports = self.presets["exclude_ports"] + start_port = self.default_port + exclude_ports = self.exclude_ports found_port = None # port check takes time so it's lowered to 100 ports for port in range(start_port, start_port+100): diff --git a/pype/modules/timers_manager/timers_manager.py b/pype/modules/timers_manager/timers_manager.py index cec730d007..16b6c658af 100644 --- a/pype/modules/timers_manager/timers_manager.py +++ b/pype/modules/timers_manager/timers_manager.py @@ -23,32 +23,33 @@ class TimersManager(metaclass=Singleton): If IdleManager is imported then is able to handle about stop timers when user idles for a long time (set in presets). """ - modules = [] - is_running = False - last_task = None + + # Presetable attributes + # - when timer will stop if idle manager is running (minutes) + full_time = 15 + # - how many minutes before the timer is stopped will popup the message + message_time = 0.5 def __init__(self, tray_widget, main_widget): self.log = Logger().get_logger(self.__class__.__name__) + + self.modules = [] + self.is_running = False + self.last_task = None + self.tray_widget = tray_widget self.main_widget = main_widget self.widget_user_idle = WidgetUserIdle(self) def set_signal_times(self): try: - timer_info = ( - config.get_presets() - .get('services') - .get('timers_manager') - .get('timer') - ) - full_time = int(float(timer_info['full_time'])*60) - message_time = int(float(timer_info['message_time'])*60) + full_time = int(self.full_time * 60) + message_time = int(self.message_time * 60) self.time_show_message = full_time - message_time self.time_stop_timer = full_time return True except Exception: - self.log.warning('Was not able to load presets for TimersManager') - return False + self.log.error("Couldn't set timer signals.", exc_info=True) def add_module(self, module): """ Adds module to context @@ -180,6 +181,7 @@ class SignalHandler(QtCore.QObject): signal_show_message = QtCore.Signal() signal_change_label = QtCore.Signal() signal_stop_timers = QtCore.Signal() + def __init__(self, cls): super().__init__() self.signal_show_message.connect(cls.show_message) From f4971c00cde6e2a4b52077e820e94b3ca3ee0816 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 26 Jun 2020 16:01:22 +0200 Subject: [PATCH 043/190] removed unused import --- pype/modules/timers_manager/widget_user_idle.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/modules/timers_manager/widget_user_idle.py b/pype/modules/timers_manager/widget_user_idle.py index 697c0a04d9..ae7e19dde9 100644 --- a/pype/modules/timers_manager/widget_user_idle.py +++ b/pype/modules/timers_manager/widget_user_idle.py @@ -1,4 +1,3 @@ -from pype.api import Logger from avalon import style from Qt import QtCore, QtGui, QtWidgets From 258d76ce6db0b73ff3f856623b0d30e9456ab0cd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 26 Jun 2020 17:16:10 +0200 Subject: [PATCH 044/190] loader plugin for DJV has hardcoded extensions and path to DJV is taken from DJV_PATH --- pype/plugins/global/load/open_djv.py | 41 +++++++++------------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/pype/plugins/global/load/open_djv.py b/pype/plugins/global/load/open_djv.py index 650936a4dc..a500333875 100644 --- a/pype/plugins/global/load/open_djv.py +++ b/pype/plugins/global/load/open_djv.py @@ -1,34 +1,27 @@ import os import subprocess -import json -from pype.api import config from avalon import api -def get_families(): - families = [] - paths = config.get_presets().get("djv_view", {}).get("config", {}).get( - "djv_paths", [] - ) - for path in paths: +def existing_djv_path(): + djv_paths = os.environ.get("DJV_PATH") or "" + for path in djv_paths.split(os.pathsep): if os.path.exists(path): - families.append("*") - break - return families - - -def get_representation(): - return config.get_presets().get("djv_view", {}).get("config", {}).get( - 'file_ext', [] - ) + return path + return None class OpenInDJV(api.Loader): """Open Image Sequence with system default""" - config_data = config.get_presets().get("djv_view", {}).get("config", {}) - families = get_families() - representations = get_representation() + djv_path = existing_djv_path() + families = ["*"] if djv_path else [] + representations = [ + "cin", "dpx", "avi", "dv", "gif", "flv", "mkv", "mov", "mpg", "mpeg", + "mp4", "m4v", "mxf", "iff", "z", "ifl", "jpeg", "jpg", "jfif", "lut", + "1dl", "exr", "pic", "png", "ppm", "pnm", "pgm", "pbm", "rla", "rpf", + "sgi", "rgba", "rgb", "bw", "tga", "tiff", "tif", "img" + ] label = "Open in DJV" order = -10 @@ -36,14 +29,6 @@ class OpenInDJV(api.Loader): color = "orange" def load(self, context, name, namespace, data): - self.djv_path = None - paths = config.get_presets().get("djv_view", {}).get("config", {}).get( - "djv_paths", [] - ) - for path in paths: - if os.path.exists(path): - self.djv_path = path - break directory = os.path.dirname(self.fname) from avalon.vendor import clique From cf171b08c9e8df31ba5e958e16a81ac98352107c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 26 Jun 2020 17:17:14 +0200 Subject: [PATCH 045/190] ftrack djv action has hardocoded extensions and find path to djv from DJV_PATH environment variable --- pype/modules/ftrack/actions/action_djvview.py | 71 +++---------------- 1 file changed, 11 insertions(+), 60 deletions(-) diff --git a/pype/modules/ftrack/actions/action_djvview.py b/pype/modules/ftrack/actions/action_djvview.py index 9708503ad1..6f667c0604 100644 --- a/pype/modules/ftrack/actions/action_djvview.py +++ b/pype/modules/ftrack/actions/action_djvview.py @@ -1,13 +1,7 @@ import os -import sys -import logging import subprocess from operator import itemgetter -import ftrack_api from pype.modules.ftrack.lib import BaseAction, statics_icon -from pype.api import Logger, config - -log = Logger().get_logger(__name__) class DJVViewAction(BaseAction): @@ -19,20 +13,18 @@ class DJVViewAction(BaseAction): type = 'Application' + allowed_types = [ + "cin", "dpx", "avi", "dv", "gif", "flv", "mkv", "mov", "mpg", "mpeg", + "mp4", "m4v", "mxf", "iff", "z", "ifl", "jpeg", "jpg", "jfif", "lut", + "1dl", "exr", "pic", "png", "ppm", "pnm", "pgm", "pbm", "rla", "rpf", + "sgi", "rgba", "rgb", "bw", "tga", "tiff", "tif", "img" + ] + def __init__(self, session, plugins_presets): '''Expects a ftrack_api.Session instance''' super().__init__(session, plugins_presets) - self.djv_path = None - self.config_data = config.get_presets()['djv_view']['config'] - self.set_djv_path() - - if self.djv_path is None: - return - - self.allowed_types = self.config_data.get( - 'file_ext', ["img", "mov", "exr"] - ) + self.djv_path = self.find_djv_path() def preregister(self): if self.djv_path is None: @@ -53,11 +45,10 @@ class DJVViewAction(BaseAction): return True return False - def set_djv_path(self): - for path in self.config_data.get("djv_paths", []): + def find_djv_path(self): + for path in (os.environ.get("DJV_PATH") or "").split(os.pathsep): if os.path.exists(path): - self.djv_path = path - break + return path def interface(self, session, entities, event): if event['data'].get('values', {}): @@ -221,43 +212,3 @@ def register(session, plugins_presets={}): """Register hooks.""" DJVViewAction(session, plugins_presets).register() - - -def main(arguments=None): - '''Set up logging and register action.''' - if arguments is None: - arguments = [] - - import argparse - parser = argparse.ArgumentParser() - # Allow setting of logging level from arguments. - loggingLevels = {} - for level in ( - logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, - logging.ERROR, logging.CRITICAL - ): - loggingLevels[logging.getLevelName(level).lower()] = level - - parser.add_argument( - '-v', '--verbosity', - help='Set the logging output verbosity.', - choices=loggingLevels.keys(), - default='info' - ) - namespace = parser.parse_args(arguments) - - # Set up basic logging - logging.basicConfig(level=loggingLevels[namespace.verbosity]) - - session = ftrack_api.Session() - register(session) - - # Wait for events - logging.info( - 'Registered actions and listening for events. Use Ctrl-C to abort.' - ) - session.event_hub.wait() - - -if __name__ == '__main__': - raise SystemExit(main(sys.argv[1:])) From fb5abe71b6e0879768160f5e0ea43ff8b070ff1e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 27 Jun 2020 08:42:05 +0200 Subject: [PATCH 046/190] fixed multiple selection in instance view --- pype/tools/pyblish_pype/view.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/tools/pyblish_pype/view.py b/pype/tools/pyblish_pype/view.py index e4bc3a483e..477303eae8 100644 --- a/pype/tools/pyblish_pype/view.py +++ b/pype/tools/pyblish_pype/view.py @@ -164,6 +164,7 @@ class InstanceView(OverviewView): index = self.indexAt(event.pos()) if index.data(Roles.TypeRole) == model.GroupType: self.update(index) + super(InstanceView, self).mouseMoveEvent(event) def item_expand(self, index, expand=None): if expand is None: From 97f30b6e691869d99d3b5f9611b85ad8642435e0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 27 Jun 2020 08:42:58 +0200 Subject: [PATCH 047/190] instance group has right radius --- pype/tools/pyblish_pype/delegate.py | 85 ++++++++++++++++++----------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/pype/tools/pyblish_pype/delegate.py b/pype/tools/pyblish_pype/delegate.py index 641158295b..8fce63ee28 100644 --- a/pype/tools/pyblish_pype/delegate.py +++ b/pype/tools/pyblish_pype/delegate.py @@ -14,17 +14,16 @@ colors = { "ok": QtGui.QColor("#77AE24"), "active": QtGui.QColor("#99CEEE"), "idle": QtCore.Qt.white, - "font": QtGui.QColor("#DDD"), "inactive": QtGui.QColor("#888"), "hover": QtGui.QColor(255, 255, 255, 10), "selected": QtGui.QColor(255, 255, 255, 20), "outline": QtGui.QColor("#333"), "group": QtGui.QColor("#333"), "group-hover": QtGui.QColor("#3c3c3c"), - "group-selected": QtGui.QColor("#555555"), + "group-selected-hover": QtGui.QColor("#555555"), "expander-bg": QtGui.QColor("#222"), "expander-hover": QtGui.QColor("#2d6c9f"), - "expander-selected": QtGui.QColor("#3784c5"), + "expander-selected-hover": QtGui.QColor("#3784c5") } scale_factors = {"darwin": 1.5} @@ -287,6 +286,8 @@ class InstanceItemDelegate(QtWidgets.QStyledItemDelegate): class InstanceDelegate(QtWidgets.QStyledItemDelegate): """Generic delegate for instance header""" + radius = 8.0 + def __init__(self, parent): super(InstanceDelegate, self).__init__(parent) self.item_delegate = InstanceItemDelegate(parent) @@ -321,18 +322,32 @@ class InstanceDelegate(QtWidgets.QStyledItemDelegate): expander_rect.height() ) - radius = 8.0 width = float(expander_rect.width()) height = float(expander_rect.height()) + x_pos = expander_rect.x() y_pos = expander_rect.y() + x_radius = min(self.radius, width / 2) + y_radius = min(self.radius, height / 2) + x_radius2 = x_radius * 2 + y_radius2 = y_radius * 2 + expander_path = QtGui.QPainterPath() - expander_path.moveTo(x_pos + width - radius, y_pos) + expander_path.moveTo(x_pos, y_pos + y_radius) + expander_path.arcTo( + x_pos, y_pos, + x_radius2, y_radius2, + 180.0, -90.0 + ) expander_path.lineTo(x_pos + width, y_pos) expander_path.lineTo(x_pos + width, y_pos + height) - expander_path.lineTo(x_pos + width - radius, y_pos + height) - expander_path.arcTo(x_pos, y_pos, radius, height, 270.0, -180.0) + expander_path.lineTo(x_pos + x_radius, y_pos + height) + expander_path.arcTo( + x_pos, y_pos + height - y_radius2, + x_radius2, y_radius2, + 270.0, -90.0 + ) expander_path.closeSubpath() width = float(remainder_rect.width()) @@ -340,42 +355,50 @@ class InstanceDelegate(QtWidgets.QStyledItemDelegate): x_pos = remainder_rect.x() y_pos = remainder_rect.y() + x_radius = min(self.radius, width / 2) + y_radius = min(self.radius, height / 2) + x_radius2 = x_radius * 2 + y_radius2 = y_radius * 2 + remainder_path = QtGui.QPainterPath() - remainder_path.moveTo(x_pos, y_pos) - remainder_path.lineTo(x_pos + width - radius, y_pos) + remainder_path.moveTo(x_pos + width, y_pos + height - y_radius) remainder_path.arcTo( - x_pos + width - radius, y_pos, - radius, height, - 90.0, -180.0 + x_pos + width - x_radius2, y_pos + height - y_radius2, + x_radius2, y_radius2, + 0.0, -90.0 ) remainder_path.lineTo(x_pos, y_pos + height) remainder_path.lineTo(x_pos, y_pos) + remainder_path.lineTo(x_pos + width - x_radius, y_pos) + remainder_path.arcTo( + x_pos + width - x_radius2, y_pos, + x_radius2, y_radius2, + 90.0, -90.0 + ) remainder_path.closeSubpath() - mouse_pos = option.widget.mapFromGlobal(QtGui.QCursor.pos()) - painted_expander = False - painted_remainder = False - if option.state & QtWidgets.QStyle.State_Selected: - if expander_rect.contains(mouse_pos): - painter.fillPath(expander_path, colors["expander-selected"]) - painted_expander = True - else: - painter.fillPath(remainder_path, colors["group-selected"]) - painted_remainder = True + painter.fillPath(expander_path, colors["expander-bg"]) + painter.fillPath(remainder_path, colors["group"]) - elif option.state & QtWidgets.QStyle.State_MouseOver: + mouse_pos = option.widget.mapFromGlobal(QtGui.QCursor.pos()) + selected = option.state & QtWidgets.QStyle.State_Selected + hovered = option.state & QtWidgets.QStyle.State_MouseOver + + if selected and hovered: + if expander_rect.contains(mouse_pos): + painter.fillPath( + expander_path, colors["expander-selected-hover"] + ) + else: + painter.fillPath( + remainder_path, colors["group-selected-hover"] + ) + + elif hovered: if expander_rect.contains(mouse_pos): painter.fillPath(expander_path, colors["expander-hover"]) - painted_expander = True else: painter.fillPath(remainder_path, colors["group-hover"]) - painted_remainder = True - - if not painted_expander: - painter.fillPath(expander_path, colors["expander-bg"]) - - if not painted_remainder: - painter.fillPath(remainder_path, colors["group"]) text_height = font_metrics["awesome6"].height() adjust_value = (expander_rect.height() - text_height) / 2 From 91d3d697e69edd593357ba98d980bf7885960d38 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 27 Jun 2020 08:43:17 +0200 Subject: [PATCH 048/190] plugin group has same hover and selection colors --- pype/tools/pyblish_pype/delegate.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pype/tools/pyblish_pype/delegate.py b/pype/tools/pyblish_pype/delegate.py index 8fce63ee28..cb9123bf3a 100644 --- a/pype/tools/pyblish_pype/delegate.py +++ b/pype/tools/pyblish_pype/delegate.py @@ -443,8 +443,6 @@ class InstanceDelegate(QtWidgets.QStyledItemDelegate): class PluginDelegate(QtWidgets.QStyledItemDelegate): """Generic delegate for plugin header""" - item_class = None - def __init__(self, parent): super(PluginDelegate, self).__init__(parent) self.item_delegate = PluginItemDelegate(parent) @@ -471,7 +469,14 @@ class PluginDelegate(QtWidgets.QStyledItemDelegate): radius = 8.0 bg_path = QtGui.QPainterPath() bg_path.addRoundedRect(bg_rect, radius, radius) - painter.fillPath(bg_path, colors["group"]) + hovered = option.state & QtWidgets.QStyle.State_MouseOver + selected = option.state & QtWidgets.QStyle.State_Selected + if hovered and selected: + painter.fillPath(bg_path, colors["group-selected-hover"]) + elif hovered: + painter.fillPath(bg_path, colors["group-hover"]) + else: + painter.fillPath(bg_path, colors["group"]) expander_rect = QtCore.QRectF(bg_rect) expander_rect.setWidth(expander_rect.height()) @@ -510,12 +515,6 @@ class PluginDelegate(QtWidgets.QStyledItemDelegate): painter.setFont(fonts["h5"]) painter.drawText(label_rect, label) - if option.state & QtWidgets.QStyle.State_MouseOver: - painter.fillPath(bg_path, colors["hover"]) - - if option.state & QtWidgets.QStyle.State_Selected: - painter.fillPath(bg_path, colors["selected"]) - # Ok, we're done, tidy up. painter.restore() From 279b221e33d8a1cca99736326570c978fb9b3f39 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 28 Jun 2020 15:40:00 +0100 Subject: [PATCH 049/190] Initial working version for palettes. --- pype/hosts/harmony/__init__.py | 5 +- pype/plugins/global/publish/integrate_new.py | 3 +- .../harmony/load/load_imagesequence.py | 22 +++++ pype/plugins/harmony/load/load_palette.py | 93 +++++++++++++++++++ .../harmony/publish/collect_palettes.py | 45 +++++++++ .../harmony/publish/extract_palette.py | 34 +++++++ 6 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 pype/plugins/harmony/load/load_palette.py create mode 100644 pype/plugins/harmony/publish/collect_palettes.py create mode 100644 pype/plugins/harmony/publish/extract_palette.py diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index 628397e777..4dc06cdf84 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -121,7 +121,10 @@ def check_inventory(): } func """ - outdated_nodes = [x["node"] for x in outdated_containers] + outdated_nodes = [] + for container in outdated_containers: + if container["loader"] == "ImageSequenceLoader": + outdated_nodes.append(container["name"]) harmony.send({"function": func, "args": [outdated_nodes]}) # Warn about outdated containers. diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 040ed9cd67..a33db2bcec 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -83,6 +83,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "textures", "action", "harmony.template", + "harmony.palette", "editorial" ] exclude_families = ["clip"] @@ -605,7 +606,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "type": "subset", "name": subset_name, "data": { - "families": instance.data.get('families') + "families": instance.data.get("families", []) }, "parent": asset["_id"] }).inserted_id diff --git a/pype/plugins/harmony/load/load_imagesequence.py b/pype/plugins/harmony/load/load_imagesequence.py index b56dba03d4..615188572e 100644 --- a/pype/plugins/harmony/load/load_imagesequence.py +++ b/pype/plugins/harmony/load/load_imagesequence.py @@ -3,6 +3,7 @@ import os import clique from avalon import api, harmony +import pype.lib copy_files = """function copyFile(srcFilename, dstFilename) { @@ -297,6 +298,27 @@ class ImageSequenceLoader(api.Loader): } ) + # Colour node. + func = """function func(args){ + for( var i =0; i <= args[0].length - 1; ++i) + { + var red_color = new ColorRGBA(255, 0, 0, 255); + var green_color = new ColorRGBA(0, 255, 0, 255); + if (args[1] == "red"){ + node.setColor(args[0], red_color); + } + if (args[1] == "green"){ + node.setColor(args[0], green_color); + } + } + } + func + """ + if pype.lib.is_latest(representation): + harmony.send({"function": func, "args": [node, "green"]}) + else: + harmony.send({"function": func, "args": [node, "red"]}) + harmony.imprint( node, {"representation": str(representation["_id"])} ) diff --git a/pype/plugins/harmony/load/load_palette.py b/pype/plugins/harmony/load/load_palette.py new file mode 100644 index 0000000000..cfb88ac841 --- /dev/null +++ b/pype/plugins/harmony/load/load_palette.py @@ -0,0 +1,93 @@ +import os +import shutil +import uuid + +from bson.objectid import ObjectId + +from avalon import api, harmony + + +class ImportPaletteLoader(api.Loader): + """Import palettes.""" + + families = ["harmony.palette"] + representations = ["plt"] + label = "Import Palette" + + def load(self, context, name=None, namespace=None, data=None): + name = self.load_palette(context["representation"]) + + return harmony.containerise( + name, + namespace, + name, + context, + self.__class__.__name__ + ) + + def load_palette(self, representation): + subset_name = representation["context"]["subset"] + name = subset_name.replace("palette", "") + name += "_{}".format(uuid.uuid4()) + + # Import new palette. + scene_path = harmony.send( + {"function": "scene.currentProjectPath"} + )["result"] + src = api.get_representation_path(representation) + dst = os.path.join( + scene_path, + "palette-library", + "{}.plt".format(name) + ) + shutil.copy(src, dst) + + func = """function func(args) + { + var palette_list = PaletteObjectManager.getScenePaletteList(); + var palette = palette_list.addPaletteAtLocation( + PaletteObjectManager.Constants.Location.SCENE, 0, args[0] + ); + for(var i=0; i < palette_list.numPalettes; ++i) + { + palette_list.movePaletteUp(palette.id); + } + return palette.id; + } + func + """ + harmony.send({"function": func, "args": [name]}) + + return name + + def remove(self, container): + # Replace any palettes with same name. + func = """function func(args) + { + var pom = PaletteObjectManager; + var palette_list = pom.getScenePaletteList(); + for(var i=0; i < palette_list.numPalettes; ++i) + { + var palette = palette_list.getPaletteByIndex(i); + if(palette.getName() == args[0]) + pom.removePaletteReferencesAndDeleteOnDisk(palette.id); + } + } + func + """ + harmony.send({"function": func, "args": [container["name"]]}) + + harmony.remove(container["name"]) + + harmony.save_scene() + + def switch(self, container, representation): + self.update(container, representation) + + def update(self, container, representation): + self.remove(container) + name = self.load_palette(representation) + + container["representation"] = str(representation["_id"]) + container["name"] = name + harmony.imprint(name, container) diff --git a/pype/plugins/harmony/publish/collect_palettes.py b/pype/plugins/harmony/publish/collect_palettes.py new file mode 100644 index 0000000000..2a2c1066c0 --- /dev/null +++ b/pype/plugins/harmony/publish/collect_palettes.py @@ -0,0 +1,45 @@ +import os +import json + +import pyblish.api +from avalon import harmony + + +class CollectPalettes(pyblish.api.ContextPlugin): + """Gather palettes from scene when publishing templates.""" + + label = "Palettes" + order = pyblish.api.CollectorOrder + hosts = ["harmony"] + + def process(self, context): + func = """function func() + { + var palette_list = PaletteObjectManager.getScenePaletteList(); + + var palettes = {}; + for(var i=0; i < palette_list.numPalettes; ++i) + { + var palette = palette_list.getPaletteByIndex(i); + palettes[palette.getName()] = palette.id; + } + + return palettes; + } + func + """ + palettes = harmony.send({"function": func})["result"] + + for name, id in palettes.items(): + instance = context.create_instance(name) + instance.data.update({ + "id": id, + "family": "harmony.palette", + "asset": os.environ["AVALON_ASSET"], + "subset": "palette" + name + }) + self.log.info( + "Created instance:\n" + json.dumps( + instance.data, sort_keys=True, indent=4 + ) + ) diff --git a/pype/plugins/harmony/publish/extract_palette.py b/pype/plugins/harmony/publish/extract_palette.py new file mode 100644 index 0000000000..9bca005278 --- /dev/null +++ b/pype/plugins/harmony/publish/extract_palette.py @@ -0,0 +1,34 @@ +import os + +from avalon import harmony +import pype.api +import pype.hosts.harmony + + +class ExtractPalette(pype.api.Extractor): + """Extract palette.""" + + label = "Extract Palette" + hosts = ["harmony"] + families = ["harmony.palette"] + + def process(self, instance): + func = """function func(args) + { + var palette_list = PaletteObjectManager.getScenePaletteList(); + var palette = palette_list.getPaletteById(args[0]); + return (palette.getPath() + "/" + palette.getName() + ".plt"); + } + func + """ + palette_file = harmony.send( + {"function": func, "args": [instance.data["id"]]} + )["result"] + + representation = { + "name": "plt", + "ext": "plt", + "files": os.path.basename(palette_file), + "stagingDir": os.path.dirname(palette_file) + } + instance.data["representations"] = [representation] From 1a15dc78ce8dc41a8bd886e03bab29b91b73cbfe Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 28 Jun 2020 16:23:05 +0100 Subject: [PATCH 050/190] Convert imageseuqnece loader --- pype/hosts/harmony/__init__.py | 4 +++- pype/plugins/harmony/load/load_imagesequence.py | 12 +++++++----- pype/plugins/harmony/load/load_palette.py | 2 -- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index 4dc06cdf84..3345c3134a 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -124,7 +124,9 @@ def check_inventory(): outdated_nodes = [] for container in outdated_containers: if container["loader"] == "ImageSequenceLoader": - outdated_nodes.append(container["name"]) + outdated_nodes.append( + harmony.find_node_by_name(container["name"]), "READ" + ) harmony.send({"function": func, "args": [outdated_nodes]}) # Warn about outdated containers. diff --git a/pype/plugins/harmony/load/load_imagesequence.py b/pype/plugins/harmony/load/load_imagesequence.py index 615188572e..f81018d0fb 100644 --- a/pype/plugins/harmony/load/load_imagesequence.py +++ b/pype/plugins/harmony/load/load_imagesequence.py @@ -1,4 +1,5 @@ import os +import uuid import clique @@ -252,15 +253,15 @@ class ImageSequenceLoader(api.Loader): ).replace("\\", "/") ) + name = context["subset"]["name"] + name += "_{}".format(uuid.uuid4()) read_node = harmony.send( { "function": copy_files + import_files, - "args": ["Top", files, context["subset"]["name"], 1] + "args": ["Top", files, name, 1] } )["result"] - self[:] = [read_node] - return harmony.containerise( name, namespace, @@ -270,7 +271,7 @@ class ImageSequenceLoader(api.Loader): ) def update(self, container, representation): - node = container.pop("node") + node = harmony.find_node_by_name(container["name"], "READ") path = api.get_representation_path(representation) collections, remainder = clique.assemble( @@ -324,7 +325,8 @@ class ImageSequenceLoader(api.Loader): ) def remove(self, container): - node = container.pop("node") + node = harmony.find_node_by_name(container["name"], "READ") + func = """function deleteNode(_node) { node.deleteNode(_node, true, true); diff --git a/pype/plugins/harmony/load/load_palette.py b/pype/plugins/harmony/load/load_palette.py index cfb88ac841..44aaf76aa5 100644 --- a/pype/plugins/harmony/load/load_palette.py +++ b/pype/plugins/harmony/load/load_palette.py @@ -2,8 +2,6 @@ import os import shutil import uuid -from bson.objectid import ObjectId - from avalon import api, harmony From 4ccc052f83dae3b346a7e0a0c8ccca05c4d9a9bd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 28 Jun 2020 17:16:17 +0100 Subject: [PATCH 051/190] Fix check inventory --- pype/hosts/harmony/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index 3345c3134a..3cae695852 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -125,7 +125,7 @@ def check_inventory(): for container in outdated_containers: if container["loader"] == "ImageSequenceLoader": outdated_nodes.append( - harmony.find_node_by_name(container["name"]), "READ" + harmony.find_node_by_name(container["name"], "READ") ) harmony.send({"function": func, "args": [outdated_nodes]}) From 17546da1a2ecc86fc228b17e34b7d898299d6d0a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 28 Jun 2020 17:34:05 +0100 Subject: [PATCH 052/190] Ftrack timeout needs to look at AVALON_TIMEOUT --- pype/modules/ftrack/lib/custom_db_connector.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/modules/ftrack/lib/custom_db_connector.py b/pype/modules/ftrack/lib/custom_db_connector.py index a734b3f80a..f8a188466c 100644 --- a/pype/modules/ftrack/lib/custom_db_connector.py +++ b/pype/modules/ftrack/lib/custom_db_connector.py @@ -9,6 +9,7 @@ import time import logging import functools import atexit +import os # Third-party dependencies import pymongo @@ -62,7 +63,7 @@ def check_active_table(func): class DbConnector: log = logging.getLogger(__name__) - timeout = 1000 + timeout = int(os.environ["AVALON_TIMEOUT"]) def __init__( self, uri, port=None, database_name=None, table_name=None From 1fffb6fcf48e6b3b43f9522b4402bf4516392816 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 29 Jun 2020 09:36:12 +0100 Subject: [PATCH 053/190] Make sure FFMPEG path is correct. --- pype/plugins/photoshop/publish/extract_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/photoshop/publish/extract_review.py b/pype/plugins/photoshop/publish/extract_review.py index 607e039d14..796e97600c 100644 --- a/pype/plugins/photoshop/publish/extract_review.py +++ b/pype/plugins/photoshop/publish/extract_review.py @@ -59,7 +59,7 @@ class ExtractReview(pype.api.Extractor): "-vframes", "1", thumbnail_path ] - output = pype.lib._subprocess(args) + output = pype.lib._subprocess(args, cwd=os.environ["FFMPEG_PATH"]) self.log.debug(output) @@ -79,7 +79,7 @@ class ExtractReview(pype.api.Extractor): "-vframes", "1", mov_path ] - output = pype.lib._subprocess(args) + output = pype.lib._subprocess(args, cwd=os.environ["FFMPEG_PATH"]) self.log.debug(output) From 3c07d57e1c70f2e8d2fd3219c988d0963fe80264 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 29 Jun 2020 09:36:25 +0100 Subject: [PATCH 054/190] Only publish 1 frame for review. --- pype/plugins/photoshop/publish/extract_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/photoshop/publish/extract_review.py b/pype/plugins/photoshop/publish/extract_review.py index 796e97600c..49e932eb67 100644 --- a/pype/plugins/photoshop/publish/extract_review.py +++ b/pype/plugins/photoshop/publish/extract_review.py @@ -89,7 +89,7 @@ class ExtractReview(pype.api.Extractor): "files": os.path.basename(mov_path), "stagingDir": staging_dir, "frameStart": 1, - "frameEnd": 2, + "frameEnd": 1, "fps": 25, "preview": True, "tags": ["review", "ftrackreview"] @@ -97,7 +97,7 @@ class ExtractReview(pype.api.Extractor): # Required for extract_review plugin (L222 onwards). instance.data["frameStart"] = 1 - instance.data["frameEnd"] = 2 + instance.data["frameEnd"] = 1 instance.data["fps"] = 25 self.log.info(f"Extracted {instance} to {staging_dir}") From 8de72755c478b71f14f25529c1b52d4324d1d464 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 29 Jun 2020 17:21:54 +0100 Subject: [PATCH 055/190] Layout support for namespaces --- pype/hosts/blender/plugin.py | 30 ++++++- pype/plugins/blender/load/load_layout.py | 105 ++++++++++++----------- pype/plugins/blender/load/load_model.py | 51 +++++++---- pype/plugins/blender/load/load_rig.py | 66 ++++++++------ 4 files changed, 156 insertions(+), 96 deletions(-) diff --git a/pype/hosts/blender/plugin.py b/pype/hosts/blender/plugin.py index 5a2596c9d8..eff41956e8 100644 --- a/pype/hosts/blender/plugin.py +++ b/pype/hosts/blender/plugin.py @@ -24,13 +24,17 @@ def asset_namespace( asset: str, subset: str ) -> str: """Return a unique namespace based on the asset name.""" - avalon_containers = bpy.data.collections.get( - blender.pipeline.AVALON_CONTAINERS - ) + avalon_containers = [ + c for c in bpy.data.collections + if c.name == 'AVALON_CONTAINERS' + ] + loaded_assets = [] + for c in avalon_containers: + loaded_assets.extend(c.children) if avalon_containers is None: return "1" collections_names = [ - c.name for c in avalon_containers.children + c.name for c in loaded_assets ] count = 1 name = f"{asset_name(asset, subset, str(count))}_CON" @@ -40,6 +44,12 @@ def asset_namespace( return str(count) +def prepare_data(data, container_name): + name = data.name + data = data.make_local() + data.name = f"{name}:{container_name}" + + def create_blender_context(active: Optional[bpy.types.Object] = None, selected: Optional[bpy.types.Object] = None,): """Create a new Blender context. If an object is passed as @@ -67,6 +77,18 @@ def create_blender_context(active: Optional[bpy.types.Object] = None, raise Exception("Could not create a custom Blender context.") +def get_parent_collection(collection): + """Get the parent of the input collection""" + check_list = [bpy.context.scene.collection] + + for c in check_list: + if collection.name in c.children.keys(): + return c + check_list.extend(c.children) + + return None + + class AssetLoader(api.Loader): """A basic AssetLoader for Blender diff --git a/pype/plugins/blender/load/load_layout.py b/pype/plugins/blender/load/load_layout.py index 0c1032c4fb..5d3f7d92cf 100644 --- a/pype/plugins/blender/load/load_layout.py +++ b/pype/plugins/blender/load/load_layout.py @@ -7,20 +7,14 @@ from typing import Dict, List, Optional from avalon import api, blender import bpy -import pype.hosts.blender.plugin - +import pype.hosts.blender.plugin as plugin logger = logging.getLogger("pype").getChild( "blender").getChild("load_layout") -class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): - """Load animations from a .blend file. - - Warning: - Loading the same asset more then once is not properly supported at the - moment. - """ +class BlendLayoutLoader(plugin.AssetLoader): + """Load layout from a .blend file.""" families = ["layout"] representations = ["blend"] @@ -29,24 +23,25 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): icon = "code-fork" color = "orange" - def _remove(self, objects, lib_container): - + def _remove(self, objects, obj_container): for obj in objects: - if obj.type == 'ARMATURE': bpy.data.armatures.remove(obj.data) elif obj.type == 'MESH': bpy.data.meshes.remove(obj.data) + elif obj.type == 'CAMERA': + bpy.data.cameras.remove(obj.data) + elif obj.type == 'CURVE': + bpy.data.curves.remove(obj.data) - for element_container in bpy.data.collections[lib_container].children: + for element_container in obj_container.children: for child in element_container.children: bpy.data.collections.remove(child) bpy.data.collections.remove(element_container) - bpy.data.collections.remove(bpy.data.collections[lib_container]) + bpy.data.collections.remove(obj_container) def _process(self, libpath, lib_container, container_name, actions): - relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( libpath, link=True, relative=relative @@ -58,26 +53,38 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): scene.collection.children.link(bpy.data.collections[lib_container]) layout_container = scene.collection.children[lib_container].make_local() + layout_container.name = container_name - meshes = [] + objects_local_types = ['MESH', 'CAMERA', 'CURVE'] + + objects = [] armatures = [] - objects_list = [] + containers = list(layout_container.children) - for element_container in layout_container.children: - element_container.make_local() - meshes.extend([obj for obj in element_container.objects if obj.type == 'MESH']) - armatures.extend([obj for obj in element_container.objects if obj.type == 'ARMATURE']) - for child in element_container.children: - child.make_local() - meshes.extend(child.objects) + for container in layout_container.children: + if container.name == blender.pipeline.AVALON_CONTAINERS: + containers.remove(container) + + for container in containers: + container.make_local() + objects.extend([ + obj for obj in container.objects + if obj.type in objects_local_types + ]) + armatures.extend([ + obj for obj in container.objects + if obj.type == 'ARMATURE' + ]) + containers.extend(list(container.children)) # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. - for obj in meshes + armatures: - obj = obj.make_local() - obj.data.make_local() + for obj in objects + armatures: + obj.make_local() + if obj.data: + obj.data.make_local() if not obj.get(blender.pipeline.AVALON_PROPERTY): obj[blender.pipeline.AVALON_PROPERTY] = dict() @@ -85,18 +92,16 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) - action = actions.get( obj.name, None ) + action = actions.get(obj.name, None) if obj.type == 'ARMATURE' and action is not None: obj.animation_data.action = action - objects_list.append(obj) - layout_container.pop(blender.pipeline.AVALON_PROPERTY) bpy.ops.object.select_all(action='DESELECT') - return objects_list + return layout_container def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, @@ -113,13 +118,17 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): libpath = self.fname asset = context["asset"]["name"] subset = context["subset"]["name"] - lib_container = pype.hosts.blender.plugin.asset_name(asset, subset) - container_name = pype.hosts.blender.plugin.asset_name( + lib_container = plugin.asset_name( + asset, subset + ) + namespace = namespace or plugin.asset_namespace( + asset, subset + ) + container_name = plugin.asset_name( asset, subset, namespace ) container = bpy.data.collections.new(lib_container) - container.name = container_name blender.pipeline.containerise_existing( container, name, @@ -134,11 +143,13 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - objects_list = self._process( + obj_container = self._process( libpath, lib_container, container_name, {}) + container_metadata["obj_container"] = obj_container + # Save the list of objects in the metadata container - container_metadata["objects"] = objects_list + container_metadata["objects"] = obj_container.all_objects nodes = list(container.objects) nodes.append(container) @@ -157,7 +168,6 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): Warning: No nested collections are supported at the moment! """ - collection = bpy.data.collections.get( container["objectName"] ) @@ -189,8 +199,11 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) - collection_libpath = collection_metadata["libpath"] + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + obj_container = collection_metadata["obj_container"] + normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) @@ -206,24 +219,20 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): logger.info("Library already loaded, not updating...") return - objects = collection_metadata["objects"] - lib_container = collection_metadata["lib_container"] - actions = {} for obj in objects: - if obj.type == 'ARMATURE': - actions[obj.name] = obj.animation_data.action - self._remove(objects, lib_container) + self._remove(objects, obj_container) - objects_list = self._process( + obj_container = self._process( str(libpath), lib_container, collection.name, actions) # Save the list of objects in the metadata container - collection_metadata["objects"] = objects_list + collection_metadata["obj_container"] = obj_container + collection_metadata["objects"] = obj_container.all_objects collection_metadata["libpath"] = str(libpath) collection_metadata["representation"] = str(representation["_id"]) @@ -255,9 +264,9 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] - lib_container = collection_metadata["lib_container"] + obj_container = collection_metadata["obj_container"] - self._remove(objects, lib_container) + self._remove(objects, obj_container) bpy.data.collections.remove(collection) diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index 6f34b40e70..7b2ade4570 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -34,31 +34,32 @@ class BlendModelLoader(plugin.AssetLoader): bpy.data.collections.remove(container) - def prepare_data(self, data, container_name): - name = data.name - data = data.make_local() - data.name = f"{name}:{container_name}" - - def _process(self, libpath, lib_container, container_name): + def _process( + self, libpath, lib_container, container_name, + parent_collection + ): relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( libpath, link=True, relative=relative ) as (_, data_to): data_to.collections = [lib_container] - scene = bpy.context.scene + parent = parent_collection - scene.collection.children.link(bpy.data.collections[lib_container]) + if parent is None: + parent = bpy.context.scene.collection - model_container = scene.collection.children[lib_container].make_local() + parent.children.link(bpy.data.collections[lib_container]) + + model_container = parent.children[lib_container].make_local() model_container.name = container_name for obj in model_container.objects: - self.prepare_data(obj, container_name) - self.prepare_data(obj.data, container_name) + plugin.prepare_data(obj, container_name) + plugin.prepare_data(obj.data, container_name) for material_slot in obj.material_slots: - self.prepare_data(material_slot.material, container_name) + plugin.prepare_data(material_slot.material, container_name) if not obj.get(blender.pipeline.AVALON_PROPERTY): obj[blender.pipeline.AVALON_PROPERTY] = dict() @@ -114,7 +115,7 @@ class BlendModelLoader(plugin.AssetLoader): container_metadata["lib_container"] = lib_container obj_container = self._process( - libpath, lib_container, container_name) + libpath, lib_container, container_name, None) container_metadata["obj_container"] = obj_container @@ -169,9 +170,16 @@ class BlendModelLoader(plugin.AssetLoader): collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] - objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - obj_container = collection_metadata["obj_container"] + + obj_container = [ + c for c in bpy.data.collections + if (c.name == collection_metadata["obj_container"].name and + c.library is None) + ][0] + objects = obj_container.all_objects + + container_name = obj_container.name normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) @@ -188,10 +196,12 @@ class BlendModelLoader(plugin.AssetLoader): logger.info("Library already loaded, not updating...") return + parent = plugin.get_parent_collection(obj_container) + self._remove(objects, obj_container) obj_container = self._process( - str(libpath), lib_container, collection.name) + str(libpath), lib_container, container_name, parent) # Save the list of objects in the metadata container collection_metadata["obj_container"] = obj_container @@ -223,8 +233,13 @@ class BlendModelLoader(plugin.AssetLoader): collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) - objects = collection_metadata["objects"] - obj_container = collection_metadata["obj_container"] + + obj_container = [ + c for c in bpy.data.collections + if (c.name == collection_metadata["obj_container"].name and + c.library is None) + ][0] + objects = obj_container.all_objects self._remove(objects, obj_container) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 41343e9c3a..7ac8a72993 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -26,54 +26,54 @@ class BlendRigLoader(plugin.AssetLoader): icon = "code-fork" color = "orange" - def _remove(self, objects, lib_container): - + def _remove(self, objects, obj_container): for obj in objects: if obj.type == 'ARMATURE': bpy.data.armatures.remove(obj.data) elif obj.type == 'MESH': bpy.data.meshes.remove(obj.data) - for child in bpy.data.collections[lib_container].children: + for child in obj_container.children: bpy.data.collections.remove(child) - bpy.data.collections.remove(bpy.data.collections[lib_container]) + bpy.data.collections.remove(obj_container) - def prepare_data(self, data, container_name): - name = data.name - data = data.make_local() - data.name = f"{name}:{container_name}" - - def _process(self, libpath, lib_container, container_name, action): + def _process( + self, libpath, lib_container, container_name, + action, parent_collection + ): relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( libpath, link=True, relative=relative ) as (_, data_to): data_to.collections = [lib_container] - scene = bpy.context.scene + parent = parent_collection - scene.collection.children.link(bpy.data.collections[lib_container]) + if parent is None: + parent = bpy.context.scene.collection + + parent.children.link(bpy.data.collections[lib_container]) - rig_container = scene.collection.children[lib_container].make_local() + rig_container = parent.children[lib_container].make_local() rig_container.name = container_name meshes = [] armatures = [ - obj for obj in rig_container.objects if obj.type == 'ARMATURE'] - - objects_list = [] + obj for obj in rig_container.objects + if obj.type == 'ARMATURE' + ] for child in rig_container.children: - self.prepare_data(child, container_name) - meshes.extend( child.objects ) + plugin.prepare_data(child, container_name) + meshes.extend(child.objects) # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. for obj in meshes + armatures: - self.prepare_data(obj, container_name) - self.prepare_data(obj.data, container_name) + plugin.prepare_data(obj, container_name) + plugin.prepare_data(obj.data, container_name) if not obj.get(blender.pipeline.AVALON_PROPERTY): obj[blender.pipeline.AVALON_PROPERTY] = dict() @@ -131,7 +131,7 @@ class BlendRigLoader(plugin.AssetLoader): container_metadata["lib_container"] = lib_container obj_container = self._process( - libpath, lib_container, container_name, None) + libpath, lib_container, container_name, None, None) container_metadata["obj_container"] = obj_container @@ -186,9 +186,16 @@ class BlendRigLoader(plugin.AssetLoader): collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] - objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - obj_container = collection_metadata["obj_container"] + + obj_container = [ + c for c in bpy.data.collections + if (c.name == collection_metadata["obj_container"].name and + c.library is None) + ][0] + objects = obj_container.all_objects + + container_name = obj_container.name normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) @@ -211,10 +218,12 @@ class BlendRigLoader(plugin.AssetLoader): action = armatures[0].animation_data.action + parent = plugin.get_parent_collection(obj_container) + self._remove(objects, obj_container) obj_container = self._process( - str(libpath), lib_container, collection.name, action) + str(libpath), lib_container, container_name, action, parent) # Save the list of objects in the metadata container collection_metadata["obj_container"] = obj_container @@ -249,8 +258,13 @@ class BlendRigLoader(plugin.AssetLoader): collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) - objects = collection_metadata["objects"] - obj_container = collection_metadata["obj_container"] + + obj_container = [ + c for c in bpy.data.collections + if (c.name == collection_metadata["obj_container"].name and + c.library is None) + ][0] + objects = obj_container.all_objects self._remove(objects, obj_container) From 52e2d785c10a0fbf2271b654db58ea22a39f15e5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 30 Jun 2020 11:35:24 +0100 Subject: [PATCH 056/190] Small improvements and Pep8 compliance --- pype/hosts/blender/plugin.py | 11 ++++++--- pype/plugins/blender/load/load_layout.py | 15 +++++------- pype/plugins/blender/load/load_model.py | 24 +++++++------------ pype/plugins/blender/load/load_rig.py | 30 ++++++++++-------------- 4 files changed, 35 insertions(+), 45 deletions(-) diff --git a/pype/hosts/blender/plugin.py b/pype/hosts/blender/plugin.py index eff41956e8..b0420fddfc 100644 --- a/pype/hosts/blender/plugin.py +++ b/pype/hosts/blender/plugin.py @@ -31,10 +31,8 @@ def asset_namespace( loaded_assets = [] for c in avalon_containers: loaded_assets.extend(c.children) - if avalon_containers is None: - return "1" collections_names = [ - c.name for c in loaded_assets + c.name for c in loaded_assets ] count = 1 name = f"{asset_name(asset, subset, str(count))}_CON" @@ -89,6 +87,13 @@ def get_parent_collection(collection): return None +def get_local_collection_with_name(name): + for collection in bpy.data.collections: + if collection.name == name and collection.library is None: + return collection + return None + + class AssetLoader(api.Loader): """A basic AssetLoader for Blender diff --git a/pype/plugins/blender/load/load_layout.py b/pype/plugins/blender/load/load_layout.py index 5d3f7d92cf..e9e128bb7c 100644 --- a/pype/plugins/blender/load/load_layout.py +++ b/pype/plugins/blender/load/load_layout.py @@ -9,9 +9,6 @@ from avalon import api, blender import bpy import pype.hosts.blender.plugin as plugin -logger = logging.getLogger("pype").getChild( - "blender").getChild("load_layout") - class BlendLayoutLoader(plugin.AssetLoader): """Load layout from a .blend file.""" @@ -65,15 +62,15 @@ class BlendLayoutLoader(plugin.AssetLoader): for container in layout_container.children: if container.name == blender.pipeline.AVALON_CONTAINERS: containers.remove(container) - + for container in containers: container.make_local() objects.extend([ - obj for obj in container.objects + obj for obj in container.objects if obj.type in objects_local_types ]) armatures.extend([ - obj for obj in container.objects + obj for obj in container.objects if obj.type == 'ARMATURE' ]) containers.extend(list(container.children)) @@ -175,7 +172,7 @@ class BlendLayoutLoader(plugin.AssetLoader): libpath = Path(api.get_representation_path(representation)) extension = libpath.suffix.lower() - logger.info( + self.log.info( "Container: %s\nRepresentation: %s", pformat(container, indent=2), pformat(representation, indent=2), @@ -210,13 +207,13 @@ class BlendLayoutLoader(plugin.AssetLoader): normalized_libpath = ( str(Path(bpy.path.abspath(str(libpath))).resolve()) ) - logger.debug( + self.log.debug( "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", normalized_collection_libpath, normalized_libpath, ) if normalized_collection_libpath == normalized_libpath: - logger.info("Library already loaded, not updating...") + self.log.info("Library already loaded, not updating...") return actions = {} diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index 7b2ade4570..ad4b35eb03 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -9,8 +9,6 @@ from avalon import api, blender import bpy import pype.hosts.blender.plugin as plugin -logger = logging.getLogger("pype").getChild("blender").getChild("load_model") - class BlendModelLoader(plugin.AssetLoader): """Load models from a .blend file. @@ -145,7 +143,7 @@ class BlendModelLoader(plugin.AssetLoader): libpath = Path(api.get_representation_path(representation)) extension = libpath.suffix.lower() - logger.info( + self.log.info( "Container: %s\nRepresentation: %s", pformat(container, indent=2), pformat(representation, indent=2), @@ -172,11 +170,9 @@ class BlendModelLoader(plugin.AssetLoader): collection_libpath = collection_metadata["libpath"] lib_container = collection_metadata["lib_container"] - obj_container = [ - c for c in bpy.data.collections - if (c.name == collection_metadata["obj_container"].name and - c.library is None) - ][0] + obj_container = get_local_collection_with_name( + collection_metadata["obj_container"].name + ) objects = obj_container.all_objects container_name = obj_container.name @@ -187,13 +183,13 @@ class BlendModelLoader(plugin.AssetLoader): normalized_libpath = ( str(Path(bpy.path.abspath(str(libpath))).resolve()) ) - logger.debug( + self.log.debug( "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", normalized_collection_libpath, normalized_libpath, ) if normalized_collection_libpath == normalized_libpath: - logger.info("Library already loaded, not updating...") + self.log.info("Library already loaded, not updating...") return parent = plugin.get_parent_collection(obj_container) @@ -234,11 +230,9 @@ class BlendModelLoader(plugin.AssetLoader): collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) - obj_container = [ - c for c in bpy.data.collections - if (c.name == collection_metadata["obj_container"].name and - c.library is None) - ][0] + obj_container = get_local_collection_with_name( + collection_metadata["obj_container"].name + ) objects = obj_container.all_objects self._remove(objects, obj_container) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 7ac8a72993..860cae71ba 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -9,8 +9,6 @@ from avalon import api, blender import bpy import pype.hosts.blender.plugin as plugin -logger = logging.getLogger("pype").getChild("blender").getChild("load_rig") - class BlendRigLoader(plugin.AssetLoader): """Load rigs from a .blend file. @@ -39,7 +37,7 @@ class BlendRigLoader(plugin.AssetLoader): bpy.data.collections.remove(obj_container) def _process( - self, libpath, lib_container, container_name, + self, libpath, lib_container, container_name, action, parent_collection ): relative = bpy.context.preferences.filepaths.use_relative_paths @@ -52,7 +50,7 @@ class BlendRigLoader(plugin.AssetLoader): if parent is None: parent = bpy.context.scene.collection - + parent.children.link(bpy.data.collections[lib_container]) rig_container = parent.children[lib_container].make_local() @@ -60,7 +58,7 @@ class BlendRigLoader(plugin.AssetLoader): meshes = [] armatures = [ - obj for obj in rig_container.objects + obj for obj in rig_container.objects if obj.type == 'ARMATURE' ] @@ -161,7 +159,7 @@ class BlendRigLoader(plugin.AssetLoader): libpath = Path(api.get_representation_path(representation)) extension = libpath.suffix.lower() - logger.info( + self.log.info( "Container: %s\nRepresentation: %s", pformat(container, indent=2), pformat(representation, indent=2), @@ -188,11 +186,9 @@ class BlendRigLoader(plugin.AssetLoader): collection_libpath = collection_metadata["libpath"] lib_container = collection_metadata["lib_container"] - obj_container = [ - c for c in bpy.data.collections - if (c.name == collection_metadata["obj_container"].name and - c.library is None) - ][0] + obj_container = get_local_collection_with_name( + collection_metadata["obj_container"].name + ) objects = obj_container.all_objects container_name = obj_container.name @@ -203,13 +199,13 @@ class BlendRigLoader(plugin.AssetLoader): normalized_libpath = ( str(Path(bpy.path.abspath(str(libpath))).resolve()) ) - logger.debug( + self.log.debug( "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", normalized_collection_libpath, normalized_libpath, ) if normalized_collection_libpath == normalized_libpath: - logger.info("Library already loaded, not updating...") + self.log.info("Library already loaded, not updating...") return # Get the armature of the rig @@ -259,11 +255,9 @@ class BlendRigLoader(plugin.AssetLoader): collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) - obj_container = [ - c for c in bpy.data.collections - if (c.name == collection_metadata["obj_container"].name and - c.library is None) - ][0] + obj_container = get_local_collection_with_name( + collection_metadata["obj_container"].name + ) objects = obj_container.all_objects self._remove(objects, obj_container) From aa99cc14b8f341ed01c69a671d1d46051fd75d74 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 30 Jun 2020 11:42:27 +0100 Subject: [PATCH 057/190] Fix undefined function --- pype/plugins/blender/load/load_model.py | 4 ++-- pype/plugins/blender/load/load_rig.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index ad4b35eb03..0013ccb90a 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -170,7 +170,7 @@ class BlendModelLoader(plugin.AssetLoader): collection_libpath = collection_metadata["libpath"] lib_container = collection_metadata["lib_container"] - obj_container = get_local_collection_with_name( + obj_container = plugin.get_local_collection_with_name( collection_metadata["obj_container"].name ) objects = obj_container.all_objects @@ -230,7 +230,7 @@ class BlendModelLoader(plugin.AssetLoader): collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) - obj_container = get_local_collection_with_name( + obj_container = plugin.get_local_collection_with_name( collection_metadata["obj_container"].name ) objects = obj_container.all_objects diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 860cae71ba..d9e4495090 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -186,7 +186,7 @@ class BlendRigLoader(plugin.AssetLoader): collection_libpath = collection_metadata["libpath"] lib_container = collection_metadata["lib_container"] - obj_container = get_local_collection_with_name( + obj_container = plugin.get_local_collection_with_name( collection_metadata["obj_container"].name ) objects = obj_container.all_objects @@ -255,7 +255,7 @@ class BlendRigLoader(plugin.AssetLoader): collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) - obj_container = get_local_collection_with_name( + obj_container = plugin.get_local_collection_with_name( collection_metadata["obj_container"].name ) objects = obj_container.all_objects From 87715afcb80061157a25edfa4913c7dcf398d374 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 30 Jun 2020 14:45:09 +0100 Subject: [PATCH 058/190] Increase socket thread timeout. --- pype/modules/ftrack/ftrack_server/socket_thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/ftrack/ftrack_server/socket_thread.py b/pype/modules/ftrack/ftrack_server/socket_thread.py index dda4c7db35..3602f2f138 100644 --- a/pype/modules/ftrack/ftrack_server/socket_thread.py +++ b/pype/modules/ftrack/ftrack_server/socket_thread.py @@ -11,7 +11,7 @@ from pype.api import Logger class SocketThread(threading.Thread): """Thread that checks suprocess of storer of processor of events""" - MAX_TIMEOUT = 35 + MAX_TIMEOUT = 120 def __init__(self, name, port, filepath, additional_args=[]): super(SocketThread, self).__init__() From 1ebc6d351848f60895c8e1175851b099aa758e70 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 30 Jun 2020 16:34:20 +0100 Subject: [PATCH 059/190] Set start and end frames when opening file --- pype/hosts/blender/__init__.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/pype/hosts/blender/__init__.py b/pype/hosts/blender/__init__.py index a6d3cd82ef..52cc53f05c 100644 --- a/pype/hosts/blender/__init__.py +++ b/pype/hosts/blender/__init__.py @@ -2,9 +2,11 @@ import os import sys import traceback -from avalon import api as avalon +from avalon import api as avalon, pipeline, blender from pyblish import api as pyblish +import bpy + from pype import PLUGINS_DIR PUBLISH_PATH = os.path.join(PLUGINS_DIR, "blender", "publish") @@ -25,6 +27,8 @@ def install(): avalon.register_plugin_path(avalon.Loader, str(LOAD_PATH)) avalon.register_plugin_path(avalon.Creator, str(CREATE_PATH)) + avalon.on("open", on_open) + def uninstall(): """Uninstall Blender configuration for Avalon.""" @@ -32,3 +36,17 @@ def uninstall(): pyblish.deregister_plugin_path(str(PUBLISH_PATH)) avalon.deregister_plugin_path(avalon.Loader, str(LOAD_PATH)) avalon.deregister_plugin_path(avalon.Creator, str(CREATE_PATH)) + + +def on_open(arg1, arg2): + + from avalon import io + + asset_name = io.Session["AVALON_ASSET"] + asset_doc = io.find_one({ + "type": "asset", + "name": asset_name + }) + + bpy.context.scene.frame_start = asset_doc["data"]["frameStart"] + bpy.context.scene.frame_end = asset_doc["data"]["frameEnd"] From af8bcfafaadfa6139aec6a2b5f49284875714d13 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Jul 2020 10:33:03 +0100 Subject: [PATCH 060/190] Only overwrite the .plt file and warn users about visual changes. --- pype/plugins/harmony/load/load_palette.py | 47 ++++++----------------- 1 file changed, 11 insertions(+), 36 deletions(-) diff --git a/pype/plugins/harmony/load/load_palette.py b/pype/plugins/harmony/load/load_palette.py index 44aaf76aa5..001758d5a8 100644 --- a/pype/plugins/harmony/load/load_palette.py +++ b/pype/plugins/harmony/load/load_palette.py @@ -1,8 +1,8 @@ import os import shutil -import uuid from avalon import api, harmony +from avalon.vendor import Qt class ImportPaletteLoader(api.Loader): @@ -26,9 +26,8 @@ class ImportPaletteLoader(api.Loader): def load_palette(self, representation): subset_name = representation["context"]["subset"] name = subset_name.replace("palette", "") - name += "_{}".format(uuid.uuid4()) - # Import new palette. + # Overwrite palette on disk. scene_path = harmony.send( {"function": "scene.currentProjectPath"} )["result"] @@ -40,45 +39,21 @@ class ImportPaletteLoader(api.Loader): ) shutil.copy(src, dst) - func = """function func(args) - { - var palette_list = PaletteObjectManager.getScenePaletteList(); - var palette = palette_list.addPaletteAtLocation( - PaletteObjectManager.Constants.Location.SCENE, 0, args[0] - ); - for(var i=0; i < palette_list.numPalettes; ++i) - { - palette_list.movePaletteUp(palette.id); - } - return palette.id; - } - func - """ - harmony.send({"function": func, "args": [name]}) + harmony.save_scene() + + # Dont allow instances with the same name. + message_box = Qt.QtWidgets.QMessageBox() + message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning) + msg = "Updated {}.".format(subset_name) + msg += " You need to reload the scene to see the changes." + message_box.setText(msg) + message_box.exec_() return name def remove(self, container): - # Replace any palettes with same name. - func = """function func(args) - { - var pom = PaletteObjectManager; - var palette_list = pom.getScenePaletteList(); - for(var i=0; i < palette_list.numPalettes; ++i) - { - var palette = palette_list.getPaletteByIndex(i); - if(palette.getName() == args[0]) - pom.removePaletteReferencesAndDeleteOnDisk(palette.id); - } - } - func - """ - harmony.send({"function": func, "args": [container["name"]]}) - harmony.remove(container["name"]) - harmony.save_scene() - def switch(self, container, representation): self.update(container, representation) From 138131d7e0e35699090ad613e110e4effe0c3d2f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 1 Jul 2020 11:09:34 +0100 Subject: [PATCH 061/190] Set start and end frames when opening Blender --- pype/hosts/blender/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pype/hosts/blender/__init__.py b/pype/hosts/blender/__init__.py index 52cc53f05c..498796a36a 100644 --- a/pype/hosts/blender/__init__.py +++ b/pype/hosts/blender/__init__.py @@ -27,6 +27,7 @@ def install(): avalon.register_plugin_path(avalon.Loader, str(LOAD_PATH)) avalon.register_plugin_path(avalon.Creator, str(CREATE_PATH)) + avalon.on("new", on_new) avalon.on("open", on_open) @@ -38,8 +39,7 @@ def uninstall(): avalon.deregister_plugin_path(avalon.Creator, str(CREATE_PATH)) -def on_open(arg1, arg2): - +def set_start_end_frames(): from avalon import io asset_name = io.Session["AVALON_ASSET"] @@ -50,3 +50,11 @@ def on_open(arg1, arg2): bpy.context.scene.frame_start = asset_doc["data"]["frameStart"] bpy.context.scene.frame_end = asset_doc["data"]["frameEnd"] + + +def on_new(arg1, arg2): + set_start_end_frames() + + +def on_open(arg1, arg2): + set_start_end_frames() From 034e4237fe392f040b10c4c7c83957d3439b77a0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Jul 2020 16:11:00 +0200 Subject: [PATCH 062/190] removed duplicated function --- pype/modules/ftrack/lib/custom_db_connector.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pype/modules/ftrack/lib/custom_db_connector.py b/pype/modules/ftrack/lib/custom_db_connector.py index a734b3f80a..a55c0b15dd 100644 --- a/pype/modules/ftrack/lib/custom_db_connector.py +++ b/pype/modules/ftrack/lib/custom_db_connector.py @@ -49,17 +49,6 @@ def check_active_table(func): return decorated -def check_active_table(func): - """Handling auto reconnect in 3 retry times""" - @functools.wraps(func) - def decorated(obj, *args, **kwargs): - if not obj.active_table: - raise NotActiveTable("Active table is not set. (This is bug)") - return func(obj, *args, **kwargs) - - return decorated - - class DbConnector: log = logging.getLogger(__name__) timeout = 1000 From 4f0f943d45e44c894dcd01050058e4907caf1832 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Jul 2020 16:11:50 +0200 Subject: [PATCH 063/190] make `database_name` as required argument in CustomDbConnector --- pype/modules/ftrack/lib/custom_db_connector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/ftrack/lib/custom_db_connector.py b/pype/modules/ftrack/lib/custom_db_connector.py index a55c0b15dd..60469159df 100644 --- a/pype/modules/ftrack/lib/custom_db_connector.py +++ b/pype/modules/ftrack/lib/custom_db_connector.py @@ -54,7 +54,7 @@ class DbConnector: timeout = 1000 def __init__( - self, uri, port=None, database_name=None, table_name=None + self, uri, database_name, port=None, table_name=None ): self._mongo_client = None self._sentry_client = None From 748da390a5c3360d0048f2b3c2619cd6cc381334 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Jul 2020 16:12:21 +0200 Subject: [PATCH 064/190] removed check of database_name in components --- pype/modules/ftrack/lib/custom_db_connector.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pype/modules/ftrack/lib/custom_db_connector.py b/pype/modules/ftrack/lib/custom_db_connector.py index 60469159df..05dfc0febb 100644 --- a/pype/modules/ftrack/lib/custom_db_connector.py +++ b/pype/modules/ftrack/lib/custom_db_connector.py @@ -67,9 +67,6 @@ class DbConnector: if port is None: port = components.get("port") - if database_name is None: - database_name = components.get("database") - if database_name is None: raise ValueError( "Database is not defined for connection. {}".format(uri) From a7196bce9e7773a411f89ad86bc8ddc11dccb52f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Jul 2020 16:16:10 +0200 Subject: [PATCH 065/190] renamed class `DbConnector` to `CustomDbConnector` in `custom_db_connector` and fixed argument order --- pype/modules/ftrack/ftrack_server/lib.py | 6 +++--- pype/modules/ftrack/ftrack_server/sub_event_storer.py | 4 ++-- pype/modules/ftrack/lib/custom_db_connector.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pype/modules/ftrack/ftrack_server/lib.py b/pype/modules/ftrack/ftrack_server/lib.py index 8377187ebe..bd02daebb1 100644 --- a/pype/modules/ftrack/ftrack_server/lib.py +++ b/pype/modules/ftrack/ftrack_server/lib.py @@ -26,7 +26,7 @@ from pype.api import ( compose_url ) -from pype.modules.ftrack.lib.custom_db_connector import DbConnector +from pype.modules.ftrack.lib.custom_db_connector import CustomDbConnector TOPIC_STATUS_SERVER = "pype.event.server.status" @@ -166,10 +166,10 @@ class ProcessEventHub(SocketBaseEventHub): pypelog = Logger().get_logger("Session Processor") def __init__(self, *args, **kwargs): - self.dbcon = DbConnector( + self.dbcon = CustomDbConnector( self.uri, - self.port, self.database, + self.port, self.table_name ) super(ProcessEventHub, self).__init__(*args, **kwargs) diff --git a/pype/modules/ftrack/ftrack_server/sub_event_storer.py b/pype/modules/ftrack/ftrack_server/sub_event_storer.py index 61b9aaf2c8..1635f6cea3 100644 --- a/pype/modules/ftrack/ftrack_server/sub_event_storer.py +++ b/pype/modules/ftrack/ftrack_server/sub_event_storer.py @@ -12,7 +12,7 @@ from pype.modules.ftrack.ftrack_server.lib import ( get_ftrack_event_mongo_info, TOPIC_STATUS_SERVER, TOPIC_STATUS_SERVER_RESULT ) -from pype.modules.ftrack.lib.custom_db_connector import DbConnector +from pype.modules.ftrack.lib.custom_db_connector import CustomDbConnector from pype.api import Logger log = Logger().get_logger("Event storer") @@ -24,7 +24,7 @@ class SessionFactory: uri, port, database, table_name = get_ftrack_event_mongo_info() -dbcon = DbConnector(uri, port, database, table_name) +dbcon = CustomDbConnector(uri, database, port, table_name) # ignore_topics = ["ftrack.meta.connected"] ignore_topics = [] diff --git a/pype/modules/ftrack/lib/custom_db_connector.py b/pype/modules/ftrack/lib/custom_db_connector.py index 05dfc0febb..a93eaeb08b 100644 --- a/pype/modules/ftrack/lib/custom_db_connector.py +++ b/pype/modules/ftrack/lib/custom_db_connector.py @@ -40,7 +40,7 @@ def auto_reconnect(func): def check_active_table(func): - """Check if DbConnector has active table before db method is called""" + """Check if CustomDbConnector has active collection.""" @functools.wraps(func) def decorated(obj, *args, **kwargs): if not obj.active_table: @@ -49,7 +49,7 @@ def check_active_table(func): return decorated -class DbConnector: +class CustomDbConnector: log = logging.getLogger(__name__) timeout = 1000 @@ -85,7 +85,7 @@ class DbConnector: # not all methods of PyMongo database are implemented with this it is # possible to use them too try: - return super(DbConnector, self).__getattribute__(attr) + return super(CustomDbConnector, self).__getattribute__(attr) except AttributeError: if self.active_table is None: raise NotActiveTable() From 7dba84dea1ad8754437d8de9b4f93549f4781cd6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Jul 2020 16:19:24 +0200 Subject: [PATCH 066/190] do not compose mongo url for ftrack events with database name and collection --- pype/modules/ftrack/ftrack_server/lib.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pype/modules/ftrack/ftrack_server/lib.py b/pype/modules/ftrack/ftrack_server/lib.py index bd02daebb1..acf31ab437 100644 --- a/pype/modules/ftrack/ftrack_server/lib.py +++ b/pype/modules/ftrack/ftrack_server/lib.py @@ -44,15 +44,8 @@ def get_ftrack_event_mongo_info(): mongo_url = os.environ.get("FTRACK_EVENTS_MONGO_URL") if mongo_url is not None: components = decompose_url(mongo_url) - _used_ftrack_url = True else: components = get_default_components() - _used_ftrack_url = False - - if not _used_ftrack_url or components["database"] is None: - components["database"] = database_name - - components.pop("collection", None) uri = compose_url(**components) From e7f284050b8b8b1297c9b0acf23f1dbd3cacd164 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Jul 2020 17:41:09 +0200 Subject: [PATCH 067/190] IdleManager is not QThread but threading.Thread --- pype/modules/idle_manager/idle_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/modules/idle_manager/idle_manager.py b/pype/modules/idle_manager/idle_manager.py index cfcdfef78f..80776a0541 100644 --- a/pype/modules/idle_manager/idle_manager.py +++ b/pype/modules/idle_manager/idle_manager.py @@ -1,11 +1,12 @@ import time import collections from Qt import QtCore +import threading from pynput import mouse, keyboard from pype.api import Logger -class IdleManager(QtCore.QThread): +class IdleManager(threading.Thread): """ Measure user's idle time in seconds. Idle time resets on keyboard/mouse input. Is able to emit signals at specific time idle. From b28c7f36fb03011a757f06aac91c44d92d9011c9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Jul 2020 17:42:23 +0200 Subject: [PATCH 068/190] there are not registered Qt signals ut callbacks --- pype/modules/idle_manager/idle_manager.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/pype/modules/idle_manager/idle_manager.py b/pype/modules/idle_manager/idle_manager.py index 80776a0541..66f63fbace 100644 --- a/pype/modules/idle_manager/idle_manager.py +++ b/pype/modules/idle_manager/idle_manager.py @@ -11,14 +11,13 @@ class IdleManager(threading.Thread): Idle time resets on keyboard/mouse input. Is able to emit signals at specific time idle. """ - time_signals = collections.defaultdict(list) + time_callbacks = collections.defaultdict(list) idle_time = 0 signal_reset_timer = QtCore.Signal() def __init__(self): super(IdleManager, self).__init__() self.log = Logger().get_logger(self.__class__.__name__) - self.signal_reset_timer.connect(self._reset_time) self.qaction = None self.failed_icon = None self._is_running = False @@ -33,18 +32,18 @@ class IdleManager(threading.Thread): def tray_exit(self): self.stop() try: - self.time_signals = {} + self.time_callbacks = {} except Exception: pass - def add_time_signal(self, emit_time, signal): - """ If any module want to use IdleManager, need to use add_time_signal - :param emit_time: time when signal will be emitted - :type emit_time: int - :param signal: signal that will be emitted (without objects) - :type signal: QtCore.Signal + def add_time_callback(self, emit_time, callback): + """If any module want to use IdleManager, need to use this method. + + Args: + emit_time(int): Time when callback will be triggered. + callback(func): Callback that will be triggered. """ - self.time_signals[emit_time].append(signal) + self.time_callbacks[emit_time].append(callback) @property def is_running(self): From 3660ca7c10071d1244f6e41ff0d303ba4a2fb195 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Jul 2020 17:43:48 +0200 Subject: [PATCH 069/190] mouse and keyboard threads are not QThreads --- pype/modules/idle_manager/idle_manager.py | 63 +++++++---------------- 1 file changed, 18 insertions(+), 45 deletions(-) diff --git a/pype/modules/idle_manager/idle_manager.py b/pype/modules/idle_manager/idle_manager.py index 66f63fbace..f952434546 100644 --- a/pype/modules/idle_manager/idle_manager.py +++ b/pype/modules/idle_manager/idle_manager.py @@ -58,9 +58,9 @@ class IdleManager(threading.Thread): def run(self): self.log.info('IdleManager has started') self._is_running = True - thread_mouse = MouseThread(self.signal_reset_timer) + thread_mouse = MouseThread(self._reset_time) thread_mouse.start() - thread_keyboard = KeyboardThread(self.signal_reset_timer) + thread_keyboard = KeyboardThread(self._reset_time) thread_keyboard.start() try: while self.is_running: @@ -79,16 +79,14 @@ class IdleManager(threading.Thread): # Threads don't have their attrs when Qt application already finished try: - thread_mouse.signal_stop.emit() - thread_mouse.terminate() - thread_mouse.wait() + thread_mouse.stop() + thread_mouse.join() except AttributeError: pass try: - thread_keyboard.signal_stop.emit() - thread_keyboard.terminate() - thread_keyboard.wait() + thread_keyboard.stop() + thread_keyboard.join() except AttributeError: pass @@ -96,49 +94,24 @@ class IdleManager(threading.Thread): self.log.info('IdleManager has stopped') -class MouseThread(QtCore.QThread): - """Listens user's mouse movement - """ - signal_stop = QtCore.Signal() +class MouseThread(mouse.Listener): + """Listens user's mouse movement.""" - def __init__(self, signal): - super(MouseThread, self).__init__() - self.signal_stop.connect(self.stop) - self.m_listener = None - - self.signal_reset_timer = signal - - def stop(self): - if self.m_listener is not None: - self.m_listener.stop() + def __init__(self, callback): + super(MouseThread, self).__init__(on_move=self.on_move) + self.callback = callback def on_move(self, posx, posy): - self.signal_reset_timer.emit() - - def run(self): - self.m_listener = mouse.Listener(on_move=self.on_move) - self.m_listener.start() + self.callback() -class KeyboardThread(QtCore.QThread): - """Listens user's keyboard input - """ - signal_stop = QtCore.Signal() +class KeyboardThread(keyboard.Listener): + """Listens user's keyboard input.""" - def __init__(self, signal): - super(KeyboardThread, self).__init__() - self.signal_stop.connect(self.stop) - self.k_listener = None + def __init__(self, callback): + super(KeyboardThread, self).__init__(on_press=self.on_press) - self.signal_reset_timer = signal - - def stop(self): - if self.k_listener is not None: - self.k_listener.stop() + self.callback = callback def on_press(self, key): - self.signal_reset_timer.emit() - - def run(self): - self.k_listener = keyboard.Listener(on_press=self.on_press) - self.k_listener.start() + self.callback() From b15af67bdb96d6bc5bf14828f42503e1a5fe2c90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Jul 2020 17:44:49 +0200 Subject: [PATCH 070/190] removed rest of Qt from idle manager --- pype/modules/idle_manager/idle_manager.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pype/modules/idle_manager/idle_manager.py b/pype/modules/idle_manager/idle_manager.py index f952434546..15618d6076 100644 --- a/pype/modules/idle_manager/idle_manager.py +++ b/pype/modules/idle_manager/idle_manager.py @@ -1,6 +1,5 @@ import time import collections -from Qt import QtCore import threading from pynput import mouse, keyboard from pype.api import Logger @@ -13,7 +12,6 @@ class IdleManager(threading.Thread): """ time_callbacks = collections.defaultdict(list) idle_time = 0 - signal_reset_timer = QtCore.Signal() def __init__(self): super(IdleManager, self).__init__() From adb9749d63ef5c208e2f57aecd0022359b47d6ac Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Jul 2020 17:45:09 +0200 Subject: [PATCH 071/190] callback are executed in threads which are joined when done --- pype/modules/idle_manager/idle_manager.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pype/modules/idle_manager/idle_manager.py b/pype/modules/idle_manager/idle_manager.py index 15618d6076..3a9f9154a9 100644 --- a/pype/modules/idle_manager/idle_manager.py +++ b/pype/modules/idle_manager/idle_manager.py @@ -19,6 +19,7 @@ class IdleManager(threading.Thread): self.qaction = None self.failed_icon = None self._is_running = False + self.threads = [] def set_qaction(self, qaction, failed_icon): self.qaction = qaction @@ -62,11 +63,20 @@ class IdleManager(threading.Thread): thread_keyboard.start() try: while self.is_running: + if self.idle_time in self.time_callbacks: + for callback in self.time_callbacks[self.idle_time]: + thread = threading.Thread(target=callback) + thread.start() + self.threads.append(thread) + + for thread in tuple(self.threads): + if not thread.isAlive(): + thread.join() + self.threads.remove(thread) + self.idle_time += 1 - if self.idle_time in self.time_signals: - for signal in self.time_signals[self.idle_time]: - signal.emit() time.sleep(1) + except Exception: self.log.warning( 'Idle Manager service has failed', exc_info=True From 66190c01854d1b5cdce204b7b6e1dc7c8210f8bd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Jul 2020 17:45:52 +0200 Subject: [PATCH 072/190] SignalHandler moved from timers_manager to widget part --- pype/modules/timers_manager/timers_manager.py | 14 +------------- pype/modules/timers_manager/widget_user_idle.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pype/modules/timers_manager/timers_manager.py b/pype/modules/timers_manager/timers_manager.py index cec730d007..55bec8d963 100644 --- a/pype/modules/timers_manager/timers_manager.py +++ b/pype/modules/timers_manager/timers_manager.py @@ -1,5 +1,4 @@ -from Qt import QtCore -from .widget_user_idle import WidgetUserIdle +from .widget_user_idle import WidgetUserIdle, SignalHandler from pype.api import Logger, config @@ -174,14 +173,3 @@ class TimersManager(metaclass=Singleton): return if self.widget_user_idle.bool_is_showed is False: self.widget_user_idle.show() - - -class SignalHandler(QtCore.QObject): - signal_show_message = QtCore.Signal() - signal_change_label = QtCore.Signal() - signal_stop_timers = QtCore.Signal() - def __init__(self, cls): - super().__init__() - self.signal_show_message.connect(cls.show_message) - self.signal_change_label.connect(cls.change_label) - self.signal_stop_timers.connect(cls.stop_timers) diff --git a/pype/modules/timers_manager/widget_user_idle.py b/pype/modules/timers_manager/widget_user_idle.py index 697c0a04d9..f6f3c49357 100644 --- a/pype/modules/timers_manager/widget_user_idle.py +++ b/pype/modules/timers_manager/widget_user_idle.py @@ -154,3 +154,15 @@ class WidgetUserIdle(QtWidgets.QWidget): def showEvent(self, event): self.bool_is_showed = True + + +class SignalHandler(QtCore.QObject): + signal_show_message = QtCore.Signal() + signal_change_label = QtCore.Signal() + signal_stop_timers = QtCore.Signal() + + def __init__(self, cls): + super().__init__() + self.signal_show_message.connect(cls.show_message) + self.signal_change_label.connect(cls.change_label) + self.signal_stop_timers.connect(cls.stop_timers) From 14d8d3c6a88951957eaa29a39e3989533177d087 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Jul 2020 17:46:26 +0200 Subject: [PATCH 073/190] simplified user widget in timers manager --- pype/modules/timers_manager/widget_user_idle.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pype/modules/timers_manager/widget_user_idle.py b/pype/modules/timers_manager/widget_user_idle.py index f6f3c49357..22455846fd 100644 --- a/pype/modules/timers_manager/widget_user_idle.py +++ b/pype/modules/timers_manager/widget_user_idle.py @@ -1,4 +1,3 @@ -from pype.api import Logger from avalon import style from Qt import QtCore, QtGui, QtWidgets @@ -8,18 +7,18 @@ class WidgetUserIdle(QtWidgets.QWidget): SIZE_W = 300 SIZE_H = 160 - def __init__(self, parent): + def __init__(self, module, tray_widget): super(WidgetUserIdle, self).__init__() self.bool_is_showed = False self.bool_not_stopped = True - self.parent_widget = parent - self.setWindowIcon(parent.tray_widget.icon) + self.module = module + self.setWindowIcon(tray_widget.icon) self.setWindowFlags( - QtCore.Qt.WindowCloseButtonHint | - QtCore.Qt.WindowMinimizeButtonHint + QtCore.Qt.WindowCloseButtonHint + | QtCore.Qt.WindowMinimizeButtonHint ) self._translate = QtCore.QCoreApplication.translate @@ -129,11 +128,11 @@ class WidgetUserIdle(QtWidgets.QWidget): self.lbl_rest_time.setText(str_time) def stop_timer(self): - self.parent_widget.stop_timers() + self.module.stop_timers() self.close_widget() def restart_timer(self): - self.parent_widget.restart_timers() + self.module.restart_timers() self.close_widget() def continue_timer(self): From 31a0bfef1805f89add37c015f9a99791fd19384e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Jul 2020 17:46:57 +0200 Subject: [PATCH 074/190] timers manager does not register signals but callbacks in idle manager --- pype/modules/timers_manager/timers_manager.py | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/pype/modules/timers_manager/timers_manager.py b/pype/modules/timers_manager/timers_manager.py index 55bec8d963..8df7952baf 100644 --- a/pype/modules/timers_manager/timers_manager.py +++ b/pype/modules/timers_manager/timers_manager.py @@ -30,7 +30,10 @@ class TimersManager(metaclass=Singleton): self.log = Logger().get_logger(self.__class__.__name__) self.tray_widget = tray_widget self.main_widget = main_widget - self.widget_user_idle = WidgetUserIdle(self) + + self.idle_man = None + self.signal_handler = None + self.widget_user_idle = WidgetUserIdle(self, tray_widget) def set_signal_times(self): try: @@ -113,49 +116,59 @@ class TimersManager(metaclass=Singleton): :param modules: All imported modules from TrayManager :type modules: dict """ - self.s_handler = SignalHandler(self) if 'IdleManager' in modules: + self.signal_handler = SignalHandler(self) if self.set_signal_times() is True: self.register_to_idle_manager(modules['IdleManager']) + def time_callback(self, int_def): + if not self.signal_handler: + return + + if int_def == 0: + self.signal_handler.signal_show_message.emit() + elif int_def == 1: + self.signal_handler.signal_change_label.emit() + elif int_def == 2: + self.signal_handler.signal_stop_timers.emit() + def register_to_idle_manager(self, man_obj): self.idle_man = man_obj + + # Time when message is shown + self.idle_man.add_time_callback( + self.time_show_message, + lambda: self.time_callback(0) + ) + # Times when idle is between show widget and stop timers show_to_stop_range = range( - self.time_show_message-1, self.time_stop_timer + self.time_show_message - 1, self.time_stop_timer ) for num in show_to_stop_range: - self.idle_man.add_time_signal( - num, - self.s_handler.signal_change_label + self.idle_man.add_time_callback( + num, lambda: self.time_callback(1) ) # Times when widget is already shown and user restart idle shown_and_moved_range = range( self.time_stop_timer - self.time_show_message ) for num in shown_and_moved_range: - self.idle_man.add_time_signal( - num, - self.s_handler.signal_change_label + self.idle_man.add_time_callback( + num, lambda: self.time_callback(1) ) - # Time when message is shown - self.idle_man.add_time_signal( - self.time_show_message, - self.s_handler.signal_show_message - ) + # Time when timers are stopped - self.idle_man.add_time_signal( + self.idle_man.add_time_callback( self.time_stop_timer, - self.s_handler.signal_stop_timers + lambda: self.time_callback(2) ) def change_label(self): if self.is_running is False: return - if self.widget_user_idle.bool_is_showed is False: - return - if not hasattr(self, 'idle_man'): + if not self.idle_man or self.widget_user_idle.bool_is_showed is False: return if self.idle_man.idle_time > self.time_show_message: From 72dadc74e744d9ee3f6661507acb3d1218df56cd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Jul 2020 10:05:00 +0200 Subject: [PATCH 075/190] hierachical entities are filtered to avoid deletion of parent and then trying to delete it's children --- .../ftrack/actions/action_delete_asset.py | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/pype/modules/ftrack/actions/action_delete_asset.py b/pype/modules/ftrack/actions/action_delete_asset.py index 1074efee3b..f4f6378f1c 100644 --- a/pype/modules/ftrack/actions/action_delete_asset.py +++ b/pype/modules/ftrack/actions/action_delete_asset.py @@ -534,14 +534,9 @@ class DeleteAssetSubset(BaseAction): ftrack_proc_txt, ", ".join(ftrack_ids_to_delete) )) - joined_ids_to_delete = ", ".join( - ["\"{}\"".format(id) for id in ftrack_ids_to_delete] + ftrack_ents_to_delete = ( + self._filter_entities_to_delete(ftrack_ids_to_delete, session) ) - ftrack_ents_to_delete = self.session.query( - "select id, link from TypedContext where id in ({})".format( - joined_ids_to_delete - ) - ).all() for entity in ftrack_ents_to_delete: self.session.delete(entity) try: @@ -592,6 +587,40 @@ class DeleteAssetSubset(BaseAction): return self.report_handle(report_messages, project_name, event) + def _filter_entities_to_delete(self, ftrack_ids_to_delete, session): + """Filter children entities to avoid CircularDependencyError.""" + joined_ids_to_delete = ", ".join( + ["\"{}\"".format(id) for id in ftrack_ids_to_delete] + ) + to_delete_entities = session.query( + "select id, link from TypedContext where id in ({})".format( + joined_ids_to_delete + ) + ).all() + filtered = to_delete_entities[:] + while True: + changed = False + _filtered = filtered[:] + for entity in filtered: + entity_id = entity["id"] + + for _entity in tuple(_filtered): + if entity_id == _entity["id"]: + continue + + for _link in _entity["link"]: + if entity_id == _link["id"] and _entity in _filtered: + _filtered.remove(_entity) + changed = True + break + + filtered = _filtered + + if not changed: + break + + return filtered + def report_handle(self, report_messages, project_name, event): if not report_messages: return { From 900f07d69ec48aa19fc357ca82f53caca0d3a720 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Jul 2020 10:05:20 +0200 Subject: [PATCH 076/190] do not use self.session but session in code --- pype/modules/ftrack/actions/action_delete_asset.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pype/modules/ftrack/actions/action_delete_asset.py b/pype/modules/ftrack/actions/action_delete_asset.py index f4f6378f1c..d09cf73f5f 100644 --- a/pype/modules/ftrack/actions/action_delete_asset.py +++ b/pype/modules/ftrack/actions/action_delete_asset.py @@ -538,16 +538,16 @@ class DeleteAssetSubset(BaseAction): self._filter_entities_to_delete(ftrack_ids_to_delete, session) ) for entity in ftrack_ents_to_delete: - self.session.delete(entity) + session.delete(entity) try: - self.session.commit() + session.commit() except Exception: ent_path = "/".join( [ent["name"] for ent in entity["link"]] ) msg = "Failed to delete entity" report_messages[msg].append(ent_path) - self.session.rollback() + session.rollback() self.log.warning( "{} <{}>".format(msg, ent_path), exc_info=True @@ -563,7 +563,7 @@ class DeleteAssetSubset(BaseAction): for name in asset_names_to_delete ]) # Find assets of selected entities with names of checked subsets - assets = self.session.query(( + assets = session.query(( "select id from Asset where" " context_id in ({}) and name in ({})" ).format(joined_not_deleted, joined_asset_names)).all() @@ -573,11 +573,11 @@ class DeleteAssetSubset(BaseAction): ", ".join([asset["id"] for asset in assets]) )) for asset in assets: - self.session.delete(asset) + session.delete(asset) try: - self.session.commit() + session.commit() except Exception: - self.session.rollback() + session.rollback() msg = "Failed to delete asset" report_messages[msg].append(asset["id"]) self.log.warning( From 25a1a56dc41598ea9de057ef9734a56b643e9104 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Jul 2020 10:05:54 +0200 Subject: [PATCH 077/190] formatting changes --- pype/modules/ftrack/actions/action_delete_asset.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pype/modules/ftrack/actions/action_delete_asset.py b/pype/modules/ftrack/actions/action_delete_asset.py index d09cf73f5f..27394770e1 100644 --- a/pype/modules/ftrack/actions/action_delete_asset.py +++ b/pype/modules/ftrack/actions/action_delete_asset.py @@ -497,9 +497,8 @@ class DeleteAssetSubset(BaseAction): for entity in entities: ftrack_id = entity["id"] ftrack_id_name_map[ftrack_id] = entity["name"] - if ftrack_id in ftrack_ids_to_delete: - continue - not_deleted_entities_id.append(ftrack_id) + if ftrack_id not in ftrack_ids_to_delete: + not_deleted_entities_id.append(ftrack_id) mongo_proc_txt = "MongoProcessing: " ftrack_proc_txt = "Ftrack processing: " @@ -581,7 +580,7 @@ class DeleteAssetSubset(BaseAction): msg = "Failed to delete asset" report_messages[msg].append(asset["id"]) self.log.warning( - "{} <{}>".format(asset["id"]), + "Asset: {} <{}>".format(asset["name"], asset["id"]), exc_info=True ) From 2b58592d123695032887f2406fd0b285cfbc3f7e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 2 Jul 2020 10:32:38 +0100 Subject: [PATCH 078/190] Supporting double digits namespace names --- pype/hosts/blender/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hosts/blender/plugin.py b/pype/hosts/blender/plugin.py index b0420fddfc..f0791c8eb6 100644 --- a/pype/hosts/blender/plugin.py +++ b/pype/hosts/blender/plugin.py @@ -16,7 +16,7 @@ def asset_name( """Return a consistent name for an asset.""" name = f"{asset}_{subset}" if namespace: - name = f"{namespace}:{name}" + name = f"{namespace:0>2}:{name}" return name From 57f795f4c5f5a4b4f0b121d67fc9ddc5c444f243 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 2 Jul 2020 11:40:30 +0100 Subject: [PATCH 079/190] Support publishing a subset of shots Currently the sequence start frame is hardcoded to 10:00:00:00, so can be expanded later to ask the user for this information. EDLs do not have any data about the sequence start frame. --- .../standalonepublisher/publish/collect_shots.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_shots.py b/pype/plugins/standalonepublisher/publish/collect_shots.py index 853ba4e8de..4f682bd808 100644 --- a/pype/plugins/standalonepublisher/publish/collect_shots.py +++ b/pype/plugins/standalonepublisher/publish/collect_shots.py @@ -56,12 +56,18 @@ class CollectShots(pyblish.api.InstancePlugin): asset_entity = instance.context.data["assetEntity"] asset_name = asset_entity["name"] + # Ask user for sequence start. Usually 10:00:00:00. + sequence_start_frame = 900000 + # Project specific prefix naming. This needs to be replaced with some # options to be more flexible. asset_name = asset_name.split("_")[0] instances = [] for track in tracks: + track_start_frame = ( + abs(track.source_range.start_time.value) - sequence_start_frame + ) for child in track.each_child(): # Transitions are ignored, because Clips have the full frame @@ -69,12 +75,17 @@ class CollectShots(pyblish.api.InstancePlugin): if isinstance(child, otio.schema.transition.Transition): continue + if child.name is None: + continue + # Hardcoded to expect a shot name of "[name].[extension]" child_name = os.path.splitext(child.name)[0].lower() name = f"{asset_name}_{child_name}" - frame_start = child.range_in_parent().start_time.value - frame_end = child.range_in_parent().end_time_inclusive().value + frame_start = track_start_frame + frame_start += child.range_in_parent().start_time.value + frame_end = track_start_frame + frame_end += child.range_in_parent().end_time_inclusive().value label = f"{name} (framerange: {frame_start}-{frame_end})" instances.append( From 72b685fa9be53f71666b30fc34c23b56fff1771f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 2 Jul 2020 15:44:13 +0200 Subject: [PATCH 080/190] stop appending of tools env to existing env --- pype/modules/ftrack/lib/ftrack_app_handler.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/modules/ftrack/lib/ftrack_app_handler.py b/pype/modules/ftrack/lib/ftrack_app_handler.py index 00bd13fd73..531ef310e7 100644 --- a/pype/modules/ftrack/lib/ftrack_app_handler.py +++ b/pype/modules/ftrack/lib/ftrack_app_handler.py @@ -213,7 +213,6 @@ class AppAction(BaseAction): tools_env = acre.get_tools(tools_attr) env = acre.compute(tools_env) env = acre.merge(env, current_env=dict(prep_env)) - env = acre.append(dict(prep_env), env) # Get path to execute st_temp_path = os.environ["PYPE_CONFIG"] From 345601a79f9850ca83c47a890e1e5046fc7331af Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 2 Jul 2020 15:23:20 +0100 Subject: [PATCH 081/190] Changed namespace formulation --- pype/hosts/blender/plugin.py | 15 ++++++++------- pype/plugins/blender/load/load_layout.py | 6 ++++-- pype/plugins/blender/load/load_model.py | 6 ++++-- pype/plugins/blender/load/load_rig.py | 6 ++++-- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/pype/hosts/blender/plugin.py b/pype/hosts/blender/plugin.py index f0791c8eb6..33cccd7d5e 100644 --- a/pype/hosts/blender/plugin.py +++ b/pype/hosts/blender/plugin.py @@ -14,16 +14,17 @@ def asset_name( asset: str, subset: str, namespace: Optional[str] = None ) -> str: """Return a consistent name for an asset.""" - name = f"{asset}_{subset}" + name = f"{asset}" if namespace: - name = f"{namespace:0>2}:{name}" + name = f"{name}_{namespace}" + name = f"{name}_{subset}" return name -def asset_namespace( +def get_unique_number( asset: str, subset: str ) -> str: - """Return a unique namespace based on the asset name.""" + """Return a unique number based on the asset name.""" avalon_containers = [ c for c in bpy.data.collections if c.name == 'AVALON_CONTAINERS' @@ -35,11 +36,11 @@ def asset_namespace( c.name for c in loaded_assets ] count = 1 - name = f"{asset_name(asset, subset, str(count))}_CON" + name = f"{asset}_{count:0>2}_{subset}_CON" while name in collections_names: count += 1 - name = f"{asset_name(asset, subset, str(count))}_CON" - return str(count) + name = f"{asset}_{count:0>2}_{subset}_CON" + return f"{count:0>2}" def prepare_data(data, container_name): diff --git a/pype/plugins/blender/load/load_layout.py b/pype/plugins/blender/load/load_layout.py index e9e128bb7c..cfab5a207b 100644 --- a/pype/plugins/blender/load/load_layout.py +++ b/pype/plugins/blender/load/load_layout.py @@ -118,14 +118,16 @@ class BlendLayoutLoader(plugin.AssetLoader): lib_container = plugin.asset_name( asset, subset ) - namespace = namespace or plugin.asset_namespace( + unique_number = plugin.get_unique_number( asset, subset ) + namespace = namespace or f"{asset}_{unique_number}" container_name = plugin.asset_name( - asset, subset, namespace + asset, subset, unique_number ) container = bpy.data.collections.new(lib_container) + container.name = container_name blender.pipeline.containerise_existing( container, name, diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index 0013ccb90a..ad9137a15d 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -90,14 +90,16 @@ class BlendModelLoader(plugin.AssetLoader): lib_container = plugin.asset_name( asset, subset ) - namespace = namespace or plugin.asset_namespace( + unique_number = plugin.get_unique_number( asset, subset ) + namespace = namespace or f"{asset}_{unique_number}" container_name = plugin.asset_name( - asset, subset, namespace + asset, subset, unique_number ) container = bpy.data.collections.new(lib_container) + container.name = container_name blender.pipeline.containerise_existing( container, name, diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index d9e4495090..e09a9cb92f 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -106,14 +106,16 @@ class BlendRigLoader(plugin.AssetLoader): lib_container = plugin.asset_name( asset, subset ) - namespace = namespace or plugin.asset_namespace( + unique_number = plugin.get_unique_number( asset, subset ) + namespace = namespace or f"{asset}_{unique_number}" container_name = plugin.asset_name( - asset, subset, namespace + asset, subset, unique_number ) container = bpy.data.collections.new(lib_container) + container.name = container_name blender.pipeline.containerise_existing( container, name, From 875962ed9b2440272bd95775650f13bcccdc364b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 3 Jul 2020 09:27:43 +0100 Subject: [PATCH 082/190] Hound fixes --- pype/hosts/blender/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/hosts/blender/plugin.py b/pype/hosts/blender/plugin.py index 33cccd7d5e..ab53d49041 100644 --- a/pype/hosts/blender/plugin.py +++ b/pype/hosts/blender/plugin.py @@ -5,7 +5,7 @@ from typing import Dict, List, Optional import bpy -from avalon import api, blender +from avalon import api VALID_EXTENSIONS = [".blend"] @@ -26,7 +26,7 @@ def get_unique_number( ) -> str: """Return a unique number based on the asset name.""" avalon_containers = [ - c for c in bpy.data.collections + c for c in bpy.data.collections if c.name == 'AVALON_CONTAINERS' ] loaded_assets = [] From b38150173a22761cc5337977b37db2fca45864e6 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 3 Jul 2020 09:35:29 +0100 Subject: [PATCH 083/190] Hound fixes --- pype/hosts/blender/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hosts/blender/__init__.py b/pype/hosts/blender/__init__.py index 498796a36a..dafeca5107 100644 --- a/pype/hosts/blender/__init__.py +++ b/pype/hosts/blender/__init__.py @@ -2,7 +2,7 @@ import os import sys import traceback -from avalon import api as avalon, pipeline, blender +from avalon import api as avalon from pyblish import api as pyblish import bpy From 4636f64b739a91ac53683be627d5eb4838af7997 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 7 Jul 2020 16:07:24 +0100 Subject: [PATCH 084/190] If camera attributes are connected, we can ignore them. --- pype/plugins/maya/load/load_image_plane.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pype/plugins/maya/load/load_image_plane.py b/pype/plugins/maya/load/load_image_plane.py index e95ea6cd8f..653a8d4128 100644 --- a/pype/plugins/maya/load/load_image_plane.py +++ b/pype/plugins/maya/load/load_image_plane.py @@ -50,8 +50,11 @@ class ImagePlaneLoader(api.Loader): camera = selection[0] - camera.displayResolution.set(1) - camera.farClipPlane.set(image_plane_depth * 10) + try: + camera.displayResolution.set(1) + camera.farClipPlane.set(image_plane_depth * 10) + except RuntimeError: + pass # Create image plane image_plane_transform, image_plane_shape = pc.imagePlane( From 2815201d29be1ba15dd1afafab578da791d571b5 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 7 Jul 2020 21:42:34 +0100 Subject: [PATCH 085/190] Fix ValidateNukeWriteKnobs --- ...idate_write_knobs.py => validate_knobs.py} | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) rename pype/plugins/nuke/publish/{validate_write_knobs.py => validate_knobs.py} (61%) diff --git a/pype/plugins/nuke/publish/validate_write_knobs.py b/pype/plugins/nuke/publish/validate_knobs.py similarity index 61% rename from pype/plugins/nuke/publish/validate_write_knobs.py rename to pype/plugins/nuke/publish/validate_knobs.py index 24572bedb3..22f0d344c9 100644 --- a/pype/plugins/nuke/publish/validate_write_knobs.py +++ b/pype/plugins/nuke/publish/validate_knobs.py @@ -4,14 +4,14 @@ import pyblish.api import pype.api -class ValidateNukeWriteKnobs(pyblish.api.ContextPlugin): +class ValidateKnobs(pyblish.api.ContextPlugin): """Ensure knobs are consistent. Knobs to validate and their values comes from the Example for presets in config: "presets/plugins/nuke/publish.json" preset, which needs this structure: - "ValidateNukeWriteKnobs": { + "ValidateKnobs": { "enabled": true, "knobs": { "family": { @@ -22,22 +22,31 @@ class ValidateNukeWriteKnobs(pyblish.api.ContextPlugin): """ order = pyblish.api.ValidatorOrder - label = "Validate Write Knobs" + label = "Validate Knobs" hosts = ["nuke"] actions = [pype.api.RepairContextAction] optional = True def process(self, context): - # Check for preset existence. - if not getattr(self, "knobs"): + nuke_presets = context.data["presets"].get("nuke") + + if not nuke_presets: + return + + publish_presets = nuke_presets.get("publish") + + if not publish_presets: + return + + plugin_preset = publish_presets.get("ValidateKnobs") + + if not plugin_preset: return - - self.log.debug("__ self.knobs: {}".format(self.knobs)) invalid = self.get_invalid(context, compute=True) if invalid: raise RuntimeError( - "Found knobs with invalid values: {}".format(invalid) + "Found knobs with invalid values:\n{}".format(invalid) ) @classmethod @@ -51,6 +60,8 @@ class ValidateNukeWriteKnobs(pyblish.api.ContextPlugin): @classmethod def get_invalid_knobs(cls, context): invalid_knobs = [] + publish_presets = context.data["presets"]["nuke"]["publish"] + knobs_preset = publish_presets["ValidateKnobs"]["knobs"] for instance in context: # Filter publisable instances. if not instance.data["publish"]: @@ -59,15 +70,15 @@ class ValidateNukeWriteKnobs(pyblish.api.ContextPlugin): # Filter families. families = [instance.data["family"]] families += instance.data.get("families", []) - families = list(set(families) & set(cls.knobs.keys())) + families = list(set(families) & set(knobs_preset.keys())) if not families: continue # Get all knobs to validate. knobs = {} for family in families: - for preset in cls.knobs[family]: - knobs.update({preset: cls.knobs[family][preset]}) + for preset in knobs_preset[family]: + knobs.update({preset: knobs_preset[family][preset]}) # Get invalid knobs. nodes = [] @@ -82,16 +93,20 @@ class ValidateNukeWriteKnobs(pyblish.api.ContextPlugin): for node in nodes: for knob in node.knobs(): - if knob in knobs.keys(): - expected = knobs[knob] - if node[knob].value() != expected: - invalid_knobs.append( - { - "knob": node[knob], - "expected": expected, - "current": node[knob].value() - } - ) + if knob not in knobs.keys(): + continue + + expected = knobs[knob] + if node[knob].value() != expected: + invalid_knobs.append( + { + "knob": node[knob], + "name": node[knob].name(), + "label": node[knob].label(), + "expected": expected, + "current": node[knob].value() + } + ) context.data["invalid_knobs"] = invalid_knobs return invalid_knobs From b27828704c52d7cc7751b9ee677473edeb5122be Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 9 Jul 2020 15:18:18 +0100 Subject: [PATCH 086/190] Priority was forced to 50 --- pype/plugins/nuke/publish/submit_nuke_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py index 3731cd25f0..2b8efb4640 100644 --- a/pype/plugins/nuke/publish/submit_nuke_deadline.py +++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py @@ -120,7 +120,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): chunk_size = self.deadline_chunk_size priority = instance.data.get("deadlinePriority") - if priority != 50: + if not priority: priority = self.deadline_priority payload = { From 0faf3971635079b1bb6d5a13055e2d172951976b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Jul 2020 16:40:10 +0200 Subject: [PATCH 087/190] tray attributes fix --- pype/tools/tray/pype_tray.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index d73c1cd0ce..5b1185fa71 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -30,12 +30,15 @@ class TrayManager: os.path.join(CURRENT_DIR, "modules_imports.json") ) presets = config.get_presets(first_run=True) + menu_items = presets["tray"]["menu_items"] try: - self.modules_usage = presets["tray"]["menu_items"]["item_usage"] + self.modules_usage = menu_items["item_usage"] except Exception: self.modules_usage = {} self.log.critical("Couldn't find modules usage data.") + self.module_attributes = menu_items.get("attributes") or {} + self.icon_run = QtGui.QIcon( resources.get_resource("icons", "circle_green.png") ) @@ -71,19 +74,20 @@ class TrayManager: if item_usage is None: item_usage = self.modules_usage.get(import_path, True) - if item_usage: - _attributes = attributes.get(title) - if _attributes is None: - _attributes = attributes.get(import_path) - - if _attributes: - item["attributes"] = _attributes - - items.append(item) - else: + if not item_usage: if not title: title = import_path self.log.info("{} - Module ignored".format(title)) + continue + + _attributes = self.module_attributes.get(title) + if _attributes is None: + _attributes = self.module_attributes.get(import_path) + + if _attributes: + item["attributes"] = _attributes + + items.append(item) if items: self.process_items(items, self.tray_widget.menu) From 6fd146b232835cb2be703852f874a6cbe4e7454d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Jul 2020 18:35:25 +0200 Subject: [PATCH 088/190] feat(premiere): adding key to registry from prelaunch --- pype/hooks/premiere/prelaunch.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pype/hooks/premiere/prelaunch.py b/pype/hooks/premiere/prelaunch.py index 118493e9a7..c0a65c0bf2 100644 --- a/pype/hooks/premiere/prelaunch.py +++ b/pype/hooks/premiere/prelaunch.py @@ -1,5 +1,6 @@ import os import traceback +import winreg from avalon import api, io, lib from pype.lib import PypeHook from pype.api import Logger, Anatomy @@ -14,6 +15,12 @@ class PremierePrelaunch(PypeHook): shell script. """ project_code = None + reg_string_value = [{ + "path": r"Software\Adobe\CSXS.9", + "name": "PlayerDebugMode", + "type": winreg.REG_SZ, + "value": "1" + }] def __init__(self, logger=None): if not logger: @@ -55,6 +62,10 @@ class PremierePrelaunch(PypeHook): # adding project code to env env["AVALON_PROJECT_CODE"] = self.project_code + # add keys to registry + self.modify_registry() + + # start avalon try: __import__("pype.hosts.premiere") __import__("pyblish") @@ -69,6 +80,24 @@ class PremierePrelaunch(PypeHook): return True + def modify_registry(self): + # adding key to registry + for key in self.reg_string_value: + winreg.CreateKey(winreg.HKEY_CURRENT_USER, key["path"]) + rg_key = winreg.OpenKey( + key=winreg.HKEY_CURRENT_USER, + sub_key=key["path"], + reserved=0, + access=winreg.KEY_ALL_ACCESS) + + winreg.SetValueEx( + rg_key, + key["name"], + 0, + key["type"], + key["value"] + ) + def get_anatomy_filled(self): root_path = api.registered_root() project_name = self._S["AVALON_PROJECT"] From 862e233ef919aa215e99dfe1f9f67e9d85ef09a7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Jul 2020 18:35:49 +0200 Subject: [PATCH 089/190] fix(premiere): order of plugins were wrong --- pype/plugins/premiere/publish/collect_frameranges.py | 2 +- .../premiere/publish/collect_instance_representations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/premiere/publish/collect_frameranges.py b/pype/plugins/premiere/publish/collect_frameranges.py index ffcc1023b5..075f84e8e3 100644 --- a/pype/plugins/premiere/publish/collect_frameranges.py +++ b/pype/plugins/premiere/publish/collect_frameranges.py @@ -11,7 +11,7 @@ class CollectFrameranges(pyblish.api.InstancePlugin): """ label = "Collect Clip Frameranges" - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder - 0.01 families = ['clip'] def process(self, instance): diff --git a/pype/plugins/premiere/publish/collect_instance_representations.py b/pype/plugins/premiere/publish/collect_instance_representations.py index f53c60ad64..b62b47c473 100644 --- a/pype/plugins/premiere/publish/collect_instance_representations.py +++ b/pype/plugins/premiere/publish/collect_instance_representations.py @@ -12,7 +12,7 @@ class CollectClipRepresentations(pyblish.api.InstancePlugin): """ label = "Collect Clip Representations" - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder + 0.1 families = ['clip'] def process(self, instance): From 5af32511575edbe66d393843387ff35801118cc5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Jul 2020 18:37:19 +0200 Subject: [PATCH 090/190] fix(premiere): repair function was not working --- .../plugins/premiere/publish/validate_auto_sync_off.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pype/plugins/premiere/publish/validate_auto_sync_off.py b/pype/plugins/premiere/publish/validate_auto_sync_off.py index b6429cfa05..1f3f0b58a5 100644 --- a/pype/plugins/premiere/publish/validate_auto_sync_off.py +++ b/pype/plugins/premiere/publish/validate_auto_sync_off.py @@ -37,13 +37,7 @@ class ValidateAutoSyncOff(pyblish.api.ContextPlugin): query = 'Project where full_name is "{}"'.format(project_name) project = session.query(query).one() - invalid = None - - if project.get('custom_attributes', {}).get( - 'avalon_auto_sync', False): - invalid = project - - return invalid + return project @classmethod def repair(cls, context): @@ -55,4 +49,4 @@ class ValidateAutoSyncOff(pyblish.api.ContextPlugin): except Exception: tp, value, tb = sys.exc_info() session.rollback() - six.reraise(tp, value, tb) + raise From b4a12efecd0e65a3cd0d65e4427568a34219d4c0 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 10 Jul 2020 12:52:17 +0100 Subject: [PATCH 091/190] Fixed the crash for all loaders --- pype/plugins/blender/load/load_action.py | 16 ++-------------- pype/plugins/blender/load/load_animation.py | 4 +--- pype/plugins/blender/load/load_camera.py | 3 +-- pype/plugins/blender/load/load_layout.py | 2 +- pype/plugins/blender/load/load_model.py | 4 ++-- pype/plugins/blender/load/load_rig.py | 2 +- 6 files changed, 8 insertions(+), 23 deletions(-) diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py index d94bd9aac6..1f2a870640 100644 --- a/pype/plugins/blender/load/load_action.py +++ b/pype/plugins/blender/load/load_action.py @@ -174,22 +174,16 @@ class BlendActionLoader(pype.hosts.blender.plugin.AssetLoader): strips = [] - for obj in collection_metadata["objects"]: - + for obj in list(collection_metadata["objects"]): # Get all the strips that use the action arm_objs = [ arm for arm in bpy.data.objects if arm.type == 'ARMATURE'] for armature_obj in arm_objs: - if armature_obj.animation_data is not None: - for track in armature_obj.animation_data.nla_tracks: - for strip in track.strips: - if strip.action == obj.animation_data.action: - strips.append(strip) bpy.data.actions.remove(obj.animation_data.action) @@ -277,22 +271,16 @@ class BlendActionLoader(pype.hosts.blender.plugin.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - for obj in objects: - + for obj in list(objects): # Get all the strips that use the action arm_objs = [ arm for arm in bpy.data.objects if arm.type == 'ARMATURE'] for armature_obj in arm_objs: - if armature_obj.animation_data is not None: - for track in armature_obj.animation_data.nla_tracks: - for strip in track.strips: - if strip.action == obj.animation_data.action: - track.strips.remove(strip) bpy.data.actions.remove(obj.animation_data.action) diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index 1c0e6e0906..32050eca99 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -30,9 +30,7 @@ class BlendAnimationLoader(pype.hosts.blender.plugin.AssetLoader): color = "orange" def _remove(self, objects, lib_container): - - for obj in objects: - + for obj in list(objects): if obj.type == 'ARMATURE': bpy.data.armatures.remove(obj.data) elif obj.type == 'MESH': diff --git a/pype/plugins/blender/load/load_camera.py b/pype/plugins/blender/load/load_camera.py index 7fd8f94b4e..9dd5c2bfd8 100644 --- a/pype/plugins/blender/load/load_camera.py +++ b/pype/plugins/blender/load/load_camera.py @@ -28,8 +28,7 @@ class BlendCameraLoader(pype.hosts.blender.plugin.AssetLoader): color = "orange" def _remove(self, objects, lib_container): - - for obj in objects: + for obj in list(objects): bpy.data.cameras.remove(obj.data) bpy.data.collections.remove(bpy.data.collections[lib_container]) diff --git a/pype/plugins/blender/load/load_layout.py b/pype/plugins/blender/load/load_layout.py index cfab5a207b..6a51d7cf16 100644 --- a/pype/plugins/blender/load/load_layout.py +++ b/pype/plugins/blender/load/load_layout.py @@ -21,7 +21,7 @@ class BlendLayoutLoader(plugin.AssetLoader): color = "orange" def _remove(self, objects, obj_container): - for obj in objects: + for obj in list(objects): if obj.type == 'ARMATURE': bpy.data.armatures.remove(obj.data) elif obj.type == 'MESH': diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index ad9137a15d..4ac86b3aef 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -25,8 +25,8 @@ class BlendModelLoader(plugin.AssetLoader): color = "orange" def _remove(self, objects, container): - for obj in objects: - for material_slot in obj.material_slots: + for obj in list(objects): + for material_slot in list(obj.material_slots): bpy.data.materials.remove(material_slot.material) bpy.data.meshes.remove(obj.data) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index e09a9cb92f..6dc2273c6e 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -25,7 +25,7 @@ class BlendRigLoader(plugin.AssetLoader): color = "orange" def _remove(self, objects, obj_container): - for obj in objects: + for obj in list(objects): if obj.type == 'ARMATURE': bpy.data.armatures.remove(obj.data) elif obj.type == 'MESH': From 000885da3cf6472f20b1f2297490dfd419b4b46d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jul 2020 11:07:26 +0200 Subject: [PATCH 092/190] add MissingPermision to ftrack.lib --- pype/modules/ftrack/lib/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/modules/ftrack/lib/__init__.py b/pype/modules/ftrack/lib/__init__.py index df546ab725..9de477059a 100644 --- a/pype/modules/ftrack/lib/__init__.py +++ b/pype/modules/ftrack/lib/__init__.py @@ -1,6 +1,6 @@ from . import avalon_sync from . import credentials -from .ftrack_base_handler import BaseHandler +from .ftrack_base_handler import BaseHandler, MissingPermision from .ftrack_event_handler import BaseEvent from .ftrack_action_handler import BaseAction, statics_icon from .ftrack_app_handler import AppAction @@ -9,6 +9,7 @@ __all__ = [ "avalon_sync", "credentials", "BaseHandler", + "MissingPermision", "BaseEvent", "BaseAction", "statics_icon", From 22ff14b93d7bfb62808f1b784d7c0f85bc722bd0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jul 2020 11:18:28 +0200 Subject: [PATCH 093/190] set CLASS_DEFINIION in clockify to be able set workspace attribute --- pype/modules/clockify/__init__.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pype/modules/clockify/__init__.py b/pype/modules/clockify/__init__.py index aab0d048de..0ee2189fa5 100644 --- a/pype/modules/clockify/__init__.py +++ b/pype/modules/clockify/__init__.py @@ -3,12 +3,8 @@ from .widget_settings import ClockifySettings from .widget_message import MessageWidget from .clockify import ClockifyModule -__all__ = [ - "ClockifyAPI", - "ClockifySettings", - "ClockifyModule", - "MessageWidget" -] - +CLASS_DEFINIION = ClockifyModule + + def tray_init(tray_widget, main_widget): return ClockifyModule(main_widget, tray_widget) From be135a79322227c9d824017dc2a3b28f7786a48f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jul 2020 11:18:58 +0200 Subject: [PATCH 094/190] added workspace_name attribute to clockify --- pype/modules/clockify/clockify.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pype/modules/clockify/clockify.py b/pype/modules/clockify/clockify.py index 2ab22702c1..02b322c1c6 100644 --- a/pype/modules/clockify/clockify.py +++ b/pype/modules/clockify/clockify.py @@ -8,7 +8,14 @@ from . import ClockifySettings, ClockifyAPI, MessageWidget class ClockifyModule: + workspace_name = None + def __init__(self, main_parent=None, parent=None): + if not self.workspace_name: + raise Exception("Clockify Workspace is not set in config.") + + os.environ["CLOCKIFY_WORKSPACE"] = self.workspace_name + self.log = Logger().get_logger(self.__class__.__name__, "PypeTray") self.main_parent = main_parent From 0ace96c4dc7e8b012c5946ea25f56b3029e5d79f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jul 2020 11:23:43 +0200 Subject: [PATCH 095/190] catch all exceptions in tray during module object creation --- pype/tools/tray/pype_tray.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 5b1185fa71..7dda8bf4f7 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -203,7 +203,7 @@ class TrayManager: obj.set_qaction(action, self.icon_failed) self.modules[name] = obj self.log.info("{} - Module imported".format(title)) - except ImportError as ie: + except Exception as exc: if self.services_submenu is None: self.services_submenu = QtWidgets.QMenu( 'Services', self.tray_widget.menu @@ -212,7 +212,7 @@ class TrayManager: action.setIcon(self.icon_failed) self.services_submenu.addAction(action) self.log.warning( - "{} - Module import Error: {}".format(title, str(ie)), + "{} - Module import Error: {}".format(title, str(exc)), exc_info=True ) return False From 53bdf84abb9407ae5e3970a6a40b335d4111b3d5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jul 2020 13:59:17 +0200 Subject: [PATCH 096/190] removed Thumbs.db from pype/res --- res/icons/Thumbs.db | Bin 6144 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 res/icons/Thumbs.db diff --git a/res/icons/Thumbs.db b/res/icons/Thumbs.db deleted file mode 100644 index fa56c871f60da645a236c2786a1fdfe9f5693dc2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6144 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 From 21645bb30bcd8f6c02751e24f660e73ce5c51aa7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jul 2020 13:59:52 +0200 Subject: [PATCH 097/190] moved remaining resources from `~/pype/res/` to `~/pype/pype/resources/icons/` --- {res => pype/resources}/icons/folder-favorite.png | Bin {res => pype/resources}/icons/folder-favorite2.png | Bin {res => pype/resources}/icons/folder-favorite3.png | Bin {res => pype/resources}/icons/inventory.png | Bin {res => pype/resources}/icons/loader.png | Bin {res => pype/resources}/icons/lookmanager.png | Bin {res => pype/resources}/icons/workfiles.png | Bin 7 files changed, 0 insertions(+), 0 deletions(-) rename {res => pype/resources}/icons/folder-favorite.png (100%) rename {res => pype/resources}/icons/folder-favorite2.png (100%) rename {res => pype/resources}/icons/folder-favorite3.png (100%) rename {res => pype/resources}/icons/inventory.png (100%) rename {res => pype/resources}/icons/loader.png (100%) rename {res => pype/resources}/icons/lookmanager.png (100%) rename {res => pype/resources}/icons/workfiles.png (100%) diff --git a/res/icons/folder-favorite.png b/pype/resources/icons/folder-favorite.png similarity index 100% rename from res/icons/folder-favorite.png rename to pype/resources/icons/folder-favorite.png diff --git a/res/icons/folder-favorite2.png b/pype/resources/icons/folder-favorite2.png similarity index 100% rename from res/icons/folder-favorite2.png rename to pype/resources/icons/folder-favorite2.png diff --git a/res/icons/folder-favorite3.png b/pype/resources/icons/folder-favorite3.png similarity index 100% rename from res/icons/folder-favorite3.png rename to pype/resources/icons/folder-favorite3.png diff --git a/res/icons/inventory.png b/pype/resources/icons/inventory.png similarity index 100% rename from res/icons/inventory.png rename to pype/resources/icons/inventory.png diff --git a/res/icons/loader.png b/pype/resources/icons/loader.png similarity index 100% rename from res/icons/loader.png rename to pype/resources/icons/loader.png diff --git a/res/icons/lookmanager.png b/pype/resources/icons/lookmanager.png similarity index 100% rename from res/icons/lookmanager.png rename to pype/resources/icons/lookmanager.png diff --git a/res/icons/workfiles.png b/pype/resources/icons/workfiles.png similarity index 100% rename from res/icons/workfiles.png rename to pype/resources/icons/workfiles.png From 308d643bae2036d81f580900a5b30587a833568b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jul 2020 14:12:18 +0200 Subject: [PATCH 098/190] moved resources changed in code --- pype/hosts/maya/customize.py | 7 ++----- pype/hosts/nuke/utils.py | 5 ++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/pype/hosts/maya/customize.py b/pype/hosts/maya/customize.py index 8bd7052d9e..cf179e1d63 100644 --- a/pype/hosts/maya/customize.py +++ b/pype/hosts/maya/customize.py @@ -70,16 +70,13 @@ def override_component_mask_commands(): def override_toolbox_ui(): """Add custom buttons in Toolbox as replacement for Maya web help icon.""" - import pype - res = os.path.join(os.path.dirname(os.path.dirname(pype.__file__)), - "res") - icons = os.path.join(res, "icons") - import avalon.tools.sceneinventory as inventory import avalon.tools.loader as loader from avalon.maya.pipeline import launch_workfiles_app import mayalookassigner + from pype.api import resources + icons = resources.get_resource("icons") # Ensure the maya web icon on toolbox exists web_button = "ToolBox|MainToolboxLayout|mayaWebButton" diff --git a/pype/hosts/nuke/utils.py b/pype/hosts/nuke/utils.py index aa5bc1077e..72c7b7bc14 100644 --- a/pype/hosts/nuke/utils.py +++ b/pype/hosts/nuke/utils.py @@ -1,6 +1,7 @@ import os import nuke from avalon.nuke import lib as anlib +from pype.api import resources def set_context_favorites(favorites={}): @@ -9,9 +10,7 @@ def set_context_favorites(favorites={}): Argumets: favorites (dict): couples of {name:path} """ - dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) - icon_path = os.path.join(dir, 'res', 'icons', 'folder-favorite3.png') - + icon_path = resources.get_resource("icons", "folder-favorite3.png") for name, path in favorites.items(): nuke.addFavoriteDir( name, From b8afa33abbe4b92bdabb3b0db9f26f1d0fc33608 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jul 2020 10:50:19 +0200 Subject: [PATCH 099/190] fix import --- pype/modules/clockify/ftrack_actions/action_clockify_sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py index a041e6ada6..1fcb72d85c 100644 --- a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py +++ b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py @@ -4,7 +4,7 @@ import argparse import logging import json import ftrack_api -from pype.modules.ftrack import BaseAction, MissingPermision +from pype.modules.ftrack.lib import BaseAction, MissingPermision from pype.modules.clockify import ClockifyAPI From 09dc456f738c8ffb62fcf2f667eb3f4fa8dd99f9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jul 2020 11:20:25 +0200 Subject: [PATCH 100/190] removed preregister method because api check must happen just before launch because is running in different process than tray --- .../clockify/ftrack_actions/action_clockify_sync.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py index 1fcb72d85c..bcc33ba6c1 100644 --- a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py +++ b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py @@ -26,15 +26,6 @@ class SyncClocify(BaseAction): #: CLockifyApi clockapi = ClockifyAPI() - def preregister(self): - if self.clockapi.workspace_id is None: - return "Clockify Workspace or API key are not set!" - - if self.clockapi.validate_workspace_perm() is False: - raise MissingPermision('Clockify') - - return True - def discover(self, session, entities, event): ''' Validation ''' if len(entities) != 1: From 11a680dba53a9f8b4c318146302273d893598b86 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jul 2020 11:20:55 +0200 Subject: [PATCH 101/190] simplified discover --- .../clockify/ftrack_actions/action_clockify_sync.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py index bcc33ba6c1..7956985012 100644 --- a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py +++ b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py @@ -27,13 +27,12 @@ class SyncClocify(BaseAction): clockapi = ClockifyAPI() def discover(self, session, entities, event): - ''' Validation ''' - if len(entities) != 1: - return False - - if entities[0].entity_type.lower() != "project": - return False - return True + if ( + len(entities) == 1 + and entities[0].entity_type.lower() == "project" + ): + return True + return False def launch(self, session, entities, event): # JOB SETTINGS From 5c20c38630184046e0526f34845e9f7779f5f07d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jul 2020 11:21:20 +0200 Subject: [PATCH 102/190] api check addedto beginning of launch method --- .../clockify/ftrack_actions/action_clockify_sync.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py index 7956985012..a3dc9ae278 100644 --- a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py +++ b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py @@ -35,6 +35,19 @@ class SyncClocify(BaseAction): return False def launch(self, session, entities, event): + self.clockapi.set_api() + if self.clockapi.workspace_id is None: + return { + "success": False, + "message": "Clockify Workspace or API key are not set!" + } + + if self.clockapi.validate_workspace_perm() is False: + return { + "success": False, + "message": "Missing permissions for this action!" + } + # JOB SETTINGS userId = event['source']['user']['id'] user = session.query('User where id is ' + userId).one() From 88ddce4313a4e0fde8dd3c3c8e748f9db73c6262 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jul 2020 11:21:35 +0200 Subject: [PATCH 103/190] removed deprecated parts of stync action --- .../ftrack_actions/action_clockify_sync.py | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py index a3dc9ae278..335c045952 100644 --- a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py +++ b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py @@ -111,48 +111,4 @@ class SyncClocify(BaseAction): def register(session, **kw): - '''Register plugin. Called when used as an plugin.''' - - if not isinstance(session, ftrack_api.session.Session): - return - SyncClocify(session).register() - - -def main(arguments=None): - '''Set up logging and register action.''' - if arguments is None: - arguments = [] - - parser = argparse.ArgumentParser() - # Allow setting of logging level from arguments. - loggingLevels = {} - for level in ( - logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, - logging.ERROR, logging.CRITICAL - ): - loggingLevels[logging.getLevelName(level).lower()] = level - - parser.add_argument( - '-v', '--verbosity', - help='Set the logging output verbosity.', - choices=loggingLevels.keys(), - default='info' - ) - namespace = parser.parse_args(arguments) - - # Set up basic logging - logging.basicConfig(level=loggingLevels[namespace.verbosity]) - - session = ftrack_api.Session() - register(session) - - # Wait for events - logging.info( - 'Registered actions and listening for events. Use Ctrl-C to abort.' - ) - session.event_hub.wait() - - -if __name__ == '__main__': - raise SystemExit(main(sys.argv[1:])) From 56736a62c1cc232858796f40686fc8daa9cd263c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jul 2020 11:22:28 +0200 Subject: [PATCH 104/190] made ftrack part of getting data easier to read --- .../ftrack_actions/action_clockify_sync.py | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py index 335c045952..105398d58c 100644 --- a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py +++ b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py @@ -60,23 +60,25 @@ class SyncClocify(BaseAction): }) }) session.commit() + + project_entity = entities[0] + if project_entity.entity_type.lower() != "project": + project_entity = self.get_project_from_entity(project_entity) + + project_name = project_entity["full_name"] + self.log.info( + "Synchronization of project \"{}\" to clockify begins.".format( + project_name + ) + ) + task_types = ( + project_entity["project_schema"]["_task_type_schema"]["types"] + ) + task_type_names = [ + task_type["name"] for task_type in task_types + ] try: - entity = entities[0] - - if entity.entity_type.lower() == 'project': - project = entity - else: - project = entity['project'] - project_name = project['full_name'] - - task_types = [] - for task_type in project['project_schema']['_task_type_schema'][ - 'types' - ]: - task_types.append(task_type['name']) - clockify_projects = self.clockapi.get_projects() - if project_name not in clockify_projects: response = self.clockapi.add_project(project_name) if 'id' not in response: From acd933954b6edb5f10932f4178d9624c0bcafe6f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jul 2020 11:22:52 +0200 Subject: [PATCH 105/190] do not store project_id because is not used --- pype/modules/clockify/ftrack_actions/action_clockify_sync.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py index 105398d58c..b490a51df6 100644 --- a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py +++ b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py @@ -89,9 +89,6 @@ class SyncClocify(BaseAction): 'success': False, 'message': 'Can\'t create project, unexpected error' } - project_id = response['id'] - else: - project_id = clockify_projects[project_name] clockify_workspace_tags = self.clockapi.get_tags() for task_type in task_types: From 8e05b9f33cf9967c9fc35706aa0ddc8547ca9f9a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jul 2020 11:23:25 +0200 Subject: [PATCH 106/190] task types have more logs --- .../ftrack_actions/action_clockify_sync.py | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py index b490a51df6..2047bb69bb 100644 --- a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py +++ b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py @@ -91,14 +91,21 @@ class SyncClocify(BaseAction): } clockify_workspace_tags = self.clockapi.get_tags() - for task_type in task_types: - if task_type not in clockify_workspace_tags: - response = self.clockapi.add_tag(task_type) - if 'id' not in response: - self.log.error('Task {} can\'t be created'.format( - task_type - )) - continue + for task_type_name in task_type_names: + if task_type_name in clockify_workspace_tags: + self.log.debug( + "Task \"{}\" already exist".format(task_type_name) + ) + continue + + response = self.clockapi.add_tag(task_type_name) + if "id" not in response: + self.log.warning( + "Task \"{}\" can't be created. Response: {}".format( + task_type_name, response + ) + ) + except Exception: job['status'] = 'failed' session.commit() From 6d8148af6b308464a477f91abbdd0715c4230592 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jul 2020 11:23:51 +0200 Subject: [PATCH 107/190] not successfull project creation log full response from clockify --- .../ftrack_actions/action_clockify_sync.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py index 2047bb69bb..4c1261e817 100644 --- a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py +++ b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py @@ -81,13 +81,18 @@ class SyncClocify(BaseAction): clockify_projects = self.clockapi.get_projects() if project_name not in clockify_projects: response = self.clockapi.add_project(project_name) - if 'id' not in response: - self.log.error('Project {} can\'t be created'.format( - project_name - )) + if "id" not in response: + self.log.warning( + "Project \"{}\" can't be created. Response: {}".format( + project_name, response + ) + ) return { - 'success': False, - 'message': 'Can\'t create project, unexpected error' + "success": False, + "message": ( + "Can't create clockify project \"{}\"." + " Unexpected error." + ).format(project_name) } clockify_workspace_tags = self.clockapi.get_tags() From a8db8cd241e60d808e587ad61a1349e2ec43cb63 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jul 2020 11:25:22 +0200 Subject: [PATCH 108/190] removed unused imports --- .../clockify/ftrack_actions/action_clockify_sync.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py index 4c1261e817..d3537d475d 100644 --- a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py +++ b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py @@ -1,10 +1,5 @@ -import os -import sys -import argparse -import logging import json -import ftrack_api -from pype.modules.ftrack.lib import BaseAction, MissingPermision +from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.modules.clockify import ClockifyAPI @@ -20,9 +15,8 @@ class SyncClocify(BaseAction): #: roles that are allowed to register this action role_list = ["Pypeclub", "Administrator", "project Manager"] #: icon - icon = '{}/app_icons/clockify-white.png'.format( - os.environ.get('PYPE_STATICS_SERVER', '') - ) + icon = statics_icon("ftrack", "app_icons", "clockify-white.png") + #: CLockifyApi clockapi = ClockifyAPI() From 2fdf6a4ec1e2253370b1bc4a052e233c8b7929b2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jul 2020 11:28:39 +0200 Subject: [PATCH 109/190] undo changes in ftrack.lib.__init__ --- pype/modules/ftrack/lib/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pype/modules/ftrack/lib/__init__.py b/pype/modules/ftrack/lib/__init__.py index 9de477059a..df546ab725 100644 --- a/pype/modules/ftrack/lib/__init__.py +++ b/pype/modules/ftrack/lib/__init__.py @@ -1,6 +1,6 @@ from . import avalon_sync from . import credentials -from .ftrack_base_handler import BaseHandler, MissingPermision +from .ftrack_base_handler import BaseHandler from .ftrack_event_handler import BaseEvent from .ftrack_action_handler import BaseAction, statics_icon from .ftrack_app_handler import AppAction @@ -9,7 +9,6 @@ __all__ = [ "avalon_sync", "credentials", "BaseHandler", - "MissingPermision", "BaseEvent", "BaseAction", "statics_icon", From 05cc4385dfbff2eb1f022f75f2cc8091161dc80c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jul 2020 15:04:42 +0200 Subject: [PATCH 110/190] check if last workfile exist before env variable is set --- pype/modules/ftrack/lib/ftrack_app_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/ftrack/lib/ftrack_app_handler.py b/pype/modules/ftrack/lib/ftrack_app_handler.py index 34ab8c5ee4..efc2df0ea5 100644 --- a/pype/modules/ftrack/lib/ftrack_app_handler.py +++ b/pype/modules/ftrack/lib/ftrack_app_handler.py @@ -216,7 +216,7 @@ class AppAction(BaseAction): "AVALON_HIERARCHY": hierarchy, "AVALON_WORKDIR": workdir }) - if last_workfile_path: + if last_workfile_path and os.path.exists(last_workfile_path): prep_env["AVALON_LAST_WORKFILE"] = last_workfile_path prep_env.update(anatomy.roots_obj.root_environments()) From 21df2093d56ff8c199457ae8c77577a8c0736152 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 15 Jul 2020 12:23:04 +0100 Subject: [PATCH 111/190] Fixed problem with local objects --- pype/hosts/blender/plugin.py | 5 ++-- pype/plugins/blender/load/load_camera.py | 35 +++++++++++++++--------- pype/plugins/blender/load/load_layout.py | 17 ++++++------ pype/plugins/blender/load/load_model.py | 10 +++---- pype/plugins/blender/load/load_rig.py | 32 ++++++++++++---------- 5 files changed, 56 insertions(+), 43 deletions(-) diff --git a/pype/hosts/blender/plugin.py b/pype/hosts/blender/plugin.py index ab53d49041..07080a86c4 100644 --- a/pype/hosts/blender/plugin.py +++ b/pype/hosts/blender/plugin.py @@ -45,8 +45,9 @@ def get_unique_number( def prepare_data(data, container_name): name = data.name - data = data.make_local() - data.name = f"{name}:{container_name}" + local_data = data.make_local() + local_data.name = f"{name}:{container_name}" + return local_data def create_blender_context(active: Optional[bpy.types.Object] = None, diff --git a/pype/plugins/blender/load/load_camera.py b/pype/plugins/blender/load/load_camera.py index 7fd8f94b4e..2cd9cd7b34 100644 --- a/pype/plugins/blender/load/load_camera.py +++ b/pype/plugins/blender/load/load_camera.py @@ -51,26 +51,26 @@ class BlendCameraLoader(pype.hosts.blender.plugin.AssetLoader): objects_list = [] for obj in camera_container.objects: - obj = obj.make_local() - obj.data.make_local() + local_obj = obj.make_local() + local_obj.data.make_local() - if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[blender.pipeline.AVALON_PROPERTY] = dict() + if not local_obj.get(blender.pipeline.AVALON_PROPERTY): + local_obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[blender.pipeline.AVALON_PROPERTY] + avalon_info = local_obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) if actions[0] is not None: - if obj.animation_data is None: - obj.animation_data_create() - obj.animation_data.action = actions[0] + if local_obj.animation_data is None: + local_obj.animation_data_create() + local_obj.animation_data.action = actions[0] if actions[1] is not None: - if obj.data.animation_data is None: - obj.data.animation_data_create() - obj.data.animation_data.action = actions[1] + if local_obj.data.animation_data is None: + local_obj.data.animation_data_create() + local_obj.data.animation_data.action = actions[1] - objects_list.append(obj) + objects_list.append(local_obj) camera_container.pop(blender.pipeline.AVALON_PROPERTY) @@ -190,7 +190,16 @@ class BlendCameraLoader(pype.hosts.blender.plugin.AssetLoader): camera = objects[0] - actions = (camera.animation_data.action, camera.data.animation_data.action) + camera_action = None + camera_data_action = None + + if camera.animation_data and camera.animation_data.action: + camera_action = camera.animation_data.action + + if camera.data.animation_data and camera.data.animation_data.action: + camera_data_action = camera.data.animation_data.action + + actions = (camera_action, camera_data_action) self._remove(objects, lib_container) diff --git a/pype/plugins/blender/load/load_layout.py b/pype/plugins/blender/load/load_layout.py index cfab5a207b..d3bf881bc1 100644 --- a/pype/plugins/blender/load/load_layout.py +++ b/pype/plugins/blender/load/load_layout.py @@ -79,21 +79,21 @@ class BlendLayoutLoader(plugin.AssetLoader): # The armature is unparented for all the non-local meshes, # when it is made local. for obj in objects + armatures: - obj.make_local() + local_obj = obj.make_local() if obj.data: obj.data.make_local() - if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[blender.pipeline.AVALON_PROPERTY] = dict() + if not local_obj.get(blender.pipeline.AVALON_PROPERTY): + local_obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[blender.pipeline.AVALON_PROPERTY] + avalon_info = local_obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) - action = actions.get(obj.name, None) + action = actions.get(local_obj.name, None) + + if local_obj.type == 'ARMATURE' and action is not None: + local_obj.animation_data.action = action - if obj.type == 'ARMATURE' and action is not None: - obj.animation_data.action = action - layout_container.pop(blender.pipeline.AVALON_PROPERTY) bpy.ops.object.select_all(action='DESELECT') @@ -222,6 +222,7 @@ class BlendLayoutLoader(plugin.AssetLoader): for obj in objects: if obj.type == 'ARMATURE': + if obj.animation_data and obj.animation_data.action: actions[obj.name] = obj.animation_data.action self._remove(objects, obj_container) diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index ad9137a15d..5e5bee1e6e 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -53,16 +53,16 @@ class BlendModelLoader(plugin.AssetLoader): model_container.name = container_name for obj in model_container.objects: - plugin.prepare_data(obj, container_name) - plugin.prepare_data(obj.data, container_name) + local_obj = plugin.prepare_data(obj, container_name) + plugin.prepare_data(local_obj.data, container_name) - for material_slot in obj.material_slots: + for material_slot in local_obj.material_slots: plugin.prepare_data(material_slot.material, container_name) if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[blender.pipeline.AVALON_PROPERTY] = dict() + local_obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[blender.pipeline.AVALON_PROPERTY] + avalon_info = local_obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) model_container.pop(blender.pipeline.AVALON_PROPERTY) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index e09a9cb92f..a9fb0c18f1 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -63,25 +63,25 @@ class BlendRigLoader(plugin.AssetLoader): ] for child in rig_container.children: - plugin.prepare_data(child, container_name) - meshes.extend(child.objects) + local_child = plugin.prepare_data(child, container_name) + meshes.extend(local_child.objects) # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. for obj in meshes + armatures: - plugin.prepare_data(obj, container_name) - plugin.prepare_data(obj.data, container_name) - - if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[blender.pipeline.AVALON_PROPERTY] = dict() - - avalon_info = obj[blender.pipeline.AVALON_PROPERTY] - avalon_info.update({"container_name": container_name}) - - if obj.type == 'ARMATURE' and action is not None: - obj.animation_data.action = action - + local_obj = plugin.prepare_data(obj, container_name) + plugin.prepare_data(local_obj.data, container_name) + + if not local_obj.get(blender.pipeline.AVALON_PROPERTY): + local_obj[blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = local_obj[blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + if local_obj.type == 'ARMATURE' and action is not None: + local_obj.animation_data.action = action + rig_container.pop(blender.pipeline.AVALON_PROPERTY) bpy.ops.object.select_all(action='DESELECT') @@ -214,7 +214,9 @@ class BlendRigLoader(plugin.AssetLoader): armatures = [obj for obj in objects if obj.type == 'ARMATURE'] assert(len(armatures) == 1) - action = armatures[0].animation_data.action + action = None + if armatures[0].animation_data and armatures[0].animation_data.action: + action = armatures[0].animation_data.action parent = plugin.get_parent_collection(obj_container) From ae043d5a65c940f2c67c5fc1f6f6ec037af54b04 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jul 2020 15:01:55 +0200 Subject: [PATCH 112/190] fix icon path --- pype/modules/clockify/ftrack_actions/action_clockify_sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py index d3537d475d..c464a38e56 100644 --- a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py +++ b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py @@ -15,7 +15,7 @@ class SyncClocify(BaseAction): #: roles that are allowed to register this action role_list = ["Pypeclub", "Administrator", "project Manager"] #: icon - icon = statics_icon("ftrack", "app_icons", "clockify-white.png") + icon = statics_icon("app_icons", "clockify-white.png") #: CLockifyApi clockapi = ClockifyAPI() From 6ea84a04e24094c6f53a5e53d240007d60469a6d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jul 2020 15:02:08 +0200 Subject: [PATCH 113/190] fix job status on crash --- .../clockify/ftrack_actions/action_clockify_sync.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py index c464a38e56..654ed91b0e 100644 --- a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py +++ b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py @@ -106,11 +106,14 @@ class SyncClocify(BaseAction): ) except Exception: - job['status'] = 'failed' - session.commit() - return False + pass - job['status'] = 'done' + finally: + if job["status"] != "done": + job["status"] = "failed" + session.commit() + + job["status"] = "done" session.commit() return True From 0cdf9ac479d2ce0bf5d5258010502ac29251cec5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jul 2020 15:02:20 +0200 Subject: [PATCH 114/190] set estimate for new project --- pype/modules/clockify/clockify_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/clockify/clockify_api.py b/pype/modules/clockify/clockify_api.py index f012efc002..64ed512f57 100644 --- a/pype/modules/clockify/clockify_api.py +++ b/pype/modules/clockify/clockify_api.py @@ -363,7 +363,7 @@ class ClockifyAPI(metaclass=Singleton): "clientId": "", "isPublic": "false", "estimate": { - # "estimate": "3600", + "estimate": "0", "type": "AUTO" }, "color": "#f44336", From f2fdd9ca0a64a2651503977b46977900c7d98c71 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jul 2020 15:22:08 +0200 Subject: [PATCH 115/190] job status fix --- pype/modules/clockify/ftrack_actions/action_clockify_sync.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py index 654ed91b0e..0ba4c3a265 100644 --- a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py +++ b/pype/modules/clockify/ftrack_actions/action_clockify_sync.py @@ -105,6 +105,8 @@ class SyncClocify(BaseAction): ) ) + job["status"] = "done" + except Exception: pass @@ -113,8 +115,6 @@ class SyncClocify(BaseAction): job["status"] = "failed" session.commit() - job["status"] = "done" - session.commit() return True From 4d1f5b12dce60b5f26f4b559f843c09763b433e4 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 16 Jul 2020 11:44:02 +0100 Subject: [PATCH 116/190] Fix preview always True. --- pype/plugins/global/publish/submit_publish_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 7a73e921e2..4f32e37c17 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -488,7 +488,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if bake_render_path: preview = False - if "celaction" in self.hosts: + if "celaction" in pyblish.api.registered_hosts(): preview = True staging = os.path.dirname(list(collection)[0]) From 8aed10d1b522f63141ff053d6af80d9b4e3a33c7 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 16 Jul 2020 14:17:33 +0100 Subject: [PATCH 117/190] Fixed indentation --- pype/plugins/blender/load/load_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/blender/load/load_layout.py b/pype/plugins/blender/load/load_layout.py index d3bf881bc1..c23cdbd354 100644 --- a/pype/plugins/blender/load/load_layout.py +++ b/pype/plugins/blender/load/load_layout.py @@ -223,7 +223,7 @@ class BlendLayoutLoader(plugin.AssetLoader): for obj in objects: if obj.type == 'ARMATURE': if obj.animation_data and obj.animation_data.action: - actions[obj.name] = obj.animation_data.action + actions[obj.name] = obj.animation_data.action self._remove(objects, obj_container) From 91786b5206edd810b77b3e7b5e0193b6d9106cb3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 16 Jul 2020 14:45:51 +0100 Subject: [PATCH 118/190] fix(celaction): support for space in folder or file name --- pype/hooks/celaction/prelaunch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hooks/celaction/prelaunch.py b/pype/hooks/celaction/prelaunch.py index df9da6cbbf..e1e86cc919 100644 --- a/pype/hooks/celaction/prelaunch.py +++ b/pype/hooks/celaction/prelaunch.py @@ -106,7 +106,7 @@ class CelactionPrelaunchHook(PypeHook): f"--project {project}", f"--asset {asset}", f"--task {task}", - "--currentFile \"*SCENE*\"", + "--currentFile \\\"\"*SCENE*\"\\\"", "--chunk *CHUNK*", "--frameStart *START*", "--frameEnd *END*", From 027ac520045cc26d449dd29f65b014747477c1e9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 16 Jul 2020 14:46:25 +0100 Subject: [PATCH 119/190] fix(celaction): correct host path --- pype/hosts/celaction/cli.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pype/hosts/celaction/cli.py b/pype/hosts/celaction/cli.py index 8cf2bcc791..42f7a1a385 100644 --- a/pype/hosts/celaction/cli.py +++ b/pype/hosts/celaction/cli.py @@ -46,9 +46,6 @@ def cli(): parser.add_argument("--resolutionHeight", help=("Height of resolution")) - # parser.add_argument("--programDir", - # help=("Directory with celaction program installation")) - celaction.kwargs = parser.parse_args(sys.argv[1:]).__dict__ @@ -78,7 +75,7 @@ def _prepare_publish_environments(): env["AVALON_WORKDIR"] = os.getenv("AVALON_WORKDIR") env["AVALON_HIERARCHY"] = hierarchy env["AVALON_PROJECTCODE"] = project_doc["data"].get("code", "") - env["AVALON_APP"] = publish_host + env["AVALON_APP"] = f"hosts.{publish_host}" env["AVALON_APP_NAME"] = "celaction_local" env["PYBLISH_HOSTS"] = publish_host From 5f53213dc5fbd70cd263f54e6db4f41728ca603b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 16 Jul 2020 17:22:54 +0200 Subject: [PATCH 120/190] added mac stuff to gitignore --- .gitignore | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.gitignore b/.gitignore index 4b2eb5453a..101c1e6224 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,31 @@ __pycache__/ *.py[cod] *$py.class +# Mac Stuff +########### +# General +.DS_Store +.AppleDouble +.LSOverride +# Icon must end with two \r +Icon +# Thumbnails +._* +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + # Documentation ############### /docs/build From 59608daac1017a602ce2791e95d091592b1e9dda Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 17 Jul 2020 11:50:07 +0100 Subject: [PATCH 121/190] fix(global): remove duplicate code for copy files --- pype/plugins/global/publish/integrate_new.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 9f20999f55..d151cfc608 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -514,12 +514,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): instance: the instance to integrate """ transfers = instance.data.get("transfers", list()) - - for src, dest in transfers: - if os.path.normpath(src) != os.path.normpath(dest): - self.copy_file(src, dest) - - transfers = instance.data.get("transfers", list()) for src, dest in transfers: self.copy_file(src, dest) From f62dbe26721d557ac0e3397bcd48a2057a96e499 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 17 Jul 2020 16:10:43 +0200 Subject: [PATCH 122/190] change string attribute to ingeter in project sync --- pype/modules/clockify/clockify_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/clockify/clockify_api.py b/pype/modules/clockify/clockify_api.py index 64ed512f57..86365a9352 100644 --- a/pype/modules/clockify/clockify_api.py +++ b/pype/modules/clockify/clockify_api.py @@ -363,7 +363,7 @@ class ClockifyAPI(metaclass=Singleton): "clientId": "", "isPublic": "false", "estimate": { - "estimate": "0", + "estimate": 0, "type": "AUTO" }, "color": "#f44336", From ab9e1637fe4ee07eea5ba590c2000866e61a61a9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Jul 2020 16:14:30 +0200 Subject: [PATCH 123/190] use constant from ftrack.lib for custom attribute name --- pype/plugins/premiere/publish/validate_auto_sync_off.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/plugins/premiere/publish/validate_auto_sync_off.py b/pype/plugins/premiere/publish/validate_auto_sync_off.py index 1f3f0b58a5..ca75a4d14e 100644 --- a/pype/plugins/premiere/publish/validate_auto_sync_off.py +++ b/pype/plugins/premiere/publish/validate_auto_sync_off.py @@ -2,7 +2,11 @@ import sys import pyblish.api import pype.api import avalon.api -import six + +try: + from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_AUTO_SYNC +except Exception: + CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" class ValidateAutoSyncOff(pyblish.api.ContextPlugin): From f7304ba26d6127410a7dab1b9577135ac8593ff9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Jul 2020 16:14:48 +0200 Subject: [PATCH 124/190] do not query project in process part (not used) --- pype/plugins/premiere/publish/validate_auto_sync_off.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pype/plugins/premiere/publish/validate_auto_sync_off.py b/pype/plugins/premiere/publish/validate_auto_sync_off.py index ca75a4d14e..53042fef6a 100644 --- a/pype/plugins/premiere/publish/validate_auto_sync_off.py +++ b/pype/plugins/premiere/publish/validate_auto_sync_off.py @@ -23,12 +23,7 @@ class ValidateAutoSyncOff(pyblish.api.ContextPlugin): actions = [pype.api.RepairAction] def process(self, context): - session = context.data["ftrackSession"] - project_name = avalon.api.Session["AVALON_PROJECT"] - query = 'Project where full_name is "{}"'.format(project_name) - project = session.query(query).one() invalid = self.get_invalid(context) - assert not invalid, ( "Ftrack Project has 'Auto sync' set to On." " That may cause issues during integration." From 00acee01ce3c5b62d545ed4c58168fe94045aa13 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Jul 2020 16:15:10 +0200 Subject: [PATCH 125/190] get_invalid returns project only if auto sync is turned on --- pype/plugins/premiere/publish/validate_auto_sync_off.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/premiere/publish/validate_auto_sync_off.py b/pype/plugins/premiere/publish/validate_auto_sync_off.py index 53042fef6a..3b7937bbec 100644 --- a/pype/plugins/premiere/publish/validate_auto_sync_off.py +++ b/pype/plugins/premiere/publish/validate_auto_sync_off.py @@ -35,8 +35,8 @@ class ValidateAutoSyncOff(pyblish.api.ContextPlugin): project_name = avalon.api.Session["AVALON_PROJECT"] query = 'Project where full_name is "{}"'.format(project_name) project = session.query(query).one() - - return project + if project["custom_attributes"][CUST_ATTR_AUTO_SYNC]: + return project @classmethod def repair(cls, context): From 7f2d6a7d8518c5205f33fb5d10c98dce3093ef3d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Jul 2020 16:15:52 +0200 Subject: [PATCH 126/190] one more place where constant for custom attribute key is used --- pype/plugins/premiere/publish/validate_auto_sync_off.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/premiere/publish/validate_auto_sync_off.py b/pype/plugins/premiere/publish/validate_auto_sync_off.py index 3b7937bbec..dc4420a9f7 100644 --- a/pype/plugins/premiere/publish/validate_auto_sync_off.py +++ b/pype/plugins/premiere/publish/validate_auto_sync_off.py @@ -42,7 +42,7 @@ class ValidateAutoSyncOff(pyblish.api.ContextPlugin): def repair(cls, context): session = context.data["ftrackSession"] invalid = cls.get_invalid(context) - invalid['custom_attributes']['avalon_auto_sync'] = False + invalid["custom_attributes"][CUST_ATTR_AUTO_SYNC] = False try: session.commit() except Exception: From fdaf486ef5ddbe01862373312878a76b9b92d377 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Jul 2020 16:21:11 +0200 Subject: [PATCH 127/190] added additional check to repair for sure --- pype/plugins/premiere/publish/validate_auto_sync_off.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/plugins/premiere/publish/validate_auto_sync_off.py b/pype/plugins/premiere/publish/validate_auto_sync_off.py index dc4420a9f7..7a5d78795a 100644 --- a/pype/plugins/premiere/publish/validate_auto_sync_off.py +++ b/pype/plugins/premiere/publish/validate_auto_sync_off.py @@ -42,6 +42,10 @@ class ValidateAutoSyncOff(pyblish.api.ContextPlugin): def repair(cls, context): session = context.data["ftrackSession"] invalid = cls.get_invalid(context) + if not invalid: + cls.log.info("Project 'Auto sync' already fixed.") + return + invalid["custom_attributes"][CUST_ATTR_AUTO_SYNC] = False try: session.commit() From e879fe78958be659fadf8315745e3a7da3459a31 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Jul 2020 16:21:25 +0200 Subject: [PATCH 128/190] changed ReparAction to RepairContextAction --- pype/plugins/premiere/publish/validate_auto_sync_off.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/premiere/publish/validate_auto_sync_off.py b/pype/plugins/premiere/publish/validate_auto_sync_off.py index 7a5d78795a..cd6fef29c8 100644 --- a/pype/plugins/premiere/publish/validate_auto_sync_off.py +++ b/pype/plugins/premiere/publish/validate_auto_sync_off.py @@ -20,7 +20,7 @@ class ValidateAutoSyncOff(pyblish.api.ContextPlugin): order = pyblish.api.ValidatorOrder families = ['clip'] label = 'Ftrack project\'s auto sync off' - actions = [pype.api.RepairAction] + actions = [pype.api.RepairContextAction] def process(self, context): invalid = self.get_invalid(context) From 62e296de6d1a986d19ae41a88bf2021ac93a69f4 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Jul 2020 17:52:16 +0100 Subject: [PATCH 129/190] Using PYPE_FTRACK_SOCKET_TIMEOUT variable and default 45. --- pype/modules/ftrack/ftrack_server/socket_thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/ftrack/ftrack_server/socket_thread.py b/pype/modules/ftrack/ftrack_server/socket_thread.py index 3602f2f138..e66e8bc775 100644 --- a/pype/modules/ftrack/ftrack_server/socket_thread.py +++ b/pype/modules/ftrack/ftrack_server/socket_thread.py @@ -11,7 +11,7 @@ from pype.api import Logger class SocketThread(threading.Thread): """Thread that checks suprocess of storer of processor of events""" - MAX_TIMEOUT = 120 + MAX_TIMEOUT = int(os.environ.get("PYPE_FTRACK_SOCKET_TIMEOUT", 45)) def __init__(self, name, port, filepath, additional_args=[]): super(SocketThread, self).__init__() From 40b470b1cf3613dee9580d988f5df533f0c864e7 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 20 Jul 2020 11:08:16 +0200 Subject: [PATCH 130/190] update ffmpeg variable and small maya fix --- .../maya/publish/validate_transform_naming_suffix.py | 4 +--- pype/plugins/photoshop/publish/extract_review.py | 10 ++++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pype/plugins/maya/publish/validate_transform_naming_suffix.py b/pype/plugins/maya/publish/validate_transform_naming_suffix.py index 17066f6b12..120123af4b 100644 --- a/pype/plugins/maya/publish/validate_transform_naming_suffix.py +++ b/pype/plugins/maya/publish/validate_transform_naming_suffix.py @@ -103,9 +103,7 @@ class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin): instance (:class:`pyblish.api.Instance`): published instance. """ - invalid = self.get_invalid(instance, - self.SUFFIX_NAMING_TABLE, - self.ALLOW_IF_NOT_IN_SUFFIX_TABLE) + invalid = self.get_invalid(instance) if invalid: raise ValueError("Incorrectly named geometry " "transforms: {0}".format(invalid)) diff --git a/pype/plugins/photoshop/publish/extract_review.py b/pype/plugins/photoshop/publish/extract_review.py index 49e932eb67..d784dc0998 100644 --- a/pype/plugins/photoshop/publish/extract_review.py +++ b/pype/plugins/photoshop/publish/extract_review.py @@ -42,6 +42,8 @@ class ExtractReview(pype.api.Extractor): staging_dir, photoshop.com_objects.JPEGSaveOptions(), True ) + ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") + instance.data["representations"].append({ "name": "jpg", "ext": "jpg", @@ -53,13 +55,13 @@ class ExtractReview(pype.api.Extractor): # Generate thumbnail. thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg") args = [ - "ffmpeg", "-y", + ffmpeg_path, "-y", "-i", os.path.join(staging_dir, output_image), "-vf", "scale=300:-1", "-vframes", "1", thumbnail_path ] - output = pype.lib._subprocess(args, cwd=os.environ["FFMPEG_PATH"]) + output = pype.lib._subprocess(args) self.log.debug(output) @@ -74,12 +76,12 @@ class ExtractReview(pype.api.Extractor): # Generate mov. mov_path = os.path.join(staging_dir, "review.mov") args = [ - "ffmpeg", "-y", + ffmpeg_path, "-y", "-i", os.path.join(staging_dir, output_image), "-vframes", "1", mov_path ] - output = pype.lib._subprocess(args, cwd=os.environ["FFMPEG_PATH"]) + output = pype.lib._subprocess(args) self.log.debug(output) From d7883fe561238073e59fde455b2d7845ec7ad7aa Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 20 Jul 2020 11:21:47 +0100 Subject: [PATCH 131/190] Use published workfile for rendering. --- .../nuke/publish/submit_nuke_deadline.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py index 2b8efb4640..26d3f9b571 100644 --- a/pype/plugins/nuke/publish/submit_nuke_deadline.py +++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py @@ -49,6 +49,24 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): render_path = instance.data['path'] script_path = context.data["currentFile"] + for item in context: + if "workfile" in item.data["families"]: + msg = "Workfile (scene) must be published along" + assert item.data["publish"] is True, msg + + template_data = item.data.get("anatomyData") + rep = item.data.get("representations")[0].get("name") + template_data["representation"] = rep + template_data["ext"] = rep + template_data["comment"] = None + anatomy_filled = context.data["anatomy"].format(template_data) + template_filled = anatomy_filled["publish"]["path"] + script_path = os.path.normpath(template_filled) + + self.log.info( + "Using published scene for render {}".format(script_path) + ) + # exception for slate workflow if "slate" in instance.data["families"]: self._frame_start -= 1 From fa91da78ee6956d04e2f3d1ca412d8ef933f622b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Jul 2020 13:00:29 +0200 Subject: [PATCH 132/190] optional plugins with active attribute set to False are skipped --- pype/tools/pyblish_pype/control.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pype/tools/pyblish_pype/control.py b/pype/tools/pyblish_pype/control.py index 5138b5cc4c..77badf71b6 100644 --- a/pype/tools/pyblish_pype/control.py +++ b/pype/tools/pyblish_pype/control.py @@ -183,7 +183,18 @@ class Controller(QtCore.QObject): plugins = pyblish.api.discover() targets = pyblish.logic.registered_targets() or ["default"] - self.plugins = pyblish.logic.plugins_by_targets(plugins, targets) + plugins_by_targets = pyblish.logic.plugins_by_targets(plugins, targets) + + _plugins = [] + for plugin in plugins_by_targets: + # Skip plugin if is not optional and not active + if ( + not getattr(plugin, "optional", False) + and not getattr(plugin, "active", True) + ): + continue + _plugins.append(plugin) + self.plugins = _plugins def on_published(self): if self.is_running: From 059082a671cf414c028d64cc190b9a03272ae7e3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Jul 2020 11:55:27 +0200 Subject: [PATCH 133/190] fix(ppro): audio only was not working --- pype/hosts/premiere/extensions/com.pype/jsx/pype.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/hosts/premiere/extensions/com.pype/jsx/pype.jsx b/pype/hosts/premiere/extensions/com.pype/jsx/pype.jsx index 684cef5e5a..3cd4502653 100644 --- a/pype/hosts/premiere/extensions/com.pype/jsx/pype.jsx +++ b/pype/hosts/premiere/extensions/com.pype/jsx/pype.jsx @@ -534,7 +534,9 @@ $.pype = { if (instances === null) { return null; } - if (audioOnly === true) { + + // make only audio representations + if (audioOnly === 'true') { $.pype.log('? looping if audio True'); for (var i = 0; i < instances.length; i++) { var subsetToRepresentations = instances[i].subsetToRepresentations; From 19a33b7272843c06a592a3fe044dc82fb806ff9e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Jul 2020 12:08:23 +0200 Subject: [PATCH 134/190] feat(ppro): synchronization of ftrack project back on --- .../publish/integrate_auto_sync_back_on.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 pype/plugins/premiere/publish/integrate_auto_sync_back_on.py diff --git a/pype/plugins/premiere/publish/integrate_auto_sync_back_on.py b/pype/plugins/premiere/publish/integrate_auto_sync_back_on.py new file mode 100644 index 0000000000..ca7151d3d3 --- /dev/null +++ b/pype/plugins/premiere/publish/integrate_auto_sync_back_on.py @@ -0,0 +1,35 @@ +import sys +import pyblish.api +import avalon.api + +try: + from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_AUTO_SYNC +except Exception: + CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" + + +class IntegrateAutoSyncBackOn(pyblish.api.ContextPlugin): + """Ensure that autosync value in ftrack project is set to True. + + In case was set to False and event server with the sync to avalon event + is not running this will set synchronization back on. + """ + + order = pyblish.api.IntegratorOrder + 1 + families = ['clip'] + label = 'Ftrack project\'s auto sync on' + + def process(self, context): + session = context.data["ftrackSession"] + project_name = avalon.api.Session["AVALON_PROJECT"] + query = 'Project where full_name is "{}"'.format(project_name) + project = session.query(query).one() + if not project["custom_attributes"][CUST_ATTR_AUTO_SYNC]: + project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = True + + try: + session.commit() + except Exception: + tp, value, tb = sys.exc_info() + session.rollback() + raise From 60a9b6bface827b1190d8108f55398b59eab16d2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Jul 2020 14:27:55 +0200 Subject: [PATCH 135/190] feat(ftrack): auto-sync switcher moved to frack hierarchy integrator --- .../publish/integrate_hierarchy_ftrack.py | 49 ++++++++++++++++- .../publish/integrate_auto_sync_back_on.py | 35 ------------ .../publish/validate_auto_sync_off.py | 55 ------------------- 3 files changed, 47 insertions(+), 92 deletions(-) delete mode 100644 pype/plugins/premiere/publish/integrate_auto_sync_back_on.py delete mode 100644 pype/plugins/premiere/publish/validate_auto_sync_off.py diff --git a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py index a12fdfd36c..7a43daf781 100644 --- a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py +++ b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py @@ -1,9 +1,13 @@ import sys - import six import pyblish.api from avalon import io +try: + from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_AUTO_SYNC +except Exception: + CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" + class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): """ @@ -47,7 +51,16 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): input_data = context.data["hierarchyContext"] - self.import_to_ftrack(input_data) + # disable termporarily ftrack project's autosyncing + self.auto_sync_off(context) + + try: + # import ftrack hierarchy + self.import_to_ftrack(input_data) + except Exception: + raise + finally: + self.auto_sync_on() def import_to_ftrack(self, input_data, parent=None): for entity_name in input_data: @@ -217,3 +230,35 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): six.reraise(tp, value, tb) return entity + + def auto_sync_off(self, context): + project_name = context.data["projectEntity"]["name"] + query = 'Project where full_name is "{}"'.format(project_name) + self.project = self.session.query(query).one() + self.auto_sync_state = self.project[ + "custom_attributes"][CUST_ATTR_AUTO_SYNC] + + self.project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = False + + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + raise + + self.log.info("Ftrack autosync swithed off") + + def auto_sync_on(self): + if not self.project[ + "custom_attributes"][CUST_ATTR_AUTO_SYNC] \ + and self.auto_sync_state: + self.project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = True + self.log.info("Ftrack autosync swithed on") + + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + raise diff --git a/pype/plugins/premiere/publish/integrate_auto_sync_back_on.py b/pype/plugins/premiere/publish/integrate_auto_sync_back_on.py deleted file mode 100644 index ca7151d3d3..0000000000 --- a/pype/plugins/premiere/publish/integrate_auto_sync_back_on.py +++ /dev/null @@ -1,35 +0,0 @@ -import sys -import pyblish.api -import avalon.api - -try: - from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_AUTO_SYNC -except Exception: - CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" - - -class IntegrateAutoSyncBackOn(pyblish.api.ContextPlugin): - """Ensure that autosync value in ftrack project is set to True. - - In case was set to False and event server with the sync to avalon event - is not running this will set synchronization back on. - """ - - order = pyblish.api.IntegratorOrder + 1 - families = ['clip'] - label = 'Ftrack project\'s auto sync on' - - def process(self, context): - session = context.data["ftrackSession"] - project_name = avalon.api.Session["AVALON_PROJECT"] - query = 'Project where full_name is "{}"'.format(project_name) - project = session.query(query).one() - if not project["custom_attributes"][CUST_ATTR_AUTO_SYNC]: - project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = True - - try: - session.commit() - except Exception: - tp, value, tb = sys.exc_info() - session.rollback() - raise diff --git a/pype/plugins/premiere/publish/validate_auto_sync_off.py b/pype/plugins/premiere/publish/validate_auto_sync_off.py deleted file mode 100644 index cd6fef29c8..0000000000 --- a/pype/plugins/premiere/publish/validate_auto_sync_off.py +++ /dev/null @@ -1,55 +0,0 @@ -import sys -import pyblish.api -import pype.api -import avalon.api - -try: - from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_AUTO_SYNC -except Exception: - CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" - - -class ValidateAutoSyncOff(pyblish.api.ContextPlugin): - """Ensure that autosync value in ftrack project is set to False. - - In case was set to True and event server with the sync to avalon event - is running will cause integration to avalon will be override. - - """ - - order = pyblish.api.ValidatorOrder - families = ['clip'] - label = 'Ftrack project\'s auto sync off' - actions = [pype.api.RepairContextAction] - - def process(self, context): - invalid = self.get_invalid(context) - assert not invalid, ( - "Ftrack Project has 'Auto sync' set to On." - " That may cause issues during integration." - ) - - @staticmethod - def get_invalid(context): - session = context.data["ftrackSession"] - project_name = avalon.api.Session["AVALON_PROJECT"] - query = 'Project where full_name is "{}"'.format(project_name) - project = session.query(query).one() - if project["custom_attributes"][CUST_ATTR_AUTO_SYNC]: - return project - - @classmethod - def repair(cls, context): - session = context.data["ftrackSession"] - invalid = cls.get_invalid(context) - if not invalid: - cls.log.info("Project 'Auto sync' already fixed.") - return - - invalid["custom_attributes"][CUST_ATTR_AUTO_SYNC] = False - try: - session.commit() - except Exception: - tp, value, tb = sys.exc_info() - session.rollback() - raise From 45e2de4d08227ebd1634f1cde8bdf2d7bb79dbde Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Jul 2020 14:35:48 +0200 Subject: [PATCH 136/190] fix(ftrack): refactoring code --- .../publish/integrate_hierarchy_ftrack.py | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py index 7a43daf781..05908cb2da 100644 --- a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py +++ b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py @@ -43,6 +43,12 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): if "hierarchyContext" not in context.data: return + project_name = context.data["projectEntity"]["name"] + query = 'Project where full_name is "{}"'.format(project_name) + project = self.session.query(query).one() + auto_sync_state = project[ + "custom_attributes"][CUST_ATTR_AUTO_SYNC] + if not io.Session: io.install() @@ -52,7 +58,8 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): input_data = context.data["hierarchyContext"] # disable termporarily ftrack project's autosyncing - self.auto_sync_off(context) + if auto_sync_state: + self.auto_sync_off(project) try: # import ftrack hierarchy @@ -60,7 +67,8 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): except Exception: raise finally: - self.auto_sync_on() + if auto_sync_state: + self.auto_sync_on(project) def import_to_ftrack(self, input_data, parent=None): for entity_name in input_data: @@ -231,30 +239,23 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): return entity - def auto_sync_off(self, context): - project_name = context.data["projectEntity"]["name"] - query = 'Project where full_name is "{}"'.format(project_name) - self.project = self.session.query(query).one() - self.auto_sync_state = self.project[ - "custom_attributes"][CUST_ATTR_AUTO_SYNC] - - self.project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = False - - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - raise + def auto_sync_off(self, project): + project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = False self.log.info("Ftrack autosync swithed off") - def auto_sync_on(self): - if not self.project[ - "custom_attributes"][CUST_ATTR_AUTO_SYNC] \ - and self.auto_sync_state: - self.project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = True - self.log.info("Ftrack autosync swithed on") + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + raise + + def auto_sync_on(self, project): + + project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = True + + self.log.info("Ftrack autosync swithed on") try: self.session.commit() From eb28ba7a5f90f746b7835fdec97b2d957cf14089 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Jul 2020 14:40:56 +0200 Subject: [PATCH 137/190] feat(ftrack): fix session missing --- pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py index 05908cb2da..a0059c55a6 100644 --- a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py +++ b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py @@ -43,7 +43,8 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): if "hierarchyContext" not in context.data: return - project_name = context.data["projectEntity"]["name"] + self.session = self.context.data["ftrackSession"] + project_name = self.context.data["projectEntity"]["name"] query = 'Project where full_name is "{}"'.format(project_name) project = self.session.query(query).one() auto_sync_state = project[ @@ -53,7 +54,6 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): io.install() self.ft_project = None - self.session = context.data["ftrackSession"] input_data = context.data["hierarchyContext"] From ad25719c79802fc966a801dc5b965d5ef91f8a2e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Jul 2020 17:04:16 +0200 Subject: [PATCH 138/190] minor preparetion tweaks --- pype/modules/ftrack/lib/ftrack_app_handler.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pype/modules/ftrack/lib/ftrack_app_handler.py b/pype/modules/ftrack/lib/ftrack_app_handler.py index efc2df0ea5..6a40c1a382 100644 --- a/pype/modules/ftrack/lib/ftrack_app_handler.py +++ b/pype/modules/ftrack/lib/ftrack_app_handler.py @@ -142,6 +142,9 @@ class AppAction(BaseAction): """ entity = entities[0] + + task_name = entity["name"] + project_name = entity["project"]["full_name"] database = pypelib.get_avalon_database() @@ -164,7 +167,7 @@ class AppAction(BaseAction): "name": entity["project"]["full_name"], "code": entity["project"]["name"] }, - "task": entity["name"], + "task": task_name, "asset": asset_name, "app": host_name, "hierarchy": hierarchy @@ -210,8 +213,8 @@ class AppAction(BaseAction): prep_env.update({ "AVALON_PROJECT": project_name, "AVALON_ASSET": asset_name, - "AVALON_TASK": entity["name"], - "AVALON_APP": self.identifier.split("_")[0], + "AVALON_TASK": task_name, + "AVALON_APP": host_name, "AVALON_APP_NAME": self.identifier, "AVALON_HIERARCHY": hierarchy, "AVALON_WORKDIR": workdir From d758d93b507b32fbac56bc82763105419f6bee84 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Jul 2020 17:05:01 +0200 Subject: [PATCH 139/190] ftrack app handler is using `should_start_last_workfile` from avalon and sets variables the same way --- pype/modules/ftrack/lib/ftrack_app_handler.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pype/modules/ftrack/lib/ftrack_app_handler.py b/pype/modules/ftrack/lib/ftrack_app_handler.py index 6a40c1a382..7ba7ad3f86 100644 --- a/pype/modules/ftrack/lib/ftrack_app_handler.py +++ b/pype/modules/ftrack/lib/ftrack_app_handler.py @@ -8,7 +8,9 @@ import getpass from pype import lib as pypelib from pype.api import config, Anatomy from .ftrack_action_handler import BaseAction -from avalon.api import last_workfile, HOST_WORKFILE_EXTENSIONS +from avalon.api import ( + last_workfile, HOST_WORKFILE_EXTENSIONS, should_start_last_workfile +) class AppAction(BaseAction): @@ -219,8 +221,22 @@ class AppAction(BaseAction): "AVALON_HIERARCHY": hierarchy, "AVALON_WORKDIR": workdir }) - if last_workfile_path and os.path.exists(last_workfile_path): + + start_last_workfile = should_start_last_workfile( + project_name, host_name, task_name + ) + # Store boolean as "0"(False) or "1"(True) + prep_env["AVALON_OPEN_LAST_WORKFILE"] = ( + str(int(bool(start_last_workfile))) + ) + + if ( + start_last_workfile + and last_workfile_path + and os.path.exists(last_workfile_path) + ): prep_env["AVALON_LAST_WORKFILE"] = last_workfile_path + prep_env.update(anatomy.roots_obj.root_environments()) # collect all parents from the task From c6f5eb6b4ad1045ae7a981b4e15f014e1ecddef6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 14:58:51 +0200 Subject: [PATCH 140/190] removed icon attribute from server's sync to avalon action --- pype/modules/ftrack/events/action_sync_to_avalon.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/pype/modules/ftrack/events/action_sync_to_avalon.py b/pype/modules/ftrack/events/action_sync_to_avalon.py index a06b825d6a..4e119228c3 100644 --- a/pype/modules/ftrack/events/action_sync_to_avalon.py +++ b/pype/modules/ftrack/events/action_sync_to_avalon.py @@ -1,10 +1,8 @@ -import os import time import traceback from pype.modules.ftrack import BaseAction from pype.modules.ftrack.lib.avalon_sync import SyncEntitiesFactory -from pype.api import config class SyncToAvalonServer(BaseAction): @@ -38,17 +36,6 @@ class SyncToAvalonServer(BaseAction): variant = "- Sync To Avalon (Server)" #: Action description. description = "Send data from Ftrack to Avalon" - #: Action icon. - icon = "{}/ftrack/action_icons/PypeAdmin.svg".format( - os.environ.get( - "PYPE_STATICS_SERVER", - "http://localhost:{}".format( - config.get_presets().get("services", {}).get( - "rest_api", {} - ).get("default_port", 8021) - ) - ) - ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) From 0fc014e12f151f326f8a3e6b0cf71a3ab4b23996 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 16:49:48 +0200 Subject: [PATCH 141/190] add contants to clockify module --- pype/modules/clockify/constants.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 pype/modules/clockify/constants.py diff --git a/pype/modules/clockify/constants.py b/pype/modules/clockify/constants.py new file mode 100644 index 0000000000..5603bbc641 --- /dev/null +++ b/pype/modules/clockify/constants.py @@ -0,0 +1,17 @@ +import os +import appdirs + + +CLOCKIFY_FTRACK_SERVER_PATH = os.path.join( + os.path.dirname(__file__), "ftrack", "server" +) +CLOCKIFY_FTRACK_USER_PATH = os.path.join( + os.path.dirname(__file__), "ftrack", "user" +) +CREDENTIALS_PATH = os.path.normpath(os.path.join( + appdirs.user_data_dir("pype-app", "pype"), + "clockify.json" +)) + +ADMIN_PERMISSION_NAMES = ["WORKSPACE_OWN", "WORKSPACE_ADMIN"] +CLOCKIFY_ENDPOINT = "https://api.clockify.me/api/" From 7635c680170cf1b162a7314771021ffb07bfa126 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 16:50:37 +0200 Subject: [PATCH 142/190] moved clockify action to ftrack/user/ structure --- .../{ftrack_actions => ftrack/user}/action_clockify_sync.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pype/modules/clockify/{ftrack_actions => ftrack/user}/action_clockify_sync.py (100%) diff --git a/pype/modules/clockify/ftrack_actions/action_clockify_sync.py b/pype/modules/clockify/ftrack/user/action_clockify_sync.py similarity index 100% rename from pype/modules/clockify/ftrack_actions/action_clockify_sync.py rename to pype/modules/clockify/ftrack/user/action_clockify_sync.py From a021deb5635106ec64921d6d1191292ad8ba6856 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 16:50:56 +0200 Subject: [PATCH 143/190] created copy of clockify sync action to server folder --- .../ftrack/server/action_clockify_sync.py | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 pype/modules/clockify/ftrack/server/action_clockify_sync.py diff --git a/pype/modules/clockify/ftrack/server/action_clockify_sync.py b/pype/modules/clockify/ftrack/server/action_clockify_sync.py new file mode 100644 index 0000000000..0ba4c3a265 --- /dev/null +++ b/pype/modules/clockify/ftrack/server/action_clockify_sync.py @@ -0,0 +1,122 @@ +import json +from pype.modules.ftrack.lib import BaseAction, statics_icon +from pype.modules.clockify import ClockifyAPI + + +class SyncClocify(BaseAction): + '''Synchronise project names and task types.''' + + #: Action identifier. + identifier = 'clockify.sync' + #: Action label. + label = 'Sync To Clockify' + #: Action description. + description = 'Synchronise data to Clockify workspace' + #: roles that are allowed to register this action + role_list = ["Pypeclub", "Administrator", "project Manager"] + #: icon + icon = statics_icon("app_icons", "clockify-white.png") + + #: CLockifyApi + clockapi = ClockifyAPI() + + def discover(self, session, entities, event): + if ( + len(entities) == 1 + and entities[0].entity_type.lower() == "project" + ): + return True + return False + + def launch(self, session, entities, event): + self.clockapi.set_api() + if self.clockapi.workspace_id is None: + return { + "success": False, + "message": "Clockify Workspace or API key are not set!" + } + + if self.clockapi.validate_workspace_perm() is False: + return { + "success": False, + "message": "Missing permissions for this action!" + } + + # JOB SETTINGS + userId = event['source']['user']['id'] + user = session.query('User where id is ' + userId).one() + + job = session.create('Job', { + 'user': user, + 'status': 'running', + 'data': json.dumps({ + 'description': 'Sync Ftrack to Clockify' + }) + }) + session.commit() + + project_entity = entities[0] + if project_entity.entity_type.lower() != "project": + project_entity = self.get_project_from_entity(project_entity) + + project_name = project_entity["full_name"] + self.log.info( + "Synchronization of project \"{}\" to clockify begins.".format( + project_name + ) + ) + task_types = ( + project_entity["project_schema"]["_task_type_schema"]["types"] + ) + task_type_names = [ + task_type["name"] for task_type in task_types + ] + try: + clockify_projects = self.clockapi.get_projects() + if project_name not in clockify_projects: + response = self.clockapi.add_project(project_name) + if "id" not in response: + self.log.warning( + "Project \"{}\" can't be created. Response: {}".format( + project_name, response + ) + ) + return { + "success": False, + "message": ( + "Can't create clockify project \"{}\"." + " Unexpected error." + ).format(project_name) + } + + clockify_workspace_tags = self.clockapi.get_tags() + for task_type_name in task_type_names: + if task_type_name in clockify_workspace_tags: + self.log.debug( + "Task \"{}\" already exist".format(task_type_name) + ) + continue + + response = self.clockapi.add_tag(task_type_name) + if "id" not in response: + self.log.warning( + "Task \"{}\" can't be created. Response: {}".format( + task_type_name, response + ) + ) + + job["status"] = "done" + + except Exception: + pass + + finally: + if job["status"] != "done": + job["status"] = "failed" + session.commit() + + return True + + +def register(session, **kw): + SyncClocify(session).register() From 433a67b2dacf4e1d9c29e90cebc698af6c14ae65 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 16:52:56 +0200 Subject: [PATCH 144/190] ClockifyApi is not singleton anymore --- pype/modules/clockify/clockify_api.py | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/pype/modules/clockify/clockify_api.py b/pype/modules/clockify/clockify_api.py index 86365a9352..d8df2d8990 100644 --- a/pype/modules/clockify/clockify_api.py +++ b/pype/modules/clockify/clockify_api.py @@ -6,29 +6,7 @@ import datetime import appdirs -class Singleton(type): - _instances = {} - - def __call__(cls, *args, **kwargs): - if cls not in cls._instances: - cls._instances[cls] = super( - Singleton, cls - ).__call__(*args, **kwargs) - return cls._instances[cls] - - -class ClockifyAPI(metaclass=Singleton): - endpoint = "https://api.clockify.me/api/" - headers = {"X-Api-Key": None} - app_dir = os.path.normpath(appdirs.user_data_dir('pype-app', 'pype')) - file_name = 'clockify.json' - fpath = os.path.join(app_dir, file_name) - admin_permission_names = ['WORKSPACE_OWN', 'WORKSPACE_ADMIN'] - master_parent = None - workspace = None - workspace_id = None - - def set_master(self, master_parent): +class ClockifyAPI: self.master_parent = master_parent def verify_api(self): From b793e8bd79ce4d58031d45987c32cfcde9f65a58 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 16:55:57 +0200 Subject: [PATCH 145/190] using endpoint constant in clockify api --- pype/modules/clockify/clockify_api.py | 38 +++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/pype/modules/clockify/clockify_api.py b/pype/modules/clockify/clockify_api.py index d8df2d8990..eafe95e3bd 100644 --- a/pype/modules/clockify/clockify_api.py +++ b/pype/modules/clockify/clockify_api.py @@ -3,7 +3,7 @@ import re import requests import json import datetime -import appdirs +from . import CLOCKIFY_ENDPOINT class ClockifyAPI: @@ -31,7 +31,7 @@ class ClockifyAPI: test_headers = {'X-Api-Key': api_key} action_url = 'workspaces/' response = requests.get( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=test_headers ) if response.status_code != 200: @@ -48,7 +48,7 @@ class ClockifyAPI: workspace_id, user_id ) response = requests.get( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=self.headers ) user_permissions = response.json() @@ -60,7 +60,7 @@ class ClockifyAPI: def get_user_id(self): action_url = 'v1/user/' response = requests.get( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=self.headers ) # this regex is neccessary: UNICODE strings are crashing @@ -120,7 +120,7 @@ class ClockifyAPI: def get_workspaces(self): action_url = 'workspaces/' response = requests.get( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=self.headers ) return { @@ -132,7 +132,7 @@ class ClockifyAPI: workspace_id = self.workspace_id action_url = 'workspaces/{}/projects/'.format(workspace_id) response = requests.get( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=self.headers ) @@ -147,7 +147,7 @@ class ClockifyAPI: workspace_id, project_id ) response = requests.get( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=self.headers ) @@ -158,7 +158,7 @@ class ClockifyAPI: workspace_id = self.workspace_id action_url = 'workspaces/{}/tags/'.format(workspace_id) response = requests.get( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=self.headers ) @@ -173,7 +173,7 @@ class ClockifyAPI: workspace_id, project_id ) response = requests.get( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=self.headers ) @@ -255,7 +255,7 @@ class ClockifyAPI: "tagIds": tag_ids } response = requests.post( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body ) @@ -272,7 +272,7 @@ class ClockifyAPI: workspace_id ) response = requests.get( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=self.headers ) try: @@ -302,7 +302,7 @@ class ClockifyAPI: "end": self.get_current_time() } response = requests.put( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body ) @@ -315,7 +315,7 @@ class ClockifyAPI: workspace_id = self.workspace_id action_url = 'workspaces/{}/timeEntries/'.format(workspace_id) response = requests.get( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=self.headers ) return response.json()[:quantity] @@ -327,7 +327,7 @@ class ClockifyAPI: workspace_id, tid ) response = requests.delete( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=self.headers ) return response.json() @@ -348,7 +348,7 @@ class ClockifyAPI: "billable": "true" } response = requests.post( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body ) @@ -358,7 +358,7 @@ class ClockifyAPI: action_url = 'workspaces/' body = {"name": name} response = requests.post( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body ) @@ -377,7 +377,7 @@ class ClockifyAPI: "projectId": project_id } response = requests.post( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body ) @@ -391,7 +391,7 @@ class ClockifyAPI: "name": name } response = requests.post( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body ) @@ -406,7 +406,7 @@ class ClockifyAPI: workspace_id, project_id ) response = requests.delete( - self.endpoint + action_url, + CLOCKIFY_ENDPOINT + action_url, headers=self.headers, ) return response.json() From a0eba1199bbb27e46b4efe26903a44f3c3b70342 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 16:58:10 +0200 Subject: [PATCH 146/190] added more constants in usage --- pype/modules/clockify/clockify_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pype/modules/clockify/clockify_api.py b/pype/modules/clockify/clockify_api.py index eafe95e3bd..c4eb3bbba0 100644 --- a/pype/modules/clockify/clockify_api.py +++ b/pype/modules/clockify/clockify_api.py @@ -53,7 +53,7 @@ class ClockifyAPI: ) user_permissions = response.json() for perm in user_permissions: - if perm['name'] in self.admin_permission_names: + if perm['name'] in ADMIN_PERMISSION_NAMES: return True return False @@ -102,18 +102,18 @@ class ClockifyAPI: def get_api_key(self): api_key = None try: - file = open(self.fpath, 'r') + file = open(CREDENTIALS_JSON_PATH, 'r') api_key = json.load(file).get('api_key', None) if api_key == '': api_key = None except Exception: - file = open(self.fpath, 'w') + file = open(CREDENTIALS_JSON_PATH, 'w') file.close() return api_key def save_api_key(self, api_key): data = {'api_key': api_key} - file = open(self.fpath, 'w') + file = open(CREDENTIALS_JSON_PATH, 'w') file.write(json.dumps(data)) file.close() From 50a1545d0f8cb6a811c5f2e9c895e895b1ebc31c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 16:58:29 +0200 Subject: [PATCH 147/190] removed attributes are in init now --- pype/modules/clockify/clockify_api.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pype/modules/clockify/clockify_api.py b/pype/modules/clockify/clockify_api.py index c4eb3bbba0..8ebed302fe 100644 --- a/pype/modules/clockify/clockify_api.py +++ b/pype/modules/clockify/clockify_api.py @@ -3,11 +3,15 @@ import re import requests import json import datetime -from . import CLOCKIFY_ENDPOINT +from . import CLOCKIFY_ENDPOINT, ADMIN_PERMISSION_NAMES, CREDENTIALS_JSON_PATH class ClockifyAPI: + def __init__(self, workspace_name=None, api_key=None, master_parent=None): + self.workspace_name = workspace_name self.master_parent = master_parent + self.workspace_id = None + self.headers = {"X-Api-Key": api_key} def verify_api(self): for key, value in self.headers.items(): @@ -76,9 +80,9 @@ class ClockifyAPI: def set_workspace(self, name=None): if name is None: name = os.environ.get('CLOCKIFY_WORKSPACE', None) - self.workspace = name + self.workspace_name = name self.workspace_id = None - if self.workspace is None: + if self.workspace_name is None: return try: result = self.validate_workspace() @@ -93,7 +97,7 @@ class ClockifyAPI: def validate_workspace(self, name=None): if name is None: - name = self.workspace + name = self.workspace_name all_workspaces = self.get_workspaces() if name in all_workspaces: return all_workspaces[name] From 562fabe94df095379104bd67a4b48127b71b038d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 16:58:53 +0200 Subject: [PATCH 148/190] added constants to clockify init --- pype/modules/clockify/__init__.py | 7 +++++++ pype/modules/clockify/constants.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pype/modules/clockify/__init__.py b/pype/modules/clockify/__init__.py index 0ee2189fa5..0ea58b5b00 100644 --- a/pype/modules/clockify/__init__.py +++ b/pype/modules/clockify/__init__.py @@ -1,3 +1,10 @@ +from .constants import ( + CLOCKIFY_ENDPOINT, + ADMIN_PERMISSION_NAMES, + CREDENTIALS_JSON_PATH, + CLOCKIFY_FTRACK_USER_PATH, + CLOCKIFY_FTRACK_SERVER_PATH +) from .clockify_api import ClockifyAPI from .widget_settings import ClockifySettings from .widget_message import MessageWidget diff --git a/pype/modules/clockify/constants.py b/pype/modules/clockify/constants.py index 5603bbc641..38ad4b64cf 100644 --- a/pype/modules/clockify/constants.py +++ b/pype/modules/clockify/constants.py @@ -8,7 +8,7 @@ CLOCKIFY_FTRACK_SERVER_PATH = os.path.join( CLOCKIFY_FTRACK_USER_PATH = os.path.join( os.path.dirname(__file__), "ftrack", "user" ) -CREDENTIALS_PATH = os.path.normpath(os.path.join( +CREDENTIALS_JSON_PATH = os.path.normpath(os.path.join( appdirs.user_data_dir("pype-app", "pype"), "clockify.json" )) From 0de373f94fd19bcc0421edad3918862a1377d0ba Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 16:59:46 +0200 Subject: [PATCH 149/190] formatting fix --- pype/modules/clockify/clockify.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pype/modules/clockify/clockify.py b/pype/modules/clockify/clockify.py index 02b322c1c6..247bdaf0b1 100644 --- a/pype/modules/clockify/clockify.py +++ b/pype/modules/clockify/clockify.py @@ -195,9 +195,10 @@ class ClockifyModule: ).format(project_name)) msg = ( - "Project \"{}\" is not in Clockify Workspace \"{}\"." + "Project \"{}\" is not" + " in Clockify Workspace \"{}\"." "

Please inform your Project Manager." - ).format(project_name, str(self.clockapi.workspace)) + ).format(project_name, str(self.clockapi.workspace_name)) self.message_widget = MessageWidget( self.main_parent, msg, "Clockify - Info Message" From eeb5b54cef05c91cc610e68a0406d6f022fd3410 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 17:00:35 +0200 Subject: [PATCH 150/190] using constant in clockify module --- pype/modules/clockify/clockify.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pype/modules/clockify/clockify.py b/pype/modules/clockify/clockify.py index 247bdaf0b1..5191727b33 100644 --- a/pype/modules/clockify/clockify.py +++ b/pype/modules/clockify/clockify.py @@ -3,11 +3,12 @@ import threading from pype.api import Logger from avalon import style from Qt import QtWidgets -from . import ClockifySettings, ClockifyAPI, MessageWidget +from . import ( + ClockifySettings, ClockifyAPI, MessageWidget, CLOCKIFY_FTRACK_USER_PATH +) class ClockifyModule: - workspace_name = None def __init__(self, main_parent=None, parent=None): @@ -50,14 +51,12 @@ class ClockifyModule: def process_modules(self, modules): if 'FtrackModule' in modules: - actions_path = os.path.sep.join([ - os.path.dirname(__file__), - 'ftrack_actions' - ]) current = os.environ.get('FTRACK_ACTIONS_PATH', '') if current: current += os.pathsep - os.environ['FTRACK_ACTIONS_PATH'] = current + actions_path + os.environ['FTRACK_ACTIONS_PATH'] = ( + current + CLOCKIFY_FTRACK_USER_PATH + ) if 'AvalonApps' in modules: from launcher import lib From 774f7953dd9b0e488f738b005370ba4334bc6b38 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 17:00:57 +0200 Subject: [PATCH 151/190] clockify module can handle new clockify api --- pype/modules/clockify/clockify.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pype/modules/clockify/clockify.py b/pype/modules/clockify/clockify.py index 5191727b33..4c99fb8a15 100644 --- a/pype/modules/clockify/clockify.py +++ b/pype/modules/clockify/clockify.py @@ -21,7 +21,7 @@ class ClockifyModule: self.main_parent = main_parent self.parent = parent - self.clockapi = ClockifyAPI() + self.clockapi = ClockifyAPI(master_parent=self) self.message_widget = None self.widget_settings = ClockifySettings(main_parent, self) self.widget_settings_required = None @@ -32,8 +32,6 @@ class ClockifyModule: self.bool_api_key_set = False self.bool_workspace_set = False self.bool_timer_run = False - - self.clockapi.set_master(self) self.bool_api_key_set = self.clockapi.set_api() def tray_start(self): From 489bed82a6b83bbc9ea7c6bf4631d974e894ca44 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 17:02:18 +0200 Subject: [PATCH 152/190] sync to clockify changed label --- pype/modules/clockify/ftrack/user/action_clockify_sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/clockify/ftrack/user/action_clockify_sync.py b/pype/modules/clockify/ftrack/user/action_clockify_sync.py index 0ba4c3a265..50ec455b13 100644 --- a/pype/modules/clockify/ftrack/user/action_clockify_sync.py +++ b/pype/modules/clockify/ftrack/user/action_clockify_sync.py @@ -9,7 +9,7 @@ class SyncClocify(BaseAction): #: Action identifier. identifier = 'clockify.sync' #: Action label. - label = 'Sync To Clockify' + label = 'Sync To Clockify (local)' #: Action description. description = 'Synchronise data to Clockify workspace' #: roles that are allowed to register this action From c840f31b598fbca6f0a6aa66499bb99316b3afa2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 17:06:12 +0200 Subject: [PATCH 153/190] renamed clockify action file name --- .../{action_clockify_sync.py => action_clockify_sync_local.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename pype/modules/clockify/ftrack/user/{action_clockify_sync.py => action_clockify_sync_local.py} (99%) diff --git a/pype/modules/clockify/ftrack/user/action_clockify_sync.py b/pype/modules/clockify/ftrack/user/action_clockify_sync_local.py similarity index 99% rename from pype/modules/clockify/ftrack/user/action_clockify_sync.py rename to pype/modules/clockify/ftrack/user/action_clockify_sync_local.py index 50ec455b13..a7385c4774 100644 --- a/pype/modules/clockify/ftrack/user/action_clockify_sync.py +++ b/pype/modules/clockify/ftrack/user/action_clockify_sync_local.py @@ -7,7 +7,7 @@ class SyncClocify(BaseAction): '''Synchronise project names and task types.''' #: Action identifier. - identifier = 'clockify.sync' + identifier = 'clockify.sync.local' #: Action label. label = 'Sync To Clockify (local)' #: Action description. From e97000b26d3a7a8c7c2fac022168b5cf03b87aa1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 17:06:39 +0200 Subject: [PATCH 154/190] renamed server action --- .../{action_clockify_sync.py => action_clockify_sync_server.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pype/modules/clockify/ftrack/server/{action_clockify_sync.py => action_clockify_sync_server.py} (100%) diff --git a/pype/modules/clockify/ftrack/server/action_clockify_sync.py b/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py similarity index 100% rename from pype/modules/clockify/ftrack/server/action_clockify_sync.py rename to pype/modules/clockify/ftrack/server/action_clockify_sync_server.py From 789f965defd60fffa012b55d30ff73420a165de5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 17:07:07 +0200 Subject: [PATCH 155/190] initial modifications of clockify server action --- .../server/action_clockify_sync_server.py | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py b/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py index 0ba4c3a265..f09b1cc746 100644 --- a/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py +++ b/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py @@ -1,23 +1,16 @@ import json -from pype.modules.ftrack.lib import BaseAction, statics_icon +from pype.modules.ftrack.lib import BaseAction from pype.modules.clockify import ClockifyAPI -class SyncClocify(BaseAction): +class SyncClocifyServer(BaseAction): '''Synchronise project names and task types.''' - #: Action identifier. - identifier = 'clockify.sync' - #: Action label. - label = 'Sync To Clockify' - #: Action description. - description = 'Synchronise data to Clockify workspace' - #: roles that are allowed to register this action + identifier = "clockify.sync.server" + label = "Sync To Clockify (server)" + description = "Synchronise data to Clockify workspace" role_list = ["Pypeclub", "Administrator", "project Manager"] - #: icon - icon = statics_icon("app_icons", "clockify-white.png") - #: CLockifyApi clockapi = ClockifyAPI() def discover(self, session, entities, event): @@ -28,6 +21,18 @@ class SyncClocify(BaseAction): return True return False + def register(self): + self.session.event_hub.subscribe( + "topic=ftrack.action.discover", + self._discover, + priority=self.priority + ) + + launch_subscription = ( + "topic=ftrack.action.launch and data.actionIdentifier={}" + ).format(self.identifier) + self.session.event_hub.subscribe(launch_subscription, self._launch) + def launch(self, session, entities, event): self.clockapi.set_api() if self.clockapi.workspace_id is None: From e38de6ed581d77f89b4242c0a5b30e63a1bb943e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 17:09:24 +0200 Subject: [PATCH 156/190] allow discovering only for discover roles --- .../server/action_clockify_sync_server.py | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py b/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py index f09b1cc746..19deb05e6d 100644 --- a/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py +++ b/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py @@ -9,16 +9,30 @@ class SyncClocifyServer(BaseAction): identifier = "clockify.sync.server" label = "Sync To Clockify (server)" description = "Synchronise data to Clockify workspace" - role_list = ["Pypeclub", "Administrator", "project Manager"] + + discover_role_list = ["Pypeclub", "Administrator", "project Manager"] clockapi = ClockifyAPI() def discover(self, session, entities, event): if ( - len(entities) == 1 - and entities[0].entity_type.lower() == "project" + len(entities) != 1 + or entities[0].entity_type.lower() == "project" ): - return True + return False + + # Get user and check his roles + user_id = event.get("source", {}).get("user", {}).get("id") + if not user_id: + return False + + user = session.query("User where id is \"{}\"".format(user_id)).first() + if not user: + return False + + for role in user["user_security_roles"]: + if role["security_role"]["name"] in self.discover_role_list: + return True return False def register(self): @@ -124,4 +138,4 @@ class SyncClocifyServer(BaseAction): def register(session, **kw): - SyncClocify(session).register() + SyncClocifyServer(session).register() From de9a075f04b24afea517daeaf4486a68e855fc5c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 17:25:59 +0200 Subject: [PATCH 157/190] modified headers access in clockify --- pype/modules/clockify/clockify_api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pype/modules/clockify/clockify_api.py b/pype/modules/clockify/clockify_api.py index 8ebed302fe..a8eefe13a4 100644 --- a/pype/modules/clockify/clockify_api.py +++ b/pype/modules/clockify/clockify_api.py @@ -11,7 +11,11 @@ class ClockifyAPI: self.workspace_name = workspace_name self.master_parent = master_parent self.workspace_id = None - self.headers = {"X-Api-Key": api_key} + self.api_key = api_key + + @property + def headers(self): + return {"X-Api-Key": self.api_key} def verify_api(self): for key, value in self.headers.items(): @@ -24,7 +28,7 @@ class ClockifyAPI: api_key = self.get_api_key() if api_key is not None and self.validate_api_key(api_key) is True: - self.headers["X-Api-Key"] = api_key + self.api_key = api_key self.set_workspace() if self.master_parent: self.master_parent.signed_in() From 8dfca2378c9a0653c76f4713f50b18943822647d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 17:26:13 +0200 Subject: [PATCH 158/190] minor tweaks --- .../server/action_clockify_sync_server.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py b/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py index 19deb05e6d..7e3c266b72 100644 --- a/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py +++ b/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py @@ -62,15 +62,13 @@ class SyncClocifyServer(BaseAction): } # JOB SETTINGS - userId = event['source']['user']['id'] - user = session.query('User where id is ' + userId).one() + user_id = event["source"]["user"]["id"] + user = session.query("User where id is " + user_id).one() - job = session.create('Job', { - 'user': user, - 'status': 'running', - 'data': json.dumps({ - 'description': 'Sync Ftrack to Clockify' - }) + job = session.create("Job", { + "user": user, + "status": "running", + "data": json.dumps({"description": "Sync Ftrack to Clockify"}) }) session.commit() @@ -127,7 +125,10 @@ class SyncClocifyServer(BaseAction): job["status"] = "done" except Exception: - pass + self.log.warning( + "Synchronization to clockify failed.", + exc_info=True + ) finally: if job["status"] != "done": From 4f244d187b75677435161aa6790ef4542a4a7df0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 17:48:34 +0200 Subject: [PATCH 159/190] clockify api is set in initialization part of action on server --- .../server/action_clockify_sync_server.py | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py b/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py index 7e3c266b72..0a844c9ef5 100644 --- a/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py +++ b/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py @@ -1,3 +1,4 @@ +import os import json from pype.modules.ftrack.lib import BaseAction from pype.modules.clockify import ClockifyAPI @@ -12,7 +13,32 @@ class SyncClocifyServer(BaseAction): discover_role_list = ["Pypeclub", "Administrator", "project Manager"] - clockapi = ClockifyAPI() + def __init__(self, *args, **kwargs): + super(SyncClocifyServer, self).__init__(*args, **kwargs) + + self.workspace_name = os.environ.get("CLOCKIFY_WORKSPACE") + api_key = os.environ.get("CLOCKIFY_API_KEY") + self.clockapi = ClockifyAPI(self.workspace_name, api_key) + self.api_key = api_key + + if api_key is None: + modified_key = "None" + else: + str_len = int(len(api_key) / 2) + start_replace = int(len(api_key) / 4) + modified_key = "" + for idx in range(len(api_key)): + if idx >= start_replace and idx < start_replace + str_len: + replacement = "X" + else: + replacement = api_key[idx] + modified_key += replacement + + self.log.info( + "Clockify info. Workspace: \"{}\" API key: \"{}\"".format( + str(self.workspace_name), str(modified_key) + ) + ) def discover(self, session, entities, event): if ( @@ -48,7 +74,6 @@ class SyncClocifyServer(BaseAction): self.session.event_hub.subscribe(launch_subscription, self._launch) def launch(self, session, entities, event): - self.clockapi.set_api() if self.clockapi.workspace_id is None: return { "success": False, From eb30a53af8c6327a9f3d5d2f642a74032527147a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 17:49:12 +0200 Subject: [PATCH 160/190] changed class name --- .../clockify/ftrack/user/action_clockify_sync_local.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/modules/clockify/ftrack/user/action_clockify_sync_local.py b/pype/modules/clockify/ftrack/user/action_clockify_sync_local.py index a7385c4774..528614eeba 100644 --- a/pype/modules/clockify/ftrack/user/action_clockify_sync_local.py +++ b/pype/modules/clockify/ftrack/user/action_clockify_sync_local.py @@ -3,7 +3,7 @@ from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.modules.clockify import ClockifyAPI -class SyncClocify(BaseAction): +class SyncClocifyLocal(BaseAction): '''Synchronise project names and task types.''' #: Action identifier. @@ -119,4 +119,4 @@ class SyncClocify(BaseAction): def register(session, **kw): - SyncClocify(session).register() + SyncClocifyLocal(session).register() From 98c1587da7ad6f26d517526e7c8f534d9f35f77e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 18:02:50 +0200 Subject: [PATCH 161/190] added clockify registration to ftrack event processor --- .../ftrack_server/sub_event_processor.py | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/pype/modules/ftrack/ftrack_server/sub_event_processor.py b/pype/modules/ftrack/ftrack_server/sub_event_processor.py index d7bb7a53b3..de0f0459a9 100644 --- a/pype/modules/ftrack/ftrack_server/sub_event_processor.py +++ b/pype/modules/ftrack/ftrack_server/sub_event_processor.py @@ -9,7 +9,7 @@ from pype.modules.ftrack.ftrack_server.lib import ( SocketSession, ProcessEventHub, TOPIC_STATUS_SERVER ) import ftrack_api -from pype.api import Logger +from pype.api import Logger, config log = Logger().get_logger("Event processor") @@ -55,6 +55,42 @@ def register(session): ) +def clockify_module_registration(): + module_name = "Clockify" + + menu_items = config.get_presets()["tray"]["menu_items"] + if not menu_items["item_usage"][module_name]: + return + + api_key = os.environ.get("CLOCKIFY_API_KEY") + if not api_key: + log.warning("Clockify API key is not set.") + return + + workspace_name = os.environ.get("CLOCKIFY_WORKSPACE") + if not workspace_name: + workspace_name = ( + menu_items + .get("attributes", {}) + .get(module_name, {}) + .get("workspace_name", {}) + ) + + if not workspace_name: + log.warning("Clockify Workspace is not set.") + return + + os.environ["CLOCKIFY_WORKSPACE"] = workspace_name + + from pype.modules.clockify import CLOCKIFY_FTRACK_SERVER_PATH + + current = os.environ.get("FTRACK_EVENTS_PATH") or "" + if current: + current += os.pathsep + os.environ["FTRACK_EVENTS_PATH"] = current + CLOCKIFY_FTRACK_SERVER_PATH + return True + + def main(args): port = int(args[-1]) # Create a TCP/IP socket @@ -66,6 +102,11 @@ def main(args): sock.connect(server_address) sock.sendall(b"CreatedProcess") + try: + clockify_result = clockify_module_registration() + except Exception: + log.info("Clockify registration failed.", exc_info=True) + try: session = SocketSession( auto_connect_event_hub=True, sock=sock, Eventhub=ProcessEventHub From eb4153a7865a7ebe941c2a3f076cde680e05c9fc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 18:08:03 +0200 Subject: [PATCH 162/190] added clockifyapikey and clockifyworkspace arguments to ftrack event server script --- .../ftrack/ftrack_server/event_server_cli.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pype/modules/ftrack/ftrack_server/event_server_cli.py b/pype/modules/ftrack/ftrack_server/event_server_cli.py index 73c7abfc5d..bf51c37290 100644 --- a/pype/modules/ftrack/ftrack_server/event_server_cli.py +++ b/pype/modules/ftrack/ftrack_server/event_server_cli.py @@ -522,6 +522,21 @@ def main(argv): help="Load creadentials from apps dir", action="store_true" ) + parser.add_argument( + "-clockifyapikey", type=str, + help=( + "Enter API key for Clockify actions." + " (default from environment: $CLOCKIFY_API_KEY)" + ) + ) + parser.add_argument( + "-clockifyworkspace", type=str, + help=( + "Enter workspace for Clockify." + " (default from module presets or " + "environment: $CLOCKIFY_WORKSPACE)" + ) + ) ftrack_url = os.environ.get('FTRACK_SERVER') username = os.environ.get('FTRACK_API_USER') api_key = os.environ.get('FTRACK_API_KEY') @@ -546,6 +561,12 @@ def main(argv): if kwargs.ftrackapikey: api_key = kwargs.ftrackapikey + if kwargs.clockifyworkspace: + os.environ["CLOCKIFY_WORKSPACE"] = kwargs.clockifyworkspace + + if kwargs.clockifyapikey: + os.environ["CLOCKIFY_API_KEY"] = kwargs.clockifyapikey + legacy = kwargs.legacy # Check url regex and accessibility ftrack_url = check_ftrack_url(ftrack_url) From ec9ff173379cf6601414445bac41b621272a4a16 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 18:32:45 +0200 Subject: [PATCH 163/190] removed workspace_name from clockify api init --- pype/modules/clockify/clockify_api.py | 8 ++++---- .../ftrack/server/action_clockify_sync_server.py | 9 ++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pype/modules/clockify/clockify_api.py b/pype/modules/clockify/clockify_api.py index a8eefe13a4..ca642a1c21 100644 --- a/pype/modules/clockify/clockify_api.py +++ b/pype/modules/clockify/clockify_api.py @@ -7,10 +7,10 @@ from . import CLOCKIFY_ENDPOINT, ADMIN_PERMISSION_NAMES, CREDENTIALS_JSON_PATH class ClockifyAPI: - def __init__(self, workspace_name=None, api_key=None, master_parent=None): - self.workspace_name = workspace_name - self.master_parent = master_parent + def __init__(self, api_key=None, master_parent=None): + self.workspace_name = None self.workspace_id = None + self.master_parent = master_parent self.api_key = api_key @property @@ -73,7 +73,7 @@ class ClockifyAPI: ) # this regex is neccessary: UNICODE strings are crashing # during json serialization - id_regex ='\"{1}id\"{1}\:{1}\"{1}\w+\"{1}' + id_regex = '\"{1}id\"{1}\:{1}\"{1}\w+\"{1}' result = re.findall(id_regex, str(response.content)) if len(result) != 1: # replace with log and better message? diff --git a/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py b/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py index 0a844c9ef5..442d1f92df 100644 --- a/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py +++ b/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py @@ -16,11 +16,10 @@ class SyncClocifyServer(BaseAction): def __init__(self, *args, **kwargs): super(SyncClocifyServer, self).__init__(*args, **kwargs) - self.workspace_name = os.environ.get("CLOCKIFY_WORKSPACE") + workspace_name = os.environ.get("CLOCKIFY_WORKSPACE") api_key = os.environ.get("CLOCKIFY_API_KEY") - self.clockapi = ClockifyAPI(self.workspace_name, api_key) - self.api_key = api_key - + self.clockapi = ClockifyAPI(api_key) + self.clockapi.set_workspace(workspace_name) if api_key is None: modified_key = "None" else: @@ -36,7 +35,7 @@ class SyncClocifyServer(BaseAction): self.log.info( "Clockify info. Workspace: \"{}\" API key: \"{}\"".format( - str(self.workspace_name), str(modified_key) + str(workspace_name), str(modified_key) ) ) From 2cfe2e139307236af803428ae33cffe87d6f5b0e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 18:32:58 +0200 Subject: [PATCH 164/190] fix discover condition --- .../clockify/ftrack/server/action_clockify_sync_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py b/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py index 442d1f92df..94b2f21da4 100644 --- a/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py +++ b/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py @@ -42,7 +42,7 @@ class SyncClocifyServer(BaseAction): def discover(self, session, entities, event): if ( len(entities) != 1 - or entities[0].entity_type.lower() == "project" + or entities[0].entity_type.lower() != "project" ): return False From 0318a5c35cf4829843dbfee5acd13428fab51aba Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 18:44:18 +0200 Subject: [PATCH 165/190] removed unused variable --- pype/modules/ftrack/ftrack_server/sub_event_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/ftrack/ftrack_server/sub_event_processor.py b/pype/modules/ftrack/ftrack_server/sub_event_processor.py index de0f0459a9..baef2ec5f6 100644 --- a/pype/modules/ftrack/ftrack_server/sub_event_processor.py +++ b/pype/modules/ftrack/ftrack_server/sub_event_processor.py @@ -103,7 +103,7 @@ def main(args): sock.sendall(b"CreatedProcess") try: - clockify_result = clockify_module_registration() + clockify_module_registration() except Exception: log.info("Clockify registration failed.", exc_info=True) From f7740839b7806a4c0979a9378349b746392796e7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 18:54:33 +0200 Subject: [PATCH 166/190] removed kredenc part from code --- pype/hosts/maya/customize.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pype/hosts/maya/customize.py b/pype/hosts/maya/customize.py index cf179e1d63..c929de22b8 100644 --- a/pype/hosts/maya/customize.py +++ b/pype/hosts/maya/customize.py @@ -145,16 +145,6 @@ def override_toolbox_ui(): parent=parent) controls.append(control) - # control = mc.iconTextButton( - # "pype_toolbox", - # annotation="Kredenc", - # label="Kredenc", - # image=os.path.join(icons, "kredenc_logo.png"), - # bgc=background_color, - # width=icon_size, - # height=icon_size, - # parent=parent) - # controls.append(control) # Add the buttons on the bottom and stack # them above each other with side padding From f7aa0a83da9994971deb533ff86e2807c58d00cc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 18:56:20 +0200 Subject: [PATCH 167/190] all tools are tried to import --- pype/hosts/maya/customize.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/pype/hosts/maya/customize.py b/pype/hosts/maya/customize.py index c929de22b8..bd0901a6fd 100644 --- a/pype/hosts/maya/customize.py +++ b/pype/hosts/maya/customize.py @@ -69,11 +69,30 @@ def override_component_mask_commands(): def override_toolbox_ui(): """Add custom buttons in Toolbox as replacement for Maya web help icon.""" + inventory = None + loader = None + launch_workfiles_app = None + mayalookassigner = None + try: + import avalon.tools.sceneinventory as inventory + except Exception: + log.warning("Could not import SceneInventory tool") + + try: + import avalon.tools.loader as loader + except Exception: + log.warning("Could not import Loader tool") + + try: + from avalon.maya.pipeline import launch_workfiles_app + except Exception: + log.warning("Could not import Workfiles tool") + + try: + import mayalookassigner + except Exception: + log.warning("Could not import Maya Look assigner tool") - import avalon.tools.sceneinventory as inventory - import avalon.tools.loader as loader - from avalon.maya.pipeline import launch_workfiles_app - import mayalookassigner from pype.api import resources icons = resources.get_resource("icons") From af26c083e549e5e5851b1415aed668bcfffc69ac Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 18:56:41 +0200 Subject: [PATCH 168/190] only imported tools are added to controls --- pype/hosts/maya/customize.py | 102 +++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 46 deletions(-) diff --git a/pype/hosts/maya/customize.py b/pype/hosts/maya/customize.py index bd0901a6fd..c6b5ac63cf 100644 --- a/pype/hosts/maya/customize.py +++ b/pype/hosts/maya/customize.py @@ -115,55 +115,65 @@ def override_toolbox_ui(): # Create our controls background_color = (0.267, 0.267, 0.267) controls = [] + if mayalookassigner: + controls.append( + mc.iconTextButton( + "pype_toolbox_lookmanager", + annotation="Look Manager", + label="Look Manager", + image=os.path.join(icons, "lookmanager.png"), + command=lambda: mayalookassigner.show(), + bgc=background_color, + width=icon_size, + height=icon_size, + parent=parent + ) + ) - control = mc.iconTextButton( - "pype_toolbox_lookmanager", - annotation="Look Manager", - label="Look Manager", - image=os.path.join(icons, "lookmanager.png"), - command=lambda: mayalookassigner.show(), - bgc=background_color, - width=icon_size, - height=icon_size, - parent=parent) - controls.append(control) + if launch_workfiles_app: + controls.append( + mc.iconTextButton( + "pype_toolbox_workfiles", + annotation="Work Files", + label="Work Files", + image=os.path.join(icons, "workfiles.png"), + command=lambda: launch_workfiles_app(), + bgc=background_color, + width=icon_size, + height=icon_size, + parent=parent + ) + ) - control = mc.iconTextButton( - "pype_toolbox_workfiles", - annotation="Work Files", - label="Work Files", - image=os.path.join(icons, "workfiles.png"), - command=lambda: launch_workfiles_app(), - bgc=background_color, - width=icon_size, - height=icon_size, - parent=parent) - controls.append(control) - - control = mc.iconTextButton( - "pype_toolbox_loader", - annotation="Loader", - label="Loader", - image=os.path.join(icons, "loader.png"), - command=lambda: loader.show(use_context=True), - bgc=background_color, - width=icon_size, - height=icon_size, - parent=parent) - controls.append(control) - - control = mc.iconTextButton( - "pype_toolbox_manager", - annotation="Inventory", - label="Inventory", - image=os.path.join(icons, "inventory.png"), - command=lambda: inventory.show(), - bgc=background_color, - width=icon_size, - height=icon_size, - parent=parent) - controls.append(control) + if loader: + controls.append( + mc.iconTextButton( + "pype_toolbox_loader", + annotation="Loader", + label="Loader", + image=os.path.join(icons, "loader.png"), + command=lambda: loader.show(use_context=True), + bgc=background_color, + width=icon_size, + height=icon_size, + parent=parent + ) + ) + if inventory: + controls.append( + mc.iconTextButton( + "pype_toolbox_manager", + annotation="Inventory", + label="Inventory", + image=os.path.join(icons, "inventory.png"), + command=lambda: inventory.show(), + bgc=background_color, + width=icon_size, + height=icon_size, + parent=parent + ) + ) # Add the buttons on the bottom and stack # them above each other with side padding From 710fdd4d5b94224b500ce6b3158aa68c154d2713 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 18:57:06 +0200 Subject: [PATCH 169/190] skip adding tools process if none of tools import was succesful --- pype/hosts/maya/customize.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pype/hosts/maya/customize.py b/pype/hosts/maya/customize.py index c6b5ac63cf..ee3ad4f239 100644 --- a/pype/hosts/maya/customize.py +++ b/pype/hosts/maya/customize.py @@ -97,6 +97,11 @@ def override_toolbox_ui(): icons = resources.get_resource("icons") + if not any(( + mayalookassigner, launch_workfiles_app, loader, inventory + )): + return + # Ensure the maya web icon on toolbox exists web_button = "ToolBox|MainToolboxLayout|mayaWebButton" if not mc.iconTextButton(web_button, query=True, exists=True): From b6b3ea0f35836d364ed186cf8faf82f08247d692 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 19:56:09 +0200 Subject: [PATCH 170/190] cleaned clockify imports --- pype/modules/clockify/__init__.py | 10 ---------- pype/modules/clockify/clockify.py | 6 +++--- pype/modules/clockify/clockify_api.py | 4 +++- .../ftrack/server/action_clockify_sync_server.py | 2 +- .../clockify/ftrack/user/action_clockify_sync_local.py | 2 +- .../modules/clockify/launcher_actions/ClockifyStart.py | 2 +- pype/modules/clockify/launcher_actions/ClockifySync.py | 2 +- .../ftrack/ftrack_server/sub_event_processor.py | 2 +- 8 files changed, 11 insertions(+), 19 deletions(-) diff --git a/pype/modules/clockify/__init__.py b/pype/modules/clockify/__init__.py index 0ea58b5b00..8e11d2f5f4 100644 --- a/pype/modules/clockify/__init__.py +++ b/pype/modules/clockify/__init__.py @@ -1,13 +1,3 @@ -from .constants import ( - CLOCKIFY_ENDPOINT, - ADMIN_PERMISSION_NAMES, - CREDENTIALS_JSON_PATH, - CLOCKIFY_FTRACK_USER_PATH, - CLOCKIFY_FTRACK_SERVER_PATH -) -from .clockify_api import ClockifyAPI -from .widget_settings import ClockifySettings -from .widget_message import MessageWidget from .clockify import ClockifyModule CLASS_DEFINIION = ClockifyModule diff --git a/pype/modules/clockify/clockify.py b/pype/modules/clockify/clockify.py index 4c99fb8a15..fea15a1bea 100644 --- a/pype/modules/clockify/clockify.py +++ b/pype/modules/clockify/clockify.py @@ -3,9 +3,9 @@ import threading from pype.api import Logger from avalon import style from Qt import QtWidgets -from . import ( - ClockifySettings, ClockifyAPI, MessageWidget, CLOCKIFY_FTRACK_USER_PATH -) +from .widgets import ClockifySettings, MessageWidget +from .clockify_api import ClockifyAPI +from .constants import CLOCKIFY_FTRACK_USER_PATH class ClockifyModule: diff --git a/pype/modules/clockify/clockify_api.py b/pype/modules/clockify/clockify_api.py index ca642a1c21..0a09c65628 100644 --- a/pype/modules/clockify/clockify_api.py +++ b/pype/modules/clockify/clockify_api.py @@ -3,7 +3,9 @@ import re import requests import json import datetime -from . import CLOCKIFY_ENDPOINT, ADMIN_PERMISSION_NAMES, CREDENTIALS_JSON_PATH +from .constants import ( + CLOCKIFY_ENDPOINT, ADMIN_PERMISSION_NAMES, CREDENTIALS_JSON_PATH +) class ClockifyAPI: diff --git a/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py b/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py index 94b2f21da4..ae911f6258 100644 --- a/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py +++ b/pype/modules/clockify/ftrack/server/action_clockify_sync_server.py @@ -1,7 +1,7 @@ import os import json from pype.modules.ftrack.lib import BaseAction -from pype.modules.clockify import ClockifyAPI +from pype.modules.clockify.clockify_api import ClockifyAPI class SyncClocifyServer(BaseAction): diff --git a/pype/modules/clockify/ftrack/user/action_clockify_sync_local.py b/pype/modules/clockify/ftrack/user/action_clockify_sync_local.py index 528614eeba..e74bf3dbb3 100644 --- a/pype/modules/clockify/ftrack/user/action_clockify_sync_local.py +++ b/pype/modules/clockify/ftrack/user/action_clockify_sync_local.py @@ -1,6 +1,6 @@ import json from pype.modules.ftrack.lib import BaseAction, statics_icon -from pype.modules.clockify import ClockifyAPI +from pype.modules.clockify.clockify_api import ClockifyAPI class SyncClocifyLocal(BaseAction): diff --git a/pype/modules/clockify/launcher_actions/ClockifyStart.py b/pype/modules/clockify/launcher_actions/ClockifyStart.py index d5e9164977..f97360662f 100644 --- a/pype/modules/clockify/launcher_actions/ClockifyStart.py +++ b/pype/modules/clockify/launcher_actions/ClockifyStart.py @@ -1,6 +1,6 @@ from avalon import api, io from pype.api import Logger -from pype.modules.clockify import ClockifyAPI +from pype.modules.clockify.clockify_api import ClockifyAPI log = Logger().get_logger(__name__, "clockify_start") diff --git a/pype/modules/clockify/launcher_actions/ClockifySync.py b/pype/modules/clockify/launcher_actions/ClockifySync.py index 0f20d1dce1..a77c038076 100644 --- a/pype/modules/clockify/launcher_actions/ClockifySync.py +++ b/pype/modules/clockify/launcher_actions/ClockifySync.py @@ -1,5 +1,5 @@ from avalon import api, io -from pype.modules.clockify import ClockifyAPI +from pype.modules.clockify.clockify_api import ClockifyAPI from pype.api import Logger log = Logger().get_logger(__name__, "clockify_sync") diff --git a/pype/modules/ftrack/ftrack_server/sub_event_processor.py b/pype/modules/ftrack/ftrack_server/sub_event_processor.py index baef2ec5f6..4a3241dd4f 100644 --- a/pype/modules/ftrack/ftrack_server/sub_event_processor.py +++ b/pype/modules/ftrack/ftrack_server/sub_event_processor.py @@ -82,7 +82,7 @@ def clockify_module_registration(): os.environ["CLOCKIFY_WORKSPACE"] = workspace_name - from pype.modules.clockify import CLOCKIFY_FTRACK_SERVER_PATH + from pype.modules.clockify.constants import CLOCKIFY_FTRACK_SERVER_PATH current = os.environ.get("FTRACK_EVENTS_PATH") or "" if current: From 3b0fae9fa25055e73d99086d4348d6f9d5e42b86 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Jul 2020 19:56:21 +0200 Subject: [PATCH 171/190] clockify widgets are in one file --- pype/modules/clockify/widget_message.py | 92 ------------------- .../{widget_settings.py => widgets.py} | 90 +++++++++++++++++- 2 files changed, 89 insertions(+), 93 deletions(-) delete mode 100644 pype/modules/clockify/widget_message.py rename pype/modules/clockify/{widget_settings.py => widgets.py} (66%) diff --git a/pype/modules/clockify/widget_message.py b/pype/modules/clockify/widget_message.py deleted file mode 100644 index 9e4fad1df1..0000000000 --- a/pype/modules/clockify/widget_message.py +++ /dev/null @@ -1,92 +0,0 @@ -from Qt import QtCore, QtGui, QtWidgets -from avalon import style -from pype.api import resources - - -class MessageWidget(QtWidgets.QWidget): - - SIZE_W = 300 - SIZE_H = 130 - - closed = QtCore.Signal() - - def __init__(self, parent=None, messages=[], title="Message"): - - super(MessageWidget, self).__init__() - - self._parent = parent - - # Icon - if parent and hasattr(parent, 'icon'): - self.setWindowIcon(parent.icon) - else: - icon = QtGui.QIcon(resources.pype_icon_filepath()) - self.setWindowIcon(icon) - - self.setWindowFlags( - QtCore.Qt.WindowCloseButtonHint | - QtCore.Qt.WindowMinimizeButtonHint - ) - - # Font - self.font = QtGui.QFont() - self.font.setFamily("DejaVu Sans Condensed") - self.font.setPointSize(9) - self.font.setBold(True) - self.font.setWeight(50) - self.font.setKerning(True) - - # Size setting - self.resize(self.SIZE_W, self.SIZE_H) - self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) - self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100)) - - # Style - self.setStyleSheet(style.load_stylesheet()) - - self.setLayout(self._ui_layout(messages)) - self.setWindowTitle(title) - - def _ui_layout(self, messages): - if not messages: - messages = ["*Misssing messages (This is a bug)*", ] - - elif not isinstance(messages, (tuple, list)): - messages = [messages, ] - - main_layout = QtWidgets.QVBoxLayout(self) - - labels = [] - for message in messages: - label = QtWidgets.QLabel(message) - label.setFont(self.font) - label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - label.setTextFormat(QtCore.Qt.RichText) - label.setWordWrap(True) - - labels.append(label) - main_layout.addWidget(label) - - btn_close = QtWidgets.QPushButton("Close") - btn_close.setToolTip('Close this window') - btn_close.clicked.connect(self.on_close_clicked) - - btn_group = QtWidgets.QHBoxLayout() - btn_group.addStretch(1) - btn_group.addWidget(btn_close) - - main_layout.addLayout(btn_group) - - self.labels = labels - self.btn_group = btn_group - self.btn_close = btn_close - self.main_layout = main_layout - - return main_layout - - def on_close_clicked(self): - self.close() - - def close(self, *args, **kwargs): - self.closed.emit() - super(MessageWidget, self).close(*args, **kwargs) diff --git a/pype/modules/clockify/widget_settings.py b/pype/modules/clockify/widgets.py similarity index 66% rename from pype/modules/clockify/widget_settings.py rename to pype/modules/clockify/widgets.py index 7e5ee300bb..dc57a48ecb 100644 --- a/pype/modules/clockify/widget_settings.py +++ b/pype/modules/clockify/widgets.py @@ -1,9 +1,97 @@ -import os from Qt import QtCore, QtGui, QtWidgets from avalon import style from pype.api import resources +class MessageWidget(QtWidgets.QWidget): + + SIZE_W = 300 + SIZE_H = 130 + + closed = QtCore.Signal() + + def __init__(self, parent=None, messages=[], title="Message"): + + super(MessageWidget, self).__init__() + + self._parent = parent + + # Icon + if parent and hasattr(parent, 'icon'): + self.setWindowIcon(parent.icon) + else: + icon = QtGui.QIcon(resources.pype_icon_filepath()) + self.setWindowIcon(icon) + + self.setWindowFlags( + QtCore.Qt.WindowCloseButtonHint | + QtCore.Qt.WindowMinimizeButtonHint + ) + + # Font + self.font = QtGui.QFont() + self.font.setFamily("DejaVu Sans Condensed") + self.font.setPointSize(9) + self.font.setBold(True) + self.font.setWeight(50) + self.font.setKerning(True) + + # Size setting + self.resize(self.SIZE_W, self.SIZE_H) + self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) + self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100)) + + # Style + self.setStyleSheet(style.load_stylesheet()) + + self.setLayout(self._ui_layout(messages)) + self.setWindowTitle(title) + + def _ui_layout(self, messages): + if not messages: + messages = ["*Misssing messages (This is a bug)*", ] + + elif not isinstance(messages, (tuple, list)): + messages = [messages, ] + + main_layout = QtWidgets.QVBoxLayout(self) + + labels = [] + for message in messages: + label = QtWidgets.QLabel(message) + label.setFont(self.font) + label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + label.setTextFormat(QtCore.Qt.RichText) + label.setWordWrap(True) + + labels.append(label) + main_layout.addWidget(label) + + btn_close = QtWidgets.QPushButton("Close") + btn_close.setToolTip('Close this window') + btn_close.clicked.connect(self.on_close_clicked) + + btn_group = QtWidgets.QHBoxLayout() + btn_group.addStretch(1) + btn_group.addWidget(btn_close) + + main_layout.addLayout(btn_group) + + self.labels = labels + self.btn_group = btn_group + self.btn_close = btn_close + self.main_layout = main_layout + + return main_layout + + def on_close_clicked(self): + self.close() + + def close(self, *args, **kwargs): + self.closed.emit() + super(MessageWidget, self).close(*args, **kwargs) + + class ClockifySettings(QtWidgets.QWidget): SIZE_W = 300 From 77c9725c595e4e40f25f0ad00d9ec4a4e70a7b9d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 23 Jul 2020 14:14:28 +0200 Subject: [PATCH 172/190] store project's applications to event data to not query project all the time --- pype/modules/ftrack/lib/ftrack_app_handler.py | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/pype/modules/ftrack/lib/ftrack_app_handler.py b/pype/modules/ftrack/lib/ftrack_app_handler.py index efc2df0ea5..b16dbd7b64 100644 --- a/pype/modules/ftrack/lib/ftrack_app_handler.py +++ b/pype/modules/ftrack/lib/ftrack_app_handler.py @@ -84,7 +84,7 @@ class AppAction(BaseAction): if ( len(entities) != 1 - or entities[0].entity_type.lower() != 'task' + or entities[0].entity_type.lower() != "task" ): return False @@ -92,21 +92,31 @@ class AppAction(BaseAction): if entity["parent"].entity_type.lower() == "project": return False - ft_project = self.get_project_from_entity(entity) - database = pypelib.get_avalon_database() - project_name = ft_project["full_name"] - avalon_project = database[project_name].find_one({ - "type": "project" - }) + avalon_project_apps = event["data"].get("avalon_project_apps", None) + avalon_project_doc = event["data"].get("avalon_project_doc", None) + if avalon_project_apps is None: + if avalon_project_doc is None: + ft_project = self.get_project_from_entity(entity) + database = pypelib.get_avalon_database() + project_name = ft_project["full_name"] + avalon_project_doc = database[project_name].find_one({ + "type": "project" + }) or False + event["data"]["avalon_project_doc"] = avalon_project_doc - if not avalon_project: + if not avalon_project_doc: + return False + + project_apps_config = avalon_project_doc["config"].get("apps", []) + avalon_project_apps = [ + app["name"] for app in project_apps_config + ] or False + event["data"]["avalon_project_apps"] = avalon_project_apps + + if not avalon_project_apps: return False - project_apps = avalon_project["config"].get("apps", []) - apps = [app["name"] for app in project_apps] - if self.identifier in apps: - return True - return False + return self.identifier in avalon_project_apps def _launch(self, event): entities = self._translate_event(event) From fc27d6d2dff908a41e3f45948f97d00f40770cb4 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 23 Jul 2020 15:27:37 +0200 Subject: [PATCH 173/190] render wasn't working without audio --- .../plugins/harmony/publish/extract_render.py | 23 ++++++++---- .../plugins/harmony/publish/validate_audio.py | 37 +++++++++++++++++++ 2 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 pype/plugins/harmony/publish/validate_audio.py diff --git a/pype/plugins/harmony/publish/extract_render.py b/pype/plugins/harmony/publish/extract_render.py index 7ca83d3f0f..fe1352f9f9 100644 --- a/pype/plugins/harmony/publish/extract_render.py +++ b/pype/plugins/harmony/publish/extract_render.py @@ -111,13 +111,22 @@ class ExtractRender(pyblish.api.InstancePlugin): # Generate mov. mov_path = os.path.join(path, instance.data["name"] + ".mov") - args = [ - "ffmpeg", "-y", - "-i", audio_path, - "-i", - os.path.join(path, collection.head + "%04d" + collection.tail), - mov_path - ] + if os.path.isfile(audio_path): + args = [ + "ffmpeg", "-y", + "-i", audio_path, + "-i", + os.path.join(path, collection.head + "%04d" + collection.tail), + mov_path + ] + else: + args = [ + "ffmpeg", "-y", + "-i", + os.path.join(path, collection.head + "%04d" + collection.tail), + mov_path + ] + process = subprocess.Popen( args, stdout=subprocess.PIPE, diff --git a/pype/plugins/harmony/publish/validate_audio.py b/pype/plugins/harmony/publish/validate_audio.py new file mode 100644 index 0000000000..ba113e7610 --- /dev/null +++ b/pype/plugins/harmony/publish/validate_audio.py @@ -0,0 +1,37 @@ +import json +import os + +import pyblish.api + +import avalon.harmony +import pype.hosts.harmony + + +class ValidateAudio(pyblish.api.InstancePlugin): + """Ensures that there is an audio file in the scene. If you are sure that you want to send render without audio, you can disable this validator before clicking on "publish" """ + + order = pyblish.api.ValidatorOrder + label = "Validate Audio" + families = ["render"] + hosts = ["harmony"] + optional = True + + def process(self, instance): + # Collect scene data. + func = """function func(write_node) + { + return [ + sound.getSoundtrackAll().path() + ] + } + func + """ + result = avalon.harmony.send( + {"function": func, "args": [instance[0]]} + )["result"] + + audio_path = result[0] + + msg = "You are missing audio file:\n{}".format(audio_path) + + assert os.path.isfile(audio_path), msg From cef1e9435b8f39a01351ea6b5ecf215081c98461 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 23 Jul 2020 15:27:52 +0200 Subject: [PATCH 174/190] allow loading of workfiles as templates --- pype/plugins/harmony/load/load_template_workfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/harmony/load/load_template_workfile.py b/pype/plugins/harmony/load/load_template_workfile.py index b727cf865c..db67f20ff7 100644 --- a/pype/plugins/harmony/load/load_template_workfile.py +++ b/pype/plugins/harmony/load/load_template_workfile.py @@ -9,7 +9,7 @@ from avalon import api, harmony class ImportTemplateLoader(api.Loader): """Import templates.""" - families = ["harmony.template"] + families = ["harmony.template", "workfile"] representations = ["*"] label = "Import Template" From 8437bf939292a03c5e58fd28c4f5631bc419121c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 23 Jul 2020 16:39:52 +0100 Subject: [PATCH 175/190] correct render and publish render path passing --- .../celaction/publish/collect_render_path.py | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/pype/plugins/celaction/publish/collect_render_path.py b/pype/plugins/celaction/publish/collect_render_path.py index d5fe6c07a5..a3918a52b6 100644 --- a/pype/plugins/celaction/publish/collect_render_path.py +++ b/pype/plugins/celaction/publish/collect_render_path.py @@ -10,9 +10,14 @@ class CollectRenderPath(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.495 families = ["render.farm"] + # Presets + anatomy_render_key = None + anatomy_publish_render_key = None + def process(self, instance): anatomy = instance.context.data["anatomy"] anatomy_data = copy.deepcopy(instance.data["anatomyData"]) + anatomy_data["family"] = "render" padding = anatomy.templates.get("frame_padding", 4) anatomy_data.update({ "frame": f"%0{padding}d", @@ -21,12 +26,28 @@ class CollectRenderPath(pyblish.api.InstancePlugin): anatomy_filled = anatomy.format(anatomy_data) - render_dir = anatomy_filled["render_tmp"]["folder"] - render_path = anatomy_filled["render_tmp"]["path"] + # get anatomy rendering keys + anatomy_render_key = self.anatomy_render_key or "render" + anatomy_publish_render_key = self.anatomy_publish_render_key or "render" + + # get folder and path for rendering images from celaction + render_dir = anatomy_filled[anatomy_render_key]["folder"] + render_path = anatomy_filled[anatomy_render_key]["path"] # create dir if it doesnt exists - os.makedirs(render_dir, exist_ok=True) + try: + if not os.path.isdir(render_dir): + os.makedirs(render_dir, exist_ok=True) + except OSError: + # directory is not available + self.log.warning("Path is unreachable: `{}`".format(render_dir)) + # add rendering path to instance data instance.data["path"] = render_path + # get anatomy for published renders folder path + if anatomy_filled.get(anatomy_publish_render_key): + instance.data["publishRenderFolder"] = anatomy_filled[ + anatomy_publish_render_key]["folder"] + self.log.info(f"Render output path set to: `{render_path}`") From 7d49a281499f1ff01dc4258bd1b2b164bef13926 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 23 Jul 2020 16:40:44 +0100 Subject: [PATCH 176/190] submit celaciton to deadline with consistent slashes in path --- pype/plugins/celaction/publish/submit_celaction_deadline.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/plugins/celaction/publish/submit_celaction_deadline.py b/pype/plugins/celaction/publish/submit_celaction_deadline.py index c749ec111f..9091b24150 100644 --- a/pype/plugins/celaction/publish/submit_celaction_deadline.py +++ b/pype/plugins/celaction/publish/submit_celaction_deadline.py @@ -74,6 +74,7 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): resolution_width = instance.data["resolutionWidth"] resolution_height = instance.data["resolutionHeight"] render_dir = os.path.normpath(os.path.dirname(render_path)) + render_path = os.path.normpath(render_path) script_name = os.path.basename(script_path) jobname = "%s - %s" % (script_name, instance.name) @@ -98,6 +99,7 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): args = [ f"{script_path}", "-a", + "-16", "-s ", "-e ", f"-d {render_dir}", @@ -135,8 +137,10 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): # Optional, enable double-click to preview rendered # frames from Deadline Monitor - "OutputFilename0": output_filename_0.replace("\\", "/") + "OutputFilename0": output_filename_0.replace("\\", "/"), + # # Asset dependency to wait for at least the scene file to sync. + # "AssetDependency0": script_path }, "PluginInfo": { # Input From 2e0880d90d33c6866c3fb2bdc80976823297eb60 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 23 Jul 2020 16:41:26 +0100 Subject: [PATCH 177/190] fix(global): google drive stream file duplicity error --- pype/plugins/global/publish/integrate_new.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index d151cfc608..3582b55c82 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -551,12 +551,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # copy file with speedcopy and check if size of files are simetrical while True: + import shutil try: copyfile(src, dst) - except (OSError, AttributeError) as e: - self.log.warning(e) - # try it again with shutil - import shutil + except shutil.SameFileError as sfe: + self.log.critical("files are the same {} to {}".format(src, dst)) + os.remove(dst) try: shutil.copyfile(src, dst) self.log.debug("Copying files with shutil...") From ebe6bb4802a3ed39ab0ad00c6ad51e7c080cc255 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 23 Jul 2020 16:42:12 +0100 Subject: [PATCH 178/190] fix(global): metadata file path with different anatomy key --- .../global/publish/submit_publish_job.py | 70 +++++++++++++------ 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 4f32e37c17..f74b93cf7d 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -12,7 +12,15 @@ from avalon.vendor import requests, clique import pyblish.api -def _get_script(): +def _get_script(path): + + # pass input path if exists + if path: + if os.path.exists(path): + return str(path) + else: + raise + """Get path to the image sequence script.""" try: from pathlib import Path @@ -192,6 +200,38 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): families_transfer = ["render3d", "render2d", "ftrack", "slate"] plugin_python_version = "3.7" + # script path for publish_filesequence.py + publishing_script = None + + def _create_metadata_path(self, instance): + ins_data = instance.data + # Ensure output dir exists + output_dir = ins_data.get("publishRenderFolder", ins_data["outputDir"]) + + try: + if not os.path.isdir(output_dir): + os.makedirs(output_dir) + except OSError: + # directory is not available + self.log.warning("Path is unreachable: `{}`".format(output_dir)) + + metadata_filename = "{}_metadata.json".format(ins_data["subset"]) + + metadata_path = os.path.join(output_dir, metadata_filename) + + # Convert output dir to `{root}/rest/of/path/...` with Anatomy + success, roothless_mtdt_p = self.anatomy.find_root_template_from_path( + metadata_path) + if not success: + # `rootless_path` is not set to `output_dir` if none of roots match + self.log.warning(( + "Could not find root path for remapping \"{}\"." + " This may cause issues on farm." + ).format(output_dir)) + roothless_mtdt_p = metadata_path + + return (metadata_path, roothless_mtdt_p) + def _submit_deadline_post_job(self, instance, job): """Submit publish job to Deadline. @@ -205,17 +245,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): job_name = "Publish - {subset}".format(subset=subset) output_dir = instance.data["outputDir"] - # Convert output dir to `{root}/rest/of/path/...` with Anatomy - success, rootless_path = ( - self.anatomy.find_root_template_from_path(output_dir) - ) - if not success: - # `rootless_path` is not set to `output_dir` if none of roots match - self.log.warning(( - "Could not find root path for remapping \"{}\"." - " This may cause issues on farm." - ).format(output_dir)) - rootless_path = output_dir # Generate the payload for Deadline submission payload = { @@ -239,7 +268,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): }, "PluginInfo": { "Version": self.plugin_python_version, - "ScriptFile": _get_script(), + "ScriptFile": _get_script(self.publishing_script), "Arguments": "", "SingleFrameOnly": "True", }, @@ -249,11 +278,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # Transfer the environment from the original job to this dependent # job so they use the same environment - metadata_filename = "{}_metadata.json".format(subset) - metadata_path = os.path.join(rootless_path, metadata_filename) + metadata_path, roothless_metadata_path = self._create_metadata_path( + instance) environment = job["Props"].get("Env", {}) - environment["PYPE_METADATA_FILE"] = metadata_path + environment["PYPE_METADATA_FILE"] = roothless_metadata_path environment["AVALON_PROJECT"] = io.Session["AVALON_PROJECT"] environment["PYPE_LOG_NO_COLORS"] = "1" try: @@ -854,14 +883,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): } publish_job.update({"ftrack": ftrack}) - # Ensure output dir exists - output_dir = instance.data["outputDir"] - if not os.path.isdir(output_dir): - os.makedirs(output_dir) + metadata_path, roothless_metadata_path = self._create_metadata_path( + instance) - metadata_filename = "{}_metadata.json".format(subset) - - metadata_path = os.path.join(output_dir, metadata_filename) self.log.info("Writing json file: {}".format(metadata_path)) with open(metadata_path, "w") as f: json.dump(publish_job, f, indent=4, sort_keys=True) From 4d638ebdc85ff9077e79b49af2e38ee6fd344c2f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 24 Jul 2020 11:08:04 +0200 Subject: [PATCH 179/190] added item to tray menu with version --- pype/tools/tray/pype_tray.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 7dda8bf4f7..99832f0313 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -4,6 +4,11 @@ import platform from avalon import style from Qt import QtCore, QtGui, QtWidgets, QtSvg from pype.api import config, Logger, resources +import pype.version +try: + import configparser +except Exception: + import ConfigParser as configparser class TrayManager: @@ -100,6 +105,11 @@ class TrayManager: if items and self.services_submenu is not None: self.add_separator(self.tray_widget.menu) + version_string = self._version_string() + version_action = QtWidgets.QAction(version_string, self.tray_widget) + self.tray_widget.menu.addAction(version_action) + self.add_separator(self.tray_widget.menu) + # Add Exit action to menu aExit = QtWidgets.QAction("&Exit", self.tray_widget) aExit.triggered.connect(self.tray_widget.exit) @@ -109,6 +119,31 @@ class TrayManager: self.connect_modules() self.start_modules() + def _version_string(self): + subversion = None + client_name = None + config_file_path = os.path.join( + os.environ["PYPE_SETUP_PATH"], "pypeapp", "config.ini" + ) + version_string = pype.version.__version__ + if os.path.exists(config_file_path): + config = configparser.ConfigParser() + config.read(config_file_path) + try: + default_config = config["CLIENT"] + except Exception: + default_config = {} + subversion = default_config.get("subversion") + client_name = default_config.get("client_name") + + if subversion: + version_string += " ({})".format(subversion) + + if client_name: + version_string += ", {}".format(client_name) + + return version_string + def process_items(self, items, parent_menu): """ Loop through items and add them to parent_menu. From f4d49b330eabd9d9f6d5a744f1b9dd7374080df7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 24 Jul 2020 11:12:47 +0200 Subject: [PATCH 180/190] add version item only if client information is in config.ini --- pype/tools/tray/pype_tray.py | 40 ++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 99832f0313..3d6d6d473b 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -105,10 +105,7 @@ class TrayManager: if items and self.services_submenu is not None: self.add_separator(self.tray_widget.menu) - version_string = self._version_string() - version_action = QtWidgets.QAction(version_string, self.tray_widget) - self.tray_widget.menu.addAction(version_action) - self.add_separator(self.tray_widget.menu) + self._add_version_item() # Add Exit action to menu aExit = QtWidgets.QAction("&Exit", self.tray_widget) @@ -119,30 +116,37 @@ class TrayManager: self.connect_modules() self.start_modules() - def _version_string(self): - subversion = None - client_name = None + def _add_version_item(self): config_file_path = os.path.join( os.environ["PYPE_SETUP_PATH"], "pypeapp", "config.ini" ) - version_string = pype.version.__version__ - if os.path.exists(config_file_path): - config = configparser.ConfigParser() - config.read(config_file_path) - try: - default_config = config["CLIENT"] - except Exception: - default_config = {} - subversion = default_config.get("subversion") - client_name = default_config.get("client_name") + if not os.path.exists(config_file_path): + return + subversion = None + client_name = None + + config = configparser.ConfigParser() + config.read(config_file_path) + try: + default_config = config["CLIENT"] + except Exception: + default_config = {} + subversion = default_config.get("subversion") + client_name = default_config.get("client_name") + if not subversion and not client_name: + return + + version_string = pype.version.__version__ if subversion: version_string += " ({})".format(subversion) if client_name: version_string += ", {}".format(client_name) - return version_string + version_action = QtWidgets.QAction(version_string, self.tray_widget) + self.tray_widget.menu.addAction(version_action) + self.add_separator(self.tray_widget.menu) def process_items(self, items, parent_menu): """ Loop through items and add them to parent_menu. From 60501b17632297ddd77d07c437c31cfc59da03ae Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 24 Jul 2020 12:30:28 +0200 Subject: [PATCH 181/190] added time check for limit of 10 request in 1 second --- pype/modules/clockify/clockify_api.py | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/pype/modules/clockify/clockify_api.py b/pype/modules/clockify/clockify_api.py index 0a09c65628..d88b2ef8df 100644 --- a/pype/modules/clockify/clockify_api.py +++ b/pype/modules/clockify/clockify_api.py @@ -1,5 +1,6 @@ import os import re +import time import requests import json import datetime @@ -8,12 +9,27 @@ from .constants import ( ) +def time_check(obj): + if obj.request_counter < 10: + obj.request_counter += 1 + return + + wait_time = 1 - (time.time() - obj.request_time) + if wait_time > 0: + time.sleep(wait_time) + + obj.request_time = time.time() + obj.request_counter = 0 + + class ClockifyAPI: def __init__(self, api_key=None, master_parent=None): self.workspace_name = None self.workspace_id = None self.master_parent = master_parent self.api_key = api_key + self.request_counter = 0 + self.request_time = time.time() @property def headers(self): @@ -40,6 +56,7 @@ class ClockifyAPI: def validate_api_key(self, api_key): test_headers = {'X-Api-Key': api_key} action_url = 'workspaces/' + time_check(self) response = requests.get( CLOCKIFY_ENDPOINT + action_url, headers=test_headers @@ -57,6 +74,7 @@ class ClockifyAPI: action_url = "/workspaces/{}/users/{}/permissions".format( workspace_id, user_id ) + time_check(self) response = requests.get( CLOCKIFY_ENDPOINT + action_url, headers=self.headers @@ -69,6 +87,7 @@ class ClockifyAPI: def get_user_id(self): action_url = 'v1/user/' + time_check(self) response = requests.get( CLOCKIFY_ENDPOINT + action_url, headers=self.headers @@ -129,6 +148,7 @@ class ClockifyAPI: def get_workspaces(self): action_url = 'workspaces/' + time_check(self) response = requests.get( CLOCKIFY_ENDPOINT + action_url, headers=self.headers @@ -141,6 +161,7 @@ class ClockifyAPI: if workspace_id is None: workspace_id = self.workspace_id action_url = 'workspaces/{}/projects/'.format(workspace_id) + time_check(self) response = requests.get( CLOCKIFY_ENDPOINT + action_url, headers=self.headers @@ -156,6 +177,7 @@ class ClockifyAPI: action_url = 'workspaces/{}/projects/{}/'.format( workspace_id, project_id ) + time_check(self) response = requests.get( CLOCKIFY_ENDPOINT + action_url, headers=self.headers @@ -167,6 +189,7 @@ class ClockifyAPI: if workspace_id is None: workspace_id = self.workspace_id action_url = 'workspaces/{}/tags/'.format(workspace_id) + time_check(self) response = requests.get( CLOCKIFY_ENDPOINT + action_url, headers=self.headers @@ -182,6 +205,7 @@ class ClockifyAPI: action_url = 'workspaces/{}/projects/{}/tasks/'.format( workspace_id, project_id ) + time_check(self) response = requests.get( CLOCKIFY_ENDPOINT + action_url, headers=self.headers @@ -264,6 +288,7 @@ class ClockifyAPI: "taskId": task_id, "tagIds": tag_ids } + time_check(self) response = requests.post( CLOCKIFY_ENDPOINT + action_url, headers=self.headers, @@ -281,6 +306,7 @@ class ClockifyAPI: action_url = 'workspaces/{}/timeEntries/inProgress'.format( workspace_id ) + time_check(self) response = requests.get( CLOCKIFY_ENDPOINT + action_url, headers=self.headers @@ -311,6 +337,7 @@ class ClockifyAPI: "tagIds": current["tagIds"], "end": self.get_current_time() } + time_check(self) response = requests.put( CLOCKIFY_ENDPOINT + action_url, headers=self.headers, @@ -324,6 +351,7 @@ class ClockifyAPI: if workspace_id is None: workspace_id = self.workspace_id action_url = 'workspaces/{}/timeEntries/'.format(workspace_id) + time_check(self) response = requests.get( CLOCKIFY_ENDPOINT + action_url, headers=self.headers @@ -336,6 +364,7 @@ class ClockifyAPI: action_url = 'workspaces/{}/timeEntries/{}'.format( workspace_id, tid ) + time_check(self) response = requests.delete( CLOCKIFY_ENDPOINT + action_url, headers=self.headers @@ -357,6 +386,7 @@ class ClockifyAPI: "color": "#f44336", "billable": "true" } + time_check(self) response = requests.post( CLOCKIFY_ENDPOINT + action_url, headers=self.headers, @@ -367,6 +397,7 @@ class ClockifyAPI: def add_workspace(self, name): action_url = 'workspaces/' body = {"name": name} + time_check(self) response = requests.post( CLOCKIFY_ENDPOINT + action_url, headers=self.headers, @@ -386,6 +417,7 @@ class ClockifyAPI: "name": name, "projectId": project_id } + time_check(self) response = requests.post( CLOCKIFY_ENDPOINT + action_url, headers=self.headers, @@ -400,6 +432,7 @@ class ClockifyAPI: body = { "name": name } + time_check(self) response = requests.post( CLOCKIFY_ENDPOINT + action_url, headers=self.headers, @@ -415,6 +448,7 @@ class ClockifyAPI: action_url = '/workspaces/{}/projects/{}'.format( workspace_id, project_id ) + time_check(self) response = requests.delete( CLOCKIFY_ENDPOINT + action_url, headers=self.headers, From 50422e3b057e4de54e56528497b8b1d671c87f48 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 24 Jul 2020 17:26:41 +0200 Subject: [PATCH 182/190] reset matching profiles if profile value is higher than previous --- pype/plugins/global/publish/integrate_new.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index d151cfc608..6cb38c282c 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -740,6 +740,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): value += 1 if value > highest_value: + matching_profiles = {} highest_value = value if value == highest_value: From b7cf6679248a169f8bff0323a48014cb0e139433 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Jul 2020 10:03:22 +0200 Subject: [PATCH 183/190] show the version string all the time --- pype/tools/tray/pype_tray.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 3d6d6d473b..9537b62581 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -120,22 +120,18 @@ class TrayManager: config_file_path = os.path.join( os.environ["PYPE_SETUP_PATH"], "pypeapp", "config.ini" ) - if not os.path.exists(config_file_path): - return - subversion = None - client_name = None + default_config = {} + if os.path.exists(config_file_path): + config = configparser.ConfigParser() + config.read(config_file_path) + try: + default_config = config["CLIENT"] + except Exception: + pass - config = configparser.ConfigParser() - config.read(config_file_path) - try: - default_config = config["CLIENT"] - except Exception: - default_config = {} subversion = default_config.get("subversion") client_name = default_config.get("client_name") - if not subversion and not client_name: - return version_string = pype.version.__version__ if subversion: From 702ad110235885020a3291a58e3602f51639ab26 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 27 Jul 2020 12:13:45 +0200 Subject: [PATCH 184/190] fix(celaction): version up --- pype/plugins/celaction/publish/integrate_version_up.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/celaction/publish/integrate_version_up.py b/pype/plugins/celaction/publish/integrate_version_up.py index 7fb1efa8aa..1822ceabcb 100644 --- a/pype/plugins/celaction/publish/integrate_version_up.py +++ b/pype/plugins/celaction/publish/integrate_version_up.py @@ -4,9 +4,9 @@ import pyblish.api class VersionUpScene(pyblish.api.ContextPlugin): - order = pyblish.api.IntegratorOrder + order = pyblish.api.IntegratorOrder + 0.5 label = 'Version Up Scene' - families = ['scene'] + families = ['workfile'] optional = True active = True From f52a1de2d1ccb803b929c7b2e9fac5b1806a565e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Jul 2020 12:25:38 +0200 Subject: [PATCH 185/190] fix stored project entity in collect ftrack api --- pype/plugins/ftrack/publish/collect_ftrack_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/ftrack/publish/collect_ftrack_api.py b/pype/plugins/ftrack/publish/collect_ftrack_api.py index 151b8882a3..bbda6da3b0 100644 --- a/pype/plugins/ftrack/publish/collect_ftrack_api.py +++ b/pype/plugins/ftrack/publish/collect_ftrack_api.py @@ -96,6 +96,6 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): task_entity = None self.log.warning("Task name is not set.") - context.data["ftrackProject"] = asset_entity + context.data["ftrackProject"] = project_entity context.data["ftrackEntity"] = asset_entity context.data["ftrackTask"] = task_entity From 6f2132b0aa94d412ef37a0e28b6abaca217d2fdf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Jul 2020 12:26:10 +0200 Subject: [PATCH 186/190] added function _asset_version_status for getting statuses --- .../plugins/ftrack/publish/integrate_ftrack_api.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_api.py b/pype/plugins/ftrack/publish/integrate_ftrack_api.py index cd94b2a150..59740529bb 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_api.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_api.py @@ -54,8 +54,20 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): self.log.debug(query) return query - def process(self, instance): + def _asset_version_status(self, instance, session): + status_name = instance.context.data.get("ftrackStatus") + if not status_name: + return None + project_entity = instance.context.data.get("ftrackProject") + project_schema = project_entity["project_schema"] + asset_version_statuses = project_schema.get_statuses("AssetVersion") + asset_version_statuses_by_low_name = { + status["name"].lower(): status for status in asset_version_statuses + } + return asset_version_statuses_by_low_name.get(status_name.lower()) + + def process(self, instance): session = instance.context.data["ftrackSession"] if instance.data.get("ftrackTask"): task = instance.data["ftrackTask"] From 94be335e91f3a7c154db5c4860d35d7c37cb1508 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Jul 2020 12:27:37 +0200 Subject: [PATCH 187/190] change staus of asset version if is set --- .../ftrack/publish/integrate_ftrack_api.py | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_api.py b/pype/plugins/ftrack/publish/integrate_ftrack_api.py index 59740529bb..e5f62a93ea 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_api.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_api.py @@ -90,9 +90,11 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): info_msg += ", metadata: {metadata}." used_asset_versions = [] + + asset_version_status = self._asset_version_status(instance, session) + # Iterate over components and publish for data in instance.data.get("ftrackComponentsList", []): - # AssetType # Get existing entity. assettype_data = {"short": "upload"} @@ -106,9 +108,9 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): # Create a new entity if none exits. if not assettype_entity: assettype_entity = session.create("AssetType", assettype_data) - self.log.debug( - "Created new AssetType with data: ".format(assettype_data) - ) + self.log.debug("Created new AssetType with data: {}".format( + assettype_data + )) # Asset # Get existing entity. @@ -215,6 +217,17 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): assetversion_entity["id"], str(asset_version_comment) )) + if asset_version_status: + assetversion_entity["status"] = asset_version_status + try: + session.commit() + except Exception: + session.rollback() + status_name = instance.context.data["ftrackStatus"] + self.log.warning(( + "Couldn't set status \"{0}\" to AssetVersion \"{1}\"." + ).format(status_name, assetversion_entity["id"])) + # Adding Custom Attributes for attr, val in assetversion_cust_attrs.items(): if attr in assetversion_entity["custom_attributes"]: From d14975d9b1899594d0e7553974f385f80524f010 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Jul 2020 12:46:25 +0200 Subject: [PATCH 188/190] change status on task not on asset version --- .../ftrack/publish/integrate_ftrack_api.py | 59 +++++++++++++------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_api.py b/pype/plugins/ftrack/publish/integrate_ftrack_api.py index e5f62a93ea..0c4c6d49b5 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_api.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_api.py @@ -54,18 +54,50 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): self.log.debug(query) return query - def _asset_version_status(self, instance, session): + def _set_task_status(self, instance, task_entity, session): + project_entity = instance.context.data.get("ftrackProject") + if not project_entity: + self.log.info("Task status won't be set, project is not known.") + return + + if not task_entity: + self.log.info("Task status won't be set, task is not known.") + return + status_name = instance.context.data.get("ftrackStatus") if not status_name: - return None + self.log.info("Ftrack status name is not set.") + return + + self.log.debug( + "Ftrack status name will be (maybe) set to \"{}\"".format( + status_name + ) + ) - project_entity = instance.context.data.get("ftrackProject") project_schema = project_entity["project_schema"] - asset_version_statuses = project_schema.get_statuses("AssetVersion") - asset_version_statuses_by_low_name = { - status["name"].lower(): status for status in asset_version_statuses + task_statuses = project_schema.get_statuses( + "Task", task_entity["type_id"] + ) + task_statuses_by_low_name = { + status["name"].lower(): status for status in task_statuses } - return asset_version_statuses_by_low_name.get(status_name.lower()) + status = task_statuses_by_low_name.get(status_name.lower()) + if not status: + self.log.warning(( + "Task status \"{}\" won't be set," + " status is now allowed on task type \"{}\"." + ).format(status_name, task_entity["type"]["name"])) + return + + self.log.info("Setting task status to \"{}\"".format(status_name)) + task_entity["status"] = status + try: + session.commit() + except Exception: + tp, value, tb = sys.exc_info() + session.rollback() + six.reraise(tp, value, tb) def process(self, instance): session = instance.context.data["ftrackSession"] @@ -91,7 +123,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): used_asset_versions = [] - asset_version_status = self._asset_version_status(instance, session) + self._set_task_status(instance, task, session) # Iterate over components and publish for data in instance.data.get("ftrackComponentsList", []): @@ -217,17 +249,6 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): assetversion_entity["id"], str(asset_version_comment) )) - if asset_version_status: - assetversion_entity["status"] = asset_version_status - try: - session.commit() - except Exception: - session.rollback() - status_name = instance.context.data["ftrackStatus"] - self.log.warning(( - "Couldn't set status \"{0}\" to AssetVersion \"{1}\"." - ).format(status_name, assetversion_entity["id"])) - # Adding Custom Attributes for attr, val in assetversion_cust_attrs.items(): if attr in assetversion_entity["custom_attributes"]: From eec1d35e3f60440e3386bdcbbb66f7aa6a468ba0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 27 Jul 2020 13:04:58 +0200 Subject: [PATCH 189/190] skip scene for changing version to taks statuses --- pype/modules/ftrack/events/event_version_to_task_statuses.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/modules/ftrack/events/event_version_to_task_statuses.py b/pype/modules/ftrack/events/event_version_to_task_statuses.py index 3ff986f9c6..fdb48cbc37 100644 --- a/pype/modules/ftrack/events/event_version_to_task_statuses.py +++ b/pype/modules/ftrack/events/event_version_to_task_statuses.py @@ -84,6 +84,9 @@ class VersionToTaskStatus(BaseEvent): if not task: continue + if version["asset"]["type"]["short"].lower() == "scene": + continue + project_schema = task["project"]["project_schema"] # Get all available statuses for Task statuses = project_schema.get_statuses("Task", task["type_id"]) From c05f3060ba1f402f41a2dce573d11da219dbf818 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 27 Jul 2020 13:14:09 +0200 Subject: [PATCH 190/190] version bump --- pype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/version.py b/pype/version.py index 1c622223ba..7f6646a762 100644 --- a/pype/version.py +++ b/pype/version.py @@ -1 +1 @@ -__version__ = "2.10.0" +__version__ = "2.11.0"