From 685069a9ab63ecb047819bb0e4fa20bcbd293ae4 Mon Sep 17 00:00:00 2001 From: antirotor Date: Tue, 19 Feb 2019 23:47:33 +0100 Subject: [PATCH 01/99] new: validator for overlapping mesh uvs --- .../publish/validate_mesh_overlapping_uvs.py | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py diff --git a/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py b/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py new file mode 100644 index 0000000000..ca8faf60a9 --- /dev/null +++ b/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py @@ -0,0 +1,128 @@ +from maya import cmds + +import pyblish.api +import pype.api +import pype.maya.action + + +class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin): + """Validate the current mesh overlapping UVs. + + It validates whether the current UVs are overlapping or not. + It is optional to warn publisher about it. + """ + + order = pype.api.ValidateMeshOrder + hosts = ['maya'] + families = ['model'] + category = 'geometry' + label = 'Mesh Has Overlapping UVs' + actions = [pype.maya.action.SelectInvalidAction] + optional = True + + @classmethod + def _has_overlapping_uvs(cls, node): + + allUvSets = cmds.polyUVSet(q=1, auv=1) + # print allUvSets + currentTool = cmds.currentCtx() + cmds.setToolTo('selectSuperContext') + biglist = cmds.ls( + cmds.polyListComponentConversion(node, tf=True), fl=True) + shells = [] + overlappers = [] + bounds = [] + for uvset in allUvSets: + # print uvset + while len(biglist) > 0: + cmds.select(biglist[0], r=True) + # cmds.polySelectConstraint(t=0) + # cmds.polySelectConstraint(sh=1,m=2) + # cmds.polySelectConstraint(sh=0,m=0) + aShell = cmds.ls(sl=True, fl=True) + shells.append(aShell) + biglist = list(set(biglist) - set(aShell)) + cmds.setToolTo(currentTool) + cmds.select(clear=True) + # shells = [ [faces in uv shell 1], [faces in shell 2], [etc] ] + + for faces in shells: + shellSets = cmds.polyListComponentConversion( + faces, ff=True, tuv=True) + if shellSets != []: + uv = cmds.polyEditUV(shellSets, q=True) + + uMin = uv[0] + uMax = uv[0] + vMin = uv[1] + vMax = uv[1] + for i in range(len(uv)/2): + if uv[i*2] < uMin: + uMin = uv[i*2] + if uv[i*2] > uMax: + uMax = uv[i*2] + if uv[i*2+1] < vMin: + vMin = uv[i*2+1] + if uv[i*2+1] > vMax: + vMax = uv[i*2+1] + bounds.append([[uMin, uMax], [vMin, vMax]]) + else: + return False + + for a in range(len(shells)): + for b in range(a): + # print "b",b + if bounds != []: + # print bounds + aL = bounds[a][0][0] + aR = bounds[a][0][1] + aT = bounds[a][1][1] + aB = bounds[a][1][0] + + bL = bounds[b][0][0] + bR = bounds[b][0][1] + bT = bounds[b][1][1] + bB = bounds[b][1][0] + + overlaps = True + if aT < bB: # A entirely below B + overlaps = False + + if aB > bT: # A entirely above B + overlaps = False + + if aR < bL: # A entirely right of B + overlaps = False + + if aL > bR: # A entirely left of B + overlaps = False + + if overlaps: + overlappers.extend(shells[a]) + overlappers.extend(shells[b]) + else: + return False + pass + + if overlappers: + return True + else: + return False + + @classmethod + def get_invalid(cls, instance): + invalid = [] + + for node in cmds.ls(instance, type='mesh'): + if cls._has_overlapping_uvs(node): + invalid.append(node) + + return invalid + + def process(self, instance): + + invalid = self.get_invalid(instance) + if invalid: + raise RuntimeError("Meshes found with overlapping " + "UVs: {0}".format(invalid)) + pass From 1aa92f3de6692929051e329e4a15a951d3c01df1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 4 Mar 2019 15:41:06 +0100 Subject: [PATCH 02/99] integrate can handle with hardlinks --- pype/plugins/global/publish/integrate.py | 40 ++++++++++++++++++++++-- pype/plugins/maya/create/create_look.py | 3 ++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/publish/integrate.py b/pype/plugins/global/publish/integrate.py index be7fc3bcf3..97d0451091 100644 --- a/pype/plugins/global/publish/integrate.py +++ b/pype/plugins/global/publish/integrate.py @@ -5,6 +5,7 @@ import shutil import errno import pyblish.api from avalon import api, io +from avalon.vendor import filelink log = logging.getLogger(__name__) @@ -91,6 +92,13 @@ class IntegrateAsset(pyblish.api.InstancePlugin): self.log.debug("Establishing staging directory @ %s" % stagingdir) + # Ensure at least one file is set up for transfer in staging dir. + files = instance.data.get("files", []) + assert files, "Instance has no files to transfer" + assert isinstance(files, (list, tuple)), ( + "Instance 'files' must be a list, got: {0}".format(files) + ) + project = io.find_one({"type": "project"}) asset = io.find_one({"type": "asset", @@ -271,12 +279,22 @@ class IntegrateAsset(pyblish.api.InstancePlugin): instance: the instance to integrate """ - transfers = instance.data["transfers"] + transfers = instance.data.get("transfers", list()) for src, dest in transfers: self.log.info("Copying file .. {} -> {}".format(src, dest)) self.copy_file(src, dest) + # Produce hardlinked copies + # Note: hardlink can only be produced between two files on the same + # server/disk and editing one of the two will edit both files at once. + # As such it is recommended to only make hardlinks between static files + # to ensure publishes remain safe and non-edited. + hardlinks = instance.data.get("hardlinks", list()) + for src, dest in hardlinks: + self.log.info("Hardlinking file .. {} -> {}".format(src, dest)) + self.hardlink_file(src, dest) + def copy_file(self, src, dst): """ Copy given source to destination @@ -299,6 +317,20 @@ class IntegrateAsset(pyblish.api.InstancePlugin): shutil.copy(src, dst) + def hardlink_file(self, src, dst): + + dirname = os.path.dirname(dst) + try: + os.makedirs(dirname) + except OSError as e: + if e.errno == errno.EEXIST: + pass + else: + self.log.critical("An unexpected error occurred.") + raise + + filelink.create(src, dst, filelink.HARDLINK) + def get_subset(self, asset, instance): subset = io.find_one({"type": "subset", @@ -362,7 +394,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): families.append(instance_family) families += current_families - self.log.debug("Registered roor: {}".format(api.registered_root())) + self.log.debug("Registered root: {}".format(api.registered_root())) # create relative source path for DB try: source = instance.data['source'] @@ -382,7 +414,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "fps": context.data.get("fps")} # Include optional data if present in - optionals = ["startFrame", "endFrame", "step", "handles"] + optionals = [ + "startFrame", "endFrame", "step", "handles", "sourceHashes" + ] for key in optionals: if key in instance.data: version_data[key] = instance.data[key] diff --git a/pype/plugins/maya/create/create_look.py b/pype/plugins/maya/create/create_look.py index 32cda3a28e..299fbafe02 100644 --- a/pype/plugins/maya/create/create_look.py +++ b/pype/plugins/maya/create/create_look.py @@ -15,3 +15,6 @@ class CreateLook(avalon.maya.Creator): super(CreateLook, self).__init__(*args, **kwargs) self.data["renderlayer"] = lib.get_current_renderlayer() + + # Whether to automatically convert the textures to .tx upon publish. + self.data["maketx"] = True From 4b65b40c10e64b6d2638fa029240245f4caabeb4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 4 Mar 2019 15:48:59 +0100 Subject: [PATCH 03/99] working version, plugin creates hardlink with hash --- .../publish/collect_assumed_destination.py | 59 +---- pype/plugins/maya/publish/collect_look.py | 2 +- pype/plugins/maya/publish/extract_look.py | 227 +++++++++++++++++- .../maya/publish/validate_resources.py | 48 +++- .../maya/publish/validate_transfers.py | 45 ---- 5 files changed, 263 insertions(+), 118 deletions(-) delete mode 100644 pype/plugins/maya/publish/validate_transfers.py diff --git a/pype/plugins/global/publish/collect_assumed_destination.py b/pype/plugins/global/publish/collect_assumed_destination.py index d5d3d9a846..96f7e4b585 100644 --- a/pype/plugins/global/publish/collect_assumed_destination.py +++ b/pype/plugins/global/publish/collect_assumed_destination.py @@ -1,5 +1,5 @@ -import pyblish.api import os +import pyblish.api from avalon import io, api @@ -16,53 +16,7 @@ class CollectAssumedDestination(pyblish.api.InstancePlugin): if instance.data["family"] in ef]: return - self.create_destination_template(instance) - - template_data = instance.data["assumedTemplateData"] - # template = instance.data["template"] - - anatomy = instance.context.data['anatomy'] - # template = anatomy.publish.path - anatomy_filled = anatomy.format(template_data) - mock_template = anatomy_filled.publish.path - - # For now assume resources end up in a "resources" folder in the - # published folder - mock_destination = os.path.join(os.path.dirname(mock_template), - "resources") - - # Clean the path - mock_destination = os.path.abspath(os.path.normpath(mock_destination)) - - # Define resource destination and transfers - resources = instance.data.get("resources", list()) - transfers = instance.data.get("transfers", list()) - for resource in resources: - - # Add destination to the resource - source_filename = os.path.basename(resource["source"]) - destination = os.path.join(mock_destination, source_filename) - - # Force forward slashes to fix issue with software unable - # to work correctly with backslashes in specific scenarios - # (e.g. escape characters in PLN-151 V-Ray UDIM) - destination = destination.replace("\\", "/") - - resource['destination'] = destination - - # Collect transfers for the individual files of the resource - # e.g. all individual files of a cache or UDIM textures. - files = resource['files'] - for fsrc in files: - fname = os.path.basename(fsrc) - fdest = os.path.join(mock_destination, fname) - transfers.append([fsrc, fdest]) - - instance.data["resources"] = resources - instance.data["transfers"] = transfers - - def create_destination_template(self, instance): - """Create a filepath based on the current data available + """Create a destination filepath based on the current data available Example template: {root}/{project}/{silo}/{asset}/publish/{subset}/v{version:0>3}/ @@ -84,7 +38,7 @@ class CollectAssumedDestination(pyblish.api.InstancePlugin): projection={"config": True, "data": True}) template = project["config"]["template"]["publish"] - # anatomy = instance.context.data['anatomy'] + anatomy = instance.context.data['anatomy'] asset = io.find_one({"type": "asset", "name": asset_name, @@ -126,5 +80,10 @@ class CollectAssumedDestination(pyblish.api.InstancePlugin): "hierarchy": hierarchy, "representation": "TEMP"} - instance.data["assumedTemplateData"] = template_data instance.data["template"] = template + instance.data["assumedTemplateData"] = template_data + + # We take the parent folder of representation 'filepath' + instance.data["assumedDestination"] = os.path.dirname( + (anatomy.format(template_data)).publish.path + ) diff --git a/pype/plugins/maya/publish/collect_look.py b/pype/plugins/maya/publish/collect_look.py index 0cb17645b1..cb15976772 100644 --- a/pype/plugins/maya/publish/collect_look.py +++ b/pype/plugins/maya/publish/collect_look.py @@ -275,7 +275,7 @@ class CollectLook(pyblish.api.InstancePlugin): if looksets: for look in looksets: for at in shaderAttrs: - con = cmds.listConnections("{}.{}".format("aiStandard_SG", at)) + con = cmds.listConnections("{}.{}".format(look, at)) if con: materials.extend(con) diff --git a/pype/plugins/maya/publish/extract_look.py b/pype/plugins/maya/publish/extract_look.py index a30b1fe7d5..3aeb98477b 100644 --- a/pype/plugins/maya/publish/extract_look.py +++ b/pype/plugins/maya/publish/extract_look.py @@ -2,16 +2,95 @@ import os import json import tempfile import contextlib +import subprocess from collections import OrderedDict from maya import cmds import pyblish.api import avalon.maya +from avalon import io import pype.api import pype.maya.lib as lib +# Modes for transfer +COPY = 1 +HARDLINK = 2 + + +def source_hash(filepath, *args): + """Generate simple identifier for a source file. + This is used to identify whether a source file has previously been + processe into the pipeline, e.g. a texture. + The hash is based on source filepath, modification time and file size. + This is only used to identify whether a specific source file was already + published before from the same location with the same modification date. + We opt to do it this way as opposed to Avalanch C4 hash as this is much + faster and predictable enough for all our production use cases. + Args: + filepath (str): The source file path. + You can specify additional arguments in the function + to allow for specific 'processing' values to be included. + """ + # We replace dots with comma because . cannot be a key in a pymongo dict. + file_name = os.path.basename(filepath) + time = str(os.path.getmtime(filepath)) + size = str(os.path.getsize(filepath)) + return "|".join([ + file_name, time, size + ] + list(args)).replace(".", ",") + + +def find_paths_by_hash(texture_hash): + # Find the texture hash key in the dictionary and all paths that + # originate from it. + key = "data.sourceHashes.{0}".format(texture_hash) + return io.distinct(key, {"type": "version"}) + + +def maketx(source, destination, *args): + """Make .tx using maketx with some default settings. + The settings are based on default as used in Arnold's + txManager in the scene. + This function requires the `maketx` executable to be + on the `PATH`. + Args: + source (str): Path to source file. + destination (str): Writing destination path. + """ + + cmd = [ + "maketx", + "-v", # verbose + "-u", # update mode + # unpremultiply before conversion (recommended when alpha present) + "--unpremult", + # use oiio-optimized settings for tile-size, planarconfig, metadata + "--oiio" + ] + cmd.extend(args) + cmd.extend([ + "-o", destination, + source + ]) + + CREATE_NO_WINDOW = 0x08000000 + try: + out = subprocess.check_output( + cmd, + stderr=subprocess.STDOUT, + creationflags=CREATE_NO_WINDOW + ) + except subprocess.CalledProcessError as exc: + print exc + print out + import traceback + traceback.print_exc() + raise + + return out + @contextlib.contextmanager def no_workspace_dir(): @@ -79,12 +158,53 @@ class ExtractLook(pype.api.Extractor): relationships = lookdata["relationships"] sets = relationships.keys() + # Extract the textures to transfer, possibly convert with maketx and + # remap the node paths to the destination path. Note that a source + # might be included more than once amongst the resources as they could + # be the input file to multiple nodes. resources = instance.data["resources"] + do_maketx = instance.data.get("maketx", False) + # Collect all unique files used in the resources + files = set() + for resource in resources: + files.update(os.path.normpath(f) for f in resource["files"]) + + # Process the resource files + transfers = list() + hardlinks = list() + hashes = dict() + for filepath in files: + source, mode, hash = self._process_texture( + filepath, do_maketx, staging=dir_path + ) + destination = self.resource_destination( + instance, source, do_maketx + ) + if mode == COPY: + transfers.append((source, destination)) + elif mode == HARDLINK: + hardlinks.append((source, destination)) + + # Store the hashes from hash to destination to include in the + # database + hashes[hash] = destination + + # Remap the resources to the destination path (change node attributes) + destinations = dict() remap = OrderedDict() # needs to be ordered, see color space values for resource in resources: + source = os.path.normpath(resource["source"]) + if source not in destinations: + # Cache destination as source resource might be included + # multiple times + destinations[source] = self.resource_destination( + instance, source, do_maketx + ) + + # Remap file node filename to destination attr = resource['attribute'] - remap[attr] = resource['destination'] + remap[attr] = destinations[source] # Preserve color space values (force value after filepath change) # This will also trigger in the same order at end of context to @@ -107,15 +227,17 @@ class ExtractLook(pype.api.Extractor): with lib.attribute_values(remap): with avalon.maya.maintained_selection(): cmds.select(sets, noExpand=True) - cmds.file(maya_path, - force=True, - typ="mayaAscii", - exportSelected=True, - preserveReferences=False, - channels=True, - constraints=True, - expressions=True, - constructionHistory=True) + cmds.file( + maya_path, + force=True, + typ="mayaAscii", + exportSelected=True, + preserveReferences=False, + channels=True, + constraints=True, + expressions=True, + constructionHistory=True + ) # Write the JSON data self.log.info("Extract json..") @@ -127,9 +249,90 @@ class ExtractLook(pype.api.Extractor): if "files" not in instance.data: instance.data["files"] = list() + if "hardlinks" not in instance.data: + instance.data["hardlinks"] = list() + if "transfers" not in instance.data: + instance.data["transfers"] = list() instance.data["files"].append(maya_fname) instance.data["files"].append(json_fname) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - maya_path)) + # Set up the resources transfers/links for the integrator + instance.data["transfers"].extend(transfers) + instance.data["hardlinks"].extend(hardlinks) + + # Source hash for the textures + instance.data["sourceHashes"] = hashes + + self.log.info("Extracted instance '%s' to: %s" % ( + instance.name, maya_path) + ) + + def resource_destination(self, instance, filepath, do_maketx): + + # Compute destination location + basename, ext = os.path.splitext(os.path.basename(filepath)) + + # If maketx then the texture will always end with .tx + if do_maketx: + ext = ".tx" + + return os.path.join( + instance.data["assumedDestination"], + "resources", + basename + ext + ) + + def _process_texture(self, filepath, do_maketx, staging): + """Process a single texture file on disk for publishing. + This will: + 1. Check whether it's already published, if so it will do hardlink + 2. If not published and maketx is enabled, generate a new .tx file. + 3. Compute the destination path for the source file. + Args: + filepath (str): The source file path to process. + do_maketx (bool): Whether to produce a .tx file + Returns: + """ + + fname, ext = os.path.splitext(os.path.basename(filepath)) + + args = [] + if do_maketx: + args.append("maketx") + texture_hash = source_hash(filepath, *args) + + # If source has been published before with the same settings, + # then don't reprocess but hardlink from the original + existing = find_paths_by_hash(texture_hash) + if existing: + self.log.info("Found hash in database, preparing hardlink..") + source = next((p for p in existing if os.path.exists(p)), None) + if filepath: + return source, HARDLINK, texture_hash + else: + self.log.warning( + "Paths not found on disk, " + "skipping hardlink: %s" % (existing,) + ) + + if do_maketx and ext != ".tx": + # Produce .tx file in staging if source file is not .tx + converted = os.path.join( + staging, + "resources", + fname + ".tx" + ) + + # Ensure folder exists + if not os.path.exists(os.path.dirname(converted)): + os.makedirs(os.path.dirname(converted)) + + self.log.info("Generating .tx file for %s .." % filepath) + maketx(filepath, converted, + # Include `source-hash` as string metadata + "-sattrib", "sourceHash", texture_hash) + + return converted, COPY, texture_hash + + return filepath, COPY, texture_hash diff --git a/pype/plugins/maya/publish/validate_resources.py b/pype/plugins/maya/publish/validate_resources.py index bc10d3003c..47a94e7529 100644 --- a/pype/plugins/maya/publish/validate_resources.py +++ b/pype/plugins/maya/publish/validate_resources.py @@ -1,8 +1,9 @@ +import os +from collections import defaultdict + import pyblish.api import pype.api -import os - class ValidateResources(pyblish.api.InstancePlugin): """Validates mapped resources. @@ -12,18 +13,45 @@ class ValidateResources(pyblish.api.InstancePlugin): media. This validates: - - The resources are existing files. - - The resources have correctly collected the data. + - The resources have unique filenames (without extension) """ order = pype.api.ValidateContentsOrder - label = "Resources" + label = "Resources Unique" def process(self, instance): - for resource in instance.data.get('resources', []): - # Required data - assert "source" in resource, "No source found" - assert "files" in resource, "No files from source" - assert all(os.path.exists(f) for f in resource['files']) + resources = instance.data.get("resources", []) + if not resources: + self.log.debug("No resources to validate..") + return + + basenames = defaultdict(set) + + for resource in resources: + files = resource.get("files", []) + for filename in files: + + # Use normalized paths in comparison and ignore case + # sensitivity + filename = os.path.normpath(filename).lower() + + basename = os.path.splitext(os.path.basename(filename))[0] + basenames[basename].add(filename) + + invalid_resources = list() + for basename, sources in basenames.items(): + if len(sources) > 1: + invalid_resources.extend(sources) + + self.log.error( + "Non-unique resource name: {0}" + "{0} (sources: {1})".format( + basename, + list(sources) + ) + ) + + if invalid_resources: + raise RuntimeError("Invalid resources in instance.") diff --git a/pype/plugins/maya/publish/validate_transfers.py b/pype/plugins/maya/publish/validate_transfers.py deleted file mode 100644 index 3234b2240e..0000000000 --- a/pype/plugins/maya/publish/validate_transfers.py +++ /dev/null @@ -1,45 +0,0 @@ -import pyblish.api -import pype.api -import os - -from collections import defaultdict - - -class ValidateTransfers(pyblish.api.InstancePlugin): - """Validates mapped resources. - - This validates: - - The resources all transfer to a unique destination. - - """ - - order = pype.api.ValidateContentsOrder - label = "Transfers" - - def process(self, instance): - - transfers = instance.data.get("transfers", []) - if not transfers: - return - - # Collect all destination with its sources - collected = defaultdict(set) - for source, destination in transfers: - - # Use normalized paths in comparison and ignore case sensitivity - source = os.path.normpath(source).lower() - destination = os.path.normpath(destination).lower() - - collected[destination].add(source) - - invalid_destinations = list() - for destination, sources in collected.items(): - if len(sources) > 1: - invalid_destinations.append(destination) - - self.log.error("Non-unique file transfer for resources: " - "{0} (sources: {1})".format(destination, - list(sources))) - - if invalid_destinations: - raise RuntimeError("Invalid transfers in queue.") From aadafc2c755021efd39f626fe92555c54b21f54d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 4 Mar 2019 16:58:32 +0100 Subject: [PATCH 04/99] fixed transfers error in integrate --- pype/plugins/global/publish/integrate.py | 2 ++ pype/plugins/maya/publish/extract_look.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/integrate.py b/pype/plugins/global/publish/integrate.py index 97d0451091..00096a95ee 100644 --- a/pype/plugins/global/publish/integrate.py +++ b/pype/plugins/global/publish/integrate.py @@ -178,6 +178,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Each should be a single representation (as such, a single extension) representations = [] destination_list = [] + if 'transfers' not in instance.data: + instance.data['transfers'] = [] for files in instance.data["files"]: diff --git a/pype/plugins/maya/publish/extract_look.py b/pype/plugins/maya/publish/extract_look.py index 3aeb98477b..f6fdda8593 100644 --- a/pype/plugins/maya/publish/extract_look.py +++ b/pype/plugins/maya/publish/extract_look.py @@ -38,7 +38,9 @@ def source_hash(filepath, *args): time = str(os.path.getmtime(filepath)) size = str(os.path.getsize(filepath)) return "|".join([ - file_name, time, size + file_name, + time, + size ] + list(args)).replace(".", ",") From d5eb65100f5d27820b3995310e7c0242fa669da1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Mar 2019 11:51:20 +0100 Subject: [PATCH 05/99] added first version of clockifyAPI class --- pype/clockify/__init__.py | 3 + pype/clockify/clockify.py | 188 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 pype/clockify/__init__.py create mode 100644 pype/clockify/clockify.py diff --git a/pype/clockify/__init__.py b/pype/clockify/__init__.py new file mode 100644 index 0000000000..620e7e8a5c --- /dev/null +++ b/pype/clockify/__init__.py @@ -0,0 +1,3 @@ +from .clockify import ClockifyAPI + +__all__ = ['ClockifyAPI'] diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py new file mode 100644 index 0000000000..50b261fd2f --- /dev/null +++ b/pype/clockify/clockify.py @@ -0,0 +1,188 @@ +import os +import requests +import json +import datetime +import appdirs + + +class ClockifyAPI: + 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) + workspace = None + + def __init__(self, workspace=None, debug=False): + self.debug = debug + self.set_api() + if workspace is not None: + self.set_workspace(workspace) + + def set_api(self): + api_key = self.get_api_key() + if api_key is not None: + self.headers["X-Api-Key"] = api_key + return + + raise ValueError('Api key is not set') + + def set_workspace(self, name): + all_workspaces = self.get_workspaces() + if name in all_workspaces: + self.workspace = name + return + + def get_api_key(self): + credentials = None + try: + file = open(self.fpath, 'r') + credentials = json.load(file).get('api_key', None) + except Exception: + file = open(self.fpath, 'w') + file.close() + return credentials + + def get_workspaces(self): + action_url = 'workspaces/' + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + return { + workspace["name"]: workspace["id"] for workspace in response.json() + } + + def get_projects(self, workspace=None): + if workspace is None: + workspace = self.workspace + action_url = 'workspaces/{}/projects/'.format(workspace) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + + return { + project["name"]: project["id"] for project in response.json() + } + + def print_json(self, inputjson): + print(json.dumps(inputjson, indent=2)) + + def get_current_time(self): + return str(datetime.datetime.utcnow().isoformat())+'Z' + + def start_time_entry( + self, description, project_id, task_id, billable="true", workspace=None + ): + if workspace is None: + workspace = self.workspace + action_url = 'workspaces/{}/timeEntries/'.format(workspace) + start = self.get_current_time() + body = { + "start": start, + "billable": billable, + "description": description, + "projectId": project_id, + "taskId": task_id, + "tagIds": None + } + response = requests.post( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + + def get_in_progress(self, workspace=None): + if workspace is None: + workspace = self.workspace + action_url = 'workspaces/{}/timeEntries/inProgress'.format(workspace) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + return response.json() + + def finish_time_entry(self, workspace=None): + if workspace is None: + workspace = self.workspace + current = self.get_in_progress(workspace) + current_id = current["id"] + action_url = 'workspaces/{}/timeEntries/{}'.format( + workspace, current_id + ) + body = { + "start": current["timeInterval"]["start"], + "billable": current["billable"], + "description": current["description"], + "projectId": current["projectId"], + "taskId": current["taskId"], + "tagIds": current["tagIds"], + "end": self.get_current_time() + } + response = requests.put( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + + def get_time_entries(self, workspace=None): + if workspace is None: + workspace = self.workspace + action_url = 'workspaces/{}/timeEntries/'.format(workspace) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + return response.json()[:10] + + def remove_time_entry(self, tid, workspace=None): + if workspace is None: + workspace = self.workspace + action_url = 'workspaces/{}/timeEntries/{tid}'.format(workspace) + response = requests.delete( + self.endpoint + action_url, + headers=self.headers + ) + return response.json() + + def add_project(self, name, workspace=None): + if workspace is None: + workspace = self.workspace + action_url = 'workspaces/{}/projects/'.format(workspace) + body = { + "name": name, + "clientId": "", + "isPublic": "false", + "estimate": None, + "color": None, + "billable": None + } + response = requests.post( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + + def add_workspace(self, name): + action_url = 'workspaces/' + body = {"name": name} + response = requests.post( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + + +def main(): + clockify = ClockifyAPI() + from pprint import pprint + pprint(clockify.get_workspaces()) + + +if __name__ == "__main__": + main() From 393d2acc37e2e5e1a64166c03fe89ad962a03cce Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Mar 2019 19:47:01 +0100 Subject: [PATCH 06/99] added widget for settings --- pype/clockify/widget_settings.py | 150 +++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 pype/clockify/widget_settings.py diff --git a/pype/clockify/widget_settings.py b/pype/clockify/widget_settings.py new file mode 100644 index 0000000000..8b8abb25da --- /dev/null +++ b/pype/clockify/widget_settings.py @@ -0,0 +1,150 @@ +import os +from app.vendor.Qt import QtCore, QtGui, QtWidgets +from app import style + + +class ClockifySettings(QtWidgets.QWidget): + + SIZE_W = 300 + SIZE_H = 130 + + loginSignal = QtCore.Signal(object, object, object) + + def __init__(self, main_parent=None, parent=None, optional=True): + + super(ClockifySettings, self).__init__() + + self.parent = parent + self.main_parent = main_parent + self.clockapi = parent.clockapi + self.optional = optional + self.validated = False + + # Icon + if hasattr(parent, 'icon'): + self.setWindowIcon(self.parent.icon) + elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'): + self.setWindowIcon(self.parent.parent.icon) + else: + pype_setup = os.getenv('PYPE_SETUP_ROOT') + items = [pype_setup, "app", "resources", "icon.png"] + fname = os.path.sep.join(items) + icon = QtGui.QIcon(fname) + self.setWindowIcon(icon) + + self.setWindowFlags( + QtCore.Qt.WindowCloseButtonHint | + QtCore.Qt.WindowMinimizeButtonHint + ) + + self._translate = QtCore.QCoreApplication.translate + + # 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)) + self.setStyleSheet(style.load_stylesheet()) + + self.setLayout(self._main()) + self.setWindowTitle('Clockify settings') + + def _main(self): + self.main = QtWidgets.QVBoxLayout() + self.main.setObjectName("main") + + self.form = QtWidgets.QFormLayout() + self.form.setContentsMargins(10, 15, 10, 5) + self.form.setObjectName("form") + + self.label_api_key = QtWidgets.QLabel("Clockify API key:") + self.label_api_key.setFont(self.font) + self.label_api_key.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.label_api_key.setTextFormat(QtCore.Qt.RichText) + self.label_api_key.setObjectName("label_api_key") + + self.input_api_key = QtWidgets.QLineEdit() + self.input_api_key.setEnabled(True) + self.input_api_key.setFrame(True) + self.input_api_key.setObjectName("input_api_key") + self.input_api_key.setPlaceholderText( + self._translate("main", "e.g. XX1XxXX2x3x4xXxx") + ) + + self.error_label = QtWidgets.QLabel("") + self.error_label.setFont(self.font) + self.error_label.setTextFormat(QtCore.Qt.RichText) + self.error_label.setObjectName("error_label") + self.error_label.setWordWrap(True) + self.error_label.hide() + + self.form.addRow(self.label_api_key, self.input_api_key) + self.form.addRow(self.error_label) + + self.btn_group = QtWidgets.QHBoxLayout() + self.btn_group.addStretch(1) + self.btn_group.setObjectName("btn_group") + + self.btn_ok = QtWidgets.QPushButton("Ok") + self.btn_ok.setToolTip('Sets Clockify API Key so can Start/Stop timer') + self.btn_ok.clicked.connect(self.click_ok) + + self.btn_cancel = QtWidgets.QPushButton("Cancel") + cancel_tooltip = 'Application won\'t start' + if self.optional: + cancel_tooltip = 'Close this window' + self.btn_cancel.setToolTip(cancel_tooltip) + self.btn_cancel.clicked.connect(self._close_widget) + + self.btn_group.addWidget(self.btn_ok) + self.btn_group.addWidget(self.btn_cancel) + + self.main.addLayout(self.form) + self.main.addLayout(self.btn_group) + + return self.main + + def setError(self, msg): + self.error_label.setText(msg) + self.error_label.show() + + def invalid_input(self, entity): + entity.setStyleSheet("border: 1px solid red;") + + def click_ok(self): + api_key = self.input_api_key.text().strip() + if self.optional: + if api_key == '': + self.clockapi.save_api_key(None) + self.clockapi.set_api(api_key) + self.validated = False + self._close_widget() + return + + validation = self.clockapi.validate_api_key(api_key) + + if validation: + self.clockapi.save_api_key(api_key) + self.clockapi.set_api(api_key) + self.validated = True + self._close_widget() + else: + self.invalid_input(self.input_api_key) + self.validated = False + self.setError( + "Entered invalid API key" + ) + + def closeEvent(self, event): + event.ignore() + self._close_widget() + + def _close_widget(self): + self.hide() From cf0cebf42e13cb80fed67612b94a6020435c9e21 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Mar 2019 19:47:17 +0100 Subject: [PATCH 07/99] just little fix in assetcreator --- pype/plugins/launcher/actions/AssetCreator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/launcher/actions/AssetCreator.py b/pype/plugins/launcher/actions/AssetCreator.py index d6875bd7ff..ff06895ae0 100644 --- a/pype/plugins/launcher/actions/AssetCreator.py +++ b/pype/plugins/launcher/actions/AssetCreator.py @@ -7,7 +7,7 @@ from pype.tools import assetcreator from pype.api import Logger -log = Logger.getLogger(__name__, "aport") +log = Logger.getLogger(__name__, "asset_creator") class AssetCreator(api.Action): From 2565b08542f09caf7f9cd93badbadb62659be1bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Mar 2019 19:47:30 +0100 Subject: [PATCH 08/99] added startclockify timer to launcher actions --- .../plugins/launcher/actions/StartClockify.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 pype/plugins/launcher/actions/StartClockify.py diff --git a/pype/plugins/launcher/actions/StartClockify.py b/pype/plugins/launcher/actions/StartClockify.py new file mode 100644 index 0000000000..91adfe0185 --- /dev/null +++ b/pype/plugins/launcher/actions/StartClockify.py @@ -0,0 +1,40 @@ +from avalon import api, io +from pype.clockify import ClockifyAPI +from pype.api import Logger +log = Logger.getLogger(__name__, "start_clockify") + + +class StartClockify(api.Action): + + name = "start_clockify_timer" + label = "Start Timer - Clockify" + icon = "clockify_icon" + order = 500 + + def is_compatible(self, session): + """Return whether the action is compatible with the session""" + if "AVALON_TASK" in session: + return True + return False + + def process(self, session, **kwargs): + clockapi = ClockifyAPI() + project_name = session['AVALON_PROJECT'] + asset_name = session['AVALON_ASSET'] + task_name = session['AVALON_TASK'] + + description = asset_name + asset = io.find_one({ + 'type': 'asset', + 'name': asset_name + }) + if asset is not None: + desc_items = asset.get('data', {}).get('parents', []) + desc_items.append(asset_name) + description = '/'.join(desc_items) + + clockapi.start_time_entry( + description=description, + project_name=project_name, + task_name=task_name, + ) From 9c27a64f625ff31e14fcbb48f58fa469528c3a6f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Mar 2019 19:48:05 +0100 Subject: [PATCH 09/99] separated clockify into api and module --- pype/clockify/__init__.py | 10 +- pype/clockify/clockify.py | 220 +++++---------------- pype/clockify/clockify_api.py | 349 ++++++++++++++++++++++++++++++++++ 3 files changed, 407 insertions(+), 172 deletions(-) create mode 100644 pype/clockify/clockify_api.py diff --git a/pype/clockify/__init__.py b/pype/clockify/__init__.py index 620e7e8a5c..5f61acd751 100644 --- a/pype/clockify/__init__.py +++ b/pype/clockify/__init__.py @@ -1,3 +1,9 @@ -from .clockify import ClockifyAPI +from .clockify_api import ClockifyAPI +from .widget_settings import ClockifySettings +from .clockify import ClockifyModule -__all__ = ['ClockifyAPI'] +__all__ = [ + 'ClockifyAPI', + 'ClockifySettings', + 'ClockifyModule' +] diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py index 50b261fd2f..1541807a46 100644 --- a/pype/clockify/clockify.py +++ b/pype/clockify/clockify.py @@ -1,188 +1,68 @@ import os -import requests -import json -import datetime -import appdirs +from app import style +from app.vendor.Qt import QtWidgets +from pype.clockify import ClockifySettings, ClockifyAPI -class ClockifyAPI: - 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) - workspace = None +class ClockifyModule: - def __init__(self, workspace=None, debug=False): - self.debug = debug - self.set_api() - if workspace is not None: - self.set_workspace(workspace) + def __init__(self, main_parent=None, parent=None): + self.main_parent = main_parent + self.parent = parent + self.clockapi = ClockifyAPI() + self.widget_settings = ClockifySettings(main_parent, self) - def set_api(self): - api_key = self.get_api_key() - if api_key is not None: - self.headers["X-Api-Key"] = api_key + # Bools + self.bool_api_key_set = False + self.bool_workspace_set = False + self.bool_timer_run = False + + def start_up(self): + self.bool_api_key_set = self.clockapi.set_api() + if self.bool_api_key_set is False: + self.show_settings() return - raise ValueError('Api key is not set') - - def set_workspace(self, name): - all_workspaces = self.get_workspaces() - if name in all_workspaces: - self.workspace = name + workspace = os.environ.get('CLOCKIFY_WORKSPACE', None) + print(workspace) + self.bool_workspace_set = self.clockapi.set_workspace(workspace) + if self.bool_workspace_set is False: + # TODO show message to user + print("Nope Workspace: clockify.py - line 29") return + if self.clockapi.get_in_progress() is not None: + self.bool_timer_run = True - def get_api_key(self): - credentials = None - try: - file = open(self.fpath, 'r') - credentials = json.load(file).get('api_key', None) - except Exception: - file = open(self.fpath, 'w') - file.close() - return credentials + self.set_menu_visibility() - def get_workspaces(self): - action_url = 'workspaces/' - response = requests.get( - self.endpoint + action_url, - headers=self.headers + # Definition of Tray menu + def tray_menu(self, parent): + # Menu for Tray App + self.menu = QtWidgets.QMenu('Clockify', parent) + self.menu.setProperty('submenu', 'on') + self.menu.setStyleSheet(style.load_stylesheet()) + + # Actions + self.aShowSettings = QtWidgets.QAction( + "Settings", self.menu ) - return { - workspace["name"]: workspace["id"] for workspace in response.json() - } - - def get_projects(self, workspace=None): - if workspace is None: - workspace = self.workspace - action_url = 'workspaces/{}/projects/'.format(workspace) - response = requests.get( - self.endpoint + action_url, - headers=self.headers + self.aStopTimer = QtWidgets.QAction( + "Stop timer", self.menu ) - return { - project["name"]: project["id"] for project in response.json() - } + self.menu.addAction(self.aShowSettings) + self.menu.addAction(self.aStopTimer) - def print_json(self, inputjson): - print(json.dumps(inputjson, indent=2)) + self.aShowSettings.triggered.connect(self.show_settings) + self.aStopTimer.triggered.connect(self.clockapi.finish_time_entry) - def get_current_time(self): - return str(datetime.datetime.utcnow().isoformat())+'Z' + self.set_menu_visibility() - def start_time_entry( - self, description, project_id, task_id, billable="true", workspace=None - ): - if workspace is None: - workspace = self.workspace - action_url = 'workspaces/{}/timeEntries/'.format(workspace) - start = self.get_current_time() - body = { - "start": start, - "billable": billable, - "description": description, - "projectId": project_id, - "taskId": task_id, - "tagIds": None - } - response = requests.post( - self.endpoint + action_url, - headers=self.headers, - json=body - ) - return response.json() + return self.menu - def get_in_progress(self, workspace=None): - if workspace is None: - workspace = self.workspace - action_url = 'workspaces/{}/timeEntries/inProgress'.format(workspace) - response = requests.get( - self.endpoint + action_url, - headers=self.headers - ) - return response.json() + def show_settings(self): + self.widget_settings.input_api_key.setText(self.clockapi.get_api_key()) + self.widget_settings.show() - def finish_time_entry(self, workspace=None): - if workspace is None: - workspace = self.workspace - current = self.get_in_progress(workspace) - current_id = current["id"] - action_url = 'workspaces/{}/timeEntries/{}'.format( - workspace, current_id - ) - body = { - "start": current["timeInterval"]["start"], - "billable": current["billable"], - "description": current["description"], - "projectId": current["projectId"], - "taskId": current["taskId"], - "tagIds": current["tagIds"], - "end": self.get_current_time() - } - response = requests.put( - self.endpoint + action_url, - headers=self.headers, - json=body - ) - return response.json() - - def get_time_entries(self, workspace=None): - if workspace is None: - workspace = self.workspace - action_url = 'workspaces/{}/timeEntries/'.format(workspace) - response = requests.get( - self.endpoint + action_url, - headers=self.headers - ) - return response.json()[:10] - - def remove_time_entry(self, tid, workspace=None): - if workspace is None: - workspace = self.workspace - action_url = 'workspaces/{}/timeEntries/{tid}'.format(workspace) - response = requests.delete( - self.endpoint + action_url, - headers=self.headers - ) - return response.json() - - def add_project(self, name, workspace=None): - if workspace is None: - workspace = self.workspace - action_url = 'workspaces/{}/projects/'.format(workspace) - body = { - "name": name, - "clientId": "", - "isPublic": "false", - "estimate": None, - "color": None, - "billable": None - } - response = requests.post( - self.endpoint + action_url, - headers=self.headers, - json=body - ) - return response.json() - - def add_workspace(self, name): - action_url = 'workspaces/' - body = {"name": name} - response = requests.post( - self.endpoint + action_url, - headers=self.headers, - json=body - ) - return response.json() - - -def main(): - clockify = ClockifyAPI() - from pprint import pprint - pprint(clockify.get_workspaces()) - - -if __name__ == "__main__": - main() + def set_menu_visibility(self): + self.aStopTimer.setVisible(self.bool_timer_run) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py new file mode 100644 index 0000000000..160afe914b --- /dev/null +++ b/pype/clockify/clockify_api.py @@ -0,0 +1,349 @@ +import os +import requests +import json +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) + + def set_api(self, api_key=None): + if api_key is None: + 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 + return True + return False + + def validate_api_key(self, api_key): + test_headers = {'X-Api-Key': api_key} + action_url = 'workspaces/' + response = requests.get( + self.endpoint + action_url, + headers=test_headers + ) + if response.status_code != 200: + return False + return True + + def set_workspace(self, name=None): + if name is None: + self.workspace = None + self.workspace_id = None + return + result = self.validate_workspace(name) + if result is False: + self.workspace = None + self.workspace_id = None + return False + else: + self.workspace = name + self.workspace_id = result + return True + + def validate_workspace(self, name): + all_workspaces = self.get_workspaces() + if name in all_workspaces: + return all_workspaces[name] + return False + + def get_api_key(self): + api_key = None + try: + file = open(self.fpath, 'r') + api_key = json.load(file).get('api_key', None) + if api_key == '': + api_key = None + except Exception: + file = open(self.fpath, 'w') + file.close() + return api_key + + def save_api_key(self, api_key): + data = {'api_key': api_key} + file = open(self.fpath, 'w') + file.write(json.dumps(data)) + file.close() + + def get_workspaces(self): + action_url = 'workspaces/' + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + return { + workspace["name"]: workspace["id"] for workspace in response.json() + } + + def get_projects(self, workspace_name=None, workspace_id=None): + workspace_id = self.convert_input(workspace_id, workspace_name) + action_url = 'workspaces/{}/projects/'.format(workspace_id) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + + return { + project["name"]: project["id"] for project in response.json() + } + + def get_tags( + self, workspace_name=None, workspace_id=None + ): + workspace_id = self.convert_input(workspace_id, workspace_name) + action_url = 'workspaces/{}/tags/'.format(workspace_id) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + + return { + tag["name"]: tag["id"] for tag in response.json() + } + + def get_tasks( + self, + workspace_name=None, project_name=None, + workspace_id=None, project_id=None + ): + workspace_id = self.convert_input(workspace_id, workspace_name) + project_id = self.convert_input(project_id, project_name, 'Project') + action_url = 'workspaces/{}/projects/{}/tasks/'.format( + workspace_id, project_id + ) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + + return { + task["name"]: task["id"] for task in response.json() + } + + def get_workspace_id(self, workspace_name): + all_workspaces = self.get_workspaces() + if workspace_name not in all_workspaces: + return None + return all_workspaces[workspace_name] + + def get_project_id( + self, project_name, workspace_name=None, workspace_id=None + ): + workspace_id = self.convert_input(workspace_id, workspace_name) + all_projects = self.get_projects(workspace_id=workspace_id) + if project_name not in all_projects: + return None + return all_projects[project_name] + + def get_tag_id(self, tag_name, workspace_name=None, workspace_id=None): + workspace_id = self.convert_input(workspace_id, workspace_name) + all_tasks = self.get_tags(workspace_id=workspace_id) + if tag_name not in all_tasks: + return None + return all_tasks[tag_name] + + def get_task_id( + self, task_name, + project_name=None, workspace_name=None, + project_id=None, workspace_id=None + ): + workspace_id = self.convert_input(workspace_id, workspace_name) + project_id = self.convert_input(project_id, project_name, 'Project') + all_tasks = self.get_tasks( + workspace_id=workspace_id, project_id=project_id + ) + if task_name not in all_tasks: + return None + return all_tasks[task_name] + + def get_current_time(self): + return str(datetime.datetime.utcnow().isoformat())+'Z' + + def start_time_entry( + self, description, project_name=None, task_name=None, + billable=True, workspace_name=None, + project_id=None, task_id=None, workspace_id=None + ): + # Workspace + workspace_id = self.convert_input(workspace_id, workspace_name) + # Project + project_id = self.convert_input( + project_id, project_name, 'Project' + ) + # Task + task_id = self.convert_input(task_id, task_name, 'Task', project_id) + + # Check if is currently run another times and has same values + current = self.get_in_progress(workspace_id=workspace_id) + if current is not None: + if ( + current.get("description", None) == description and + current.get("projectId", None) == project_id and + current.get("taskId", None) == task_id + ): + self.bool_timer_run = True + return self.bool_timer_run + return False + + # Convert billable to strings + if billable: + billable = 'true' + else: + billable = 'false' + # Rest API Action + action_url = 'workspaces/{}/timeEntries/'.format(workspace_id) + start = self.get_current_time() + body = { + "start": start, + "billable": billable, + "description": description, + "projectId": project_id, + "taskId": task_id, + "tagIds": None + } + response = requests.post( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + + success = False + if response.status_code < 300: + success = True + return success + + def get_in_progress(self, workspace_name=None, workspace_id=None): + workspace_id = self.convert_input(workspace_id, workspace_name) + action_url = 'workspaces/{}/timeEntries/inProgress'.format( + workspace_id + ) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + try: + output = response.json() + except json.decoder.JSONDecodeError: + output = None + return output + + def finish_time_entry(self, workspace_name=None, workspace_id=None): + workspace_id = self.convert_input(workspace_id, workspace_name) + current = self.get_in_progress(workspace_id=workspace_id) + current_id = current["id"] + action_url = 'workspaces/{}/timeEntries/{}'.format( + workspace_id, current_id + ) + body = { + "start": current["timeInterval"]["start"], + "billable": current["billable"], + "description": current["description"], + "projectId": current["projectId"], + "taskId": current["taskId"], + "tagIds": current["tagIds"], + "end": self.get_current_time() + } + response = requests.put( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + + def get_time_entries( + self, quantity=10, workspace_name=None, workspace_id=None + ): + workspace_id = self.convert_input(workspace_id, workspace_name) + action_url = 'workspaces/{}/timeEntries/'.format(workspace_id) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + return response.json()[:quantity] + + def remove_time_entry(self, tid, workspace_name=None, workspace_id=None): + workspace_id = self.convert_input(workspace_id, workspace_name) + action_url = 'workspaces/{}/timeEntries/{}'.format( + workspace_id, tid + ) + response = requests.delete( + self.endpoint + action_url, + headers=self.headers + ) + return response.json() + + def add_project(self, name, workspace_name=None, workspace_id=None): + workspace_id = self.convert_input(workspace_id, workspace_name) + action_url = 'workspaces/{}/projects/'.format(workspace_id) + body = { + "name": name, + "clientId": "", + "isPublic": "false", + "estimate": None, + "color": None, + "billable": None + } + response = requests.post( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + + def add_workspace(self, name): + action_url = 'workspaces/' + body = {"name": name} + response = requests.post( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + + def convert_input( + self, entity_id, entity_name, mode='Workspace', project_id=None + ): + if entity_id is None: + error = False + error_msg = 'Missing information "{}"' + if mode.lower() == 'workspace': + if entity_id is None and entity_name is None: + if self.workspace_id is not None: + entity_id = self.workspace_id + else: + error = True + else: + entity_id = self.get_workspace_id(entity_name) + else: + if entity_id is None and entity_name is None: + error = True + elif mode.lower() == 'project': + entity_id = self.get_project_id(entity_name) + elif mode.lower() == 'task': + entity_id = self.get_task_id( + task_name=entity_name, project_id=project_id + ) + else: + raise TypeError('Unknown type') + # Raise error + if error: + raise ValueError(error_msg.format(mode)) + + return entity_id From c098d5b0aa83192cc022818250624a330d92c125 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 12:17:18 +0100 Subject: [PATCH 10/99] added ftrack action for start timer --- pype/ftrack/actions/action_start_clockify.py | 102 +++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 pype/ftrack/actions/action_start_clockify.py diff --git a/pype/ftrack/actions/action_start_clockify.py b/pype/ftrack/actions/action_start_clockify.py new file mode 100644 index 0000000000..2a980de6c0 --- /dev/null +++ b/pype/ftrack/actions/action_start_clockify.py @@ -0,0 +1,102 @@ +import sys +import argparse +import logging + +import ftrack_api +from pype.ftrack import BaseAction +from pype.clockify import ClockifyAPI + + +class StartClockify(BaseAction): + '''Starts timer on clockify.''' + + #: Action identifier. + identifier = 'start.clockify.timer' + #: Action label. + label = 'Start timer' + #: Action description. + description = 'Starts timer on clockify' + #: roles that are allowed to register this action + icon = 'https://clockify.me/assets/images/clockify-logo.png' + #: Clockify api + clockapi = ClockifyAPI() + + def discover(self, session, entities, event): + if len(entities) != 1: + return False + if entities[0].entity_type.lower() != 'task': + return False + return True + + def launch(self, session, entities, event): + task = entities[0] + task_name = task['name'] + project_name = task['project']['full_name'] + + def get_parents(entity): + output = [] + if entity.entity_type.lower() == 'project': + return output + output.extend(get_parents(entity['parent'])) + output.append(entity['name']) + + return output + + desc_items = get_parents(task['parent']) + description = '/'.join(desc_items) + + self.clockapi.start_time_entry( + description=description, + project_name=project_name, + task_name=task_name, + ) + + return True + + +def register(session, **kw): + '''Register plugin. Called when used as an plugin.''' + + if not isinstance(session, ftrack_api.session.Session): + return + + StartClockify(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 f051e61a0b305dc361917a1a49c4a7f439325b29 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 12:17:53 +0100 Subject: [PATCH 11/99] changed clockify API when start timer when already is one running finish him --- pype/clockify/clockify_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index 160afe914b..fce1691d43 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -200,7 +200,7 @@ class ClockifyAPI(metaclass=Singleton): ): self.bool_timer_run = True return self.bool_timer_run - return False + self.finish_time_entry(workspace_id=workspace_id) # Convert billable to strings if billable: From 40c2ccd9d789f1869e3f622eb082efd5940fe832 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 12:52:33 +0100 Subject: [PATCH 12/99] stop timer works now --- pype/clockify/clockify.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py index 1541807a46..8710bf56eb 100644 --- a/pype/clockify/clockify.py +++ b/pype/clockify/clockify.py @@ -35,6 +35,9 @@ class ClockifyModule: self.set_menu_visibility() + def stop_timer(self): + self.clockapi.finish_time_entry() + # Definition of Tray menu def tray_menu(self, parent): # Menu for Tray App @@ -54,7 +57,7 @@ class ClockifyModule: self.menu.addAction(self.aStopTimer) self.aShowSettings.triggered.connect(self.show_settings) - self.aStopTimer.triggered.connect(self.clockapi.finish_time_entry) + self.aStopTimer.triggered.connect(self.stop_timer) self.set_menu_visibility() From 463c574e883712a3b1aa91b90c89ae3096b8d53c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 12:53:02 +0100 Subject: [PATCH 13/99] added thread that check each 5 seconds if any timer is running --- pype/clockify/clockify.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py index 8710bf56eb..1a67eeba92 100644 --- a/pype/clockify/clockify.py +++ b/pype/clockify/clockify.py @@ -1,4 +1,5 @@ import os +import threading from app import style from app.vendor.Qt import QtWidgets from pype.clockify import ClockifySettings, ClockifyAPI @@ -12,7 +13,9 @@ class ClockifyModule: self.clockapi = ClockifyAPI() self.widget_settings = ClockifySettings(main_parent, self) + self.thread_timer_check = None # Bools + self.bool_thread_check_running = False self.bool_api_key_set = False self.bool_workspace_set = False self.bool_timer_run = False @@ -24,17 +27,39 @@ class ClockifyModule: return workspace = os.environ.get('CLOCKIFY_WORKSPACE', None) - print(workspace) self.bool_workspace_set = self.clockapi.set_workspace(workspace) if self.bool_workspace_set is False: # TODO show message to user print("Nope Workspace: clockify.py - line 29") return - if self.clockapi.get_in_progress() is not None: - self.bool_timer_run = True + + self.bool_thread_check_running = True + self.start_timer_check() self.set_menu_visibility() + def change_timer_run(self, bool_run): + self.bool_timer_run = bool_run + self.set_menu_visibility() + + def start_timer_check(self): + if self.thread_timer_check is None: + self.thread_timer_check = threading.Thread( + target=self.check_running + ) + self.thread_timer_check.daemon = True + self.thread_timer_check.start() + + def check_running(self): + import time + while self.bool_thread_check_running is True: + if self.clockapi.get_in_progress() is not None: + self.bool_timer_run = True + else: + self.bool_timer_run = False + self.set_menu_visibility() + time.sleep(5) + def stop_timer(self): self.clockapi.finish_time_entry() From 607e7c78b5efbc640790331158608d959038a46a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 16:50:52 +0100 Subject: [PATCH 14/99] renamed start_clockify to clockify_start --- .../{action_start_clockify.py => action_clockify_start.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename pype/ftrack/actions/{action_start_clockify.py => action_clockify_start.py} (98%) diff --git a/pype/ftrack/actions/action_start_clockify.py b/pype/ftrack/actions/action_clockify_start.py similarity index 98% rename from pype/ftrack/actions/action_start_clockify.py rename to pype/ftrack/actions/action_clockify_start.py index 2a980de6c0..7fabf1d0b7 100644 --- a/pype/ftrack/actions/action_start_clockify.py +++ b/pype/ftrack/actions/action_clockify_start.py @@ -11,7 +11,7 @@ class StartClockify(BaseAction): '''Starts timer on clockify.''' #: Action identifier. - identifier = 'start.clockify.timer' + identifier = 'clockify.start.timer' #: Action label. label = 'Start timer' #: Action description. From 67241f9df0057f511865a88a426ae3c53d12c862 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 16:51:33 +0100 Subject: [PATCH 15/99] added add_task and delete_project methods to clockify api --- pype/clockify/clockify_api.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index fce1691d43..5ef976fcbe 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -317,6 +317,41 @@ class ClockifyAPI(metaclass=Singleton): ) return response.json() + def add_task( + self, name, project_name=None, project_id=None, + workspace_name=None, workspace_id=None + ): + workspace_id = self.convert_input(workspace_id, workspace_name) + project_id = self.convert_input(project_id, project_name, 'Project') + action_url = 'workspaces/{}/projects/{}/tasks/'.format( + workspace_id, project_id + ) + body = { + "name": name, + "projectId": project_id + } + response = requests.post( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + + def delete_project( + self, project_name=None, project_id=None, + workspace_name=None, workspace_id=None + ): + workspace_id = self.convert_input(workspace_id, workspace_name) + project_id = self.convert_input(project_id, project_name, 'Project') + action_url = '/workspaces/{}/projects/{}'.format( + workspace_id, project_id + ) + response = requests.delete( + self.endpoint + action_url, + headers=self.headers, + ) + return response.json() + def convert_input( self, entity_id, entity_name, mode='Workspace', project_id=None ): From 73c3501928810b3ccf4a0e28626b7a818b064667 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 16:52:19 +0100 Subject: [PATCH 16/99] modified values in add_project so they dont include any None values (raises error) --- pype/clockify/clockify_api.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index 5ef976fcbe..5bcb05c4e4 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -296,9 +296,12 @@ class ClockifyAPI(metaclass=Singleton): "name": name, "clientId": "", "isPublic": "false", - "estimate": None, - "color": None, - "billable": None + "estimate": { + # "estimate": "3600", + "type": "AUTO" + }, + "color": "#f44336", + "billable": "true" } response = requests.post( self.endpoint + action_url, From 16ca88c86a1277ad6261a969ecf2b6a4c8aa8453 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 16:53:50 +0100 Subject: [PATCH 17/99] added basic clockify sync - Ftrack action - validates if user can create project, then creates project and task types --- pype/ftrack/actions/action_clockify_sync.py | 156 ++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 pype/ftrack/actions/action_clockify_sync.py diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py new file mode 100644 index 0000000000..afb3173e4a --- /dev/null +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -0,0 +1,156 @@ +import sys +import argparse +import logging +import json +import ftrack_api +from pype.ftrack import BaseAction +from pype.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' + #: priority + priority = 100 + #: roles that are allowed to register this action + role_list = ['Pypecub', 'Administrator'] + #: icon + icon = 'https://clockify.me/assets/images/clockify-logo-white.svg' + #: CLockifyApi + clockapi = ClockifyAPI() + + def discover(self, session, entities, event): + ''' Validation ''' + + return True + + def validate_auth_rights(self): + test_project = '__test__' + try: + self.clockapi.add_project(test_project) + except Exception: + return False + self.clockapi.delete_project(test_project) + return True + + def launch(self, session, entities, event): + authorization = self.validate_auth_rights() + if authorization is False: + return { + 'success': False, + 'message': ( + 'You don\'t have permission to modify Clockify' + ' workspace {}'.format(self.clockapi.workspace) + ) + } + + # 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() + try: + projects_info = {} + for project in session.query('Project').all(): + task_types = [] + for task_type in project['project_schema']['_task_type_schema'][ + 'types' + ]: + task_types.append(task_type['name']) + projects_info[project['full_name']] = task_types + + clockify_projects = self.clockapi.get_projects() + for project_name, task_types in projects_info.items(): + 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 + )) + continue + project_id = response['id'] + else: + project_id = clockify_projects[project_name] + + clockify_project_tasks = self.clockapi.get_tasks( + project_id=project_id + ) + for task_type in task_types: + if task_type not in clockify_project_tasks: + response = self.clockapi.add_task( + task_type, project_id=project_id + ) + if 'id' not in response: + self.log.error('Task {} can\'t be created'.format( + task_type + )) + continue + except Exception: + job['status'] = 'failed' + session.commit() + return False + + job['status'] = 'done' + session.commit() + return True + + +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 65ee3a82177beecae3a0b0590c1bd7143ca57413 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Mar 2019 18:21:17 +0100 Subject: [PATCH 18/99] added interface, fixed role list, autorization check before register --- pype/ftrack/actions/action_clockify_sync.py | 84 +++++++++++++++++---- 1 file changed, 68 insertions(+), 16 deletions(-) diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py index afb3173e4a..f3b1a49329 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -3,7 +3,7 @@ import argparse import logging import json import ftrack_api -from pype.ftrack import BaseAction +from pype.ftrack import BaseAction, MissingPermision from pype.clockify import ClockifyAPI @@ -19,16 +19,16 @@ class SyncClocify(BaseAction): #: priority priority = 100 #: roles that are allowed to register this action - role_list = ['Pypecub', 'Administrator'] + role_list = ['Pypeclub', 'Administrator'] #: icon icon = 'https://clockify.me/assets/images/clockify-logo-white.svg' #: CLockifyApi clockapi = ClockifyAPI() - def discover(self, session, entities, event): - ''' Validation ''' - - return True + def register(self): + if self.validate_auth_rights() is False: + raise MissingPermision + super().register() def validate_auth_rights(self): test_project = '__test__' @@ -39,16 +39,57 @@ class SyncClocify(BaseAction): self.clockapi.delete_project(test_project) return True - def launch(self, session, entities, event): - authorization = self.validate_auth_rights() - if authorization is False: - return { - 'success': False, - 'message': ( - 'You don\'t have permission to modify Clockify' - ' workspace {}'.format(self.clockapi.workspace) - ) + def discover(self, session, entities, event): + ''' Validation ''' + + return True + + def interface(self, session, entities, event): + if not event['data'].get('values', {}): + title = 'Select projects to sync' + + projects = session.query('Project').all() + + items = [] + all_projects_label = { + 'type': 'label', + 'value': 'All projects' } + all_projects_value = { + 'name': '__all__', + 'type': 'boolean', + 'value': False + } + line = { + 'type': 'label', + 'value': '___' + } + items.append(all_projects_label) + items.append(all_projects_value) + items.append(line) + for project in projects: + label = project['full_name'] + item_label = { + 'type': 'label', + 'value': label + } + item_value = { + 'name': project['id'], + 'type': 'boolean', + 'value': False + } + items.append(item_label) + items.append(item_value) + + return { + 'items': items, + 'title': title + } + + def launch(self, session, entities, event): + values = event['data'].get('values', {}) + if not values: + return # JOB SETTINGS userId = event['source']['user']['id'] @@ -63,8 +104,19 @@ class SyncClocify(BaseAction): }) session.commit() try: + if values.get('__all__', False) is True: + projects_to_sync = session.query('Project').all() + else: + projects_to_sync = [] + project_query = 'Project where id is "{}"' + for project_id, sync in values.items(): + if sync is True: + projects_to_sync.append(session.query( + project_query.format(project_id) + ).one()) + projects_info = {} - for project in session.query('Project').all(): + for project in projects_to_sync: task_types = [] for task_type in project['project_schema']['_task_type_schema'][ 'types' From 1df4f0c148f1b99d459c4e0abbc257c20e8d3e94 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Mar 2019 15:41:39 +0100 Subject: [PATCH 19/99] permission validation moved to clockify_api will be used in multiple actions --- pype/clockify/clockify_api.py | 17 +++++++++++++++++ pype/ftrack/actions/action_clockify_sync.py | 12 +----------- .../{StartClockify.py => ClockifyStart.py} | 12 ++++++------ 3 files changed, 24 insertions(+), 17 deletions(-) rename pype/plugins/launcher/actions/{StartClockify.py => ClockifyStart.py} (80%) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index 5bcb05c4e4..b250377756 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -43,6 +43,23 @@ class ClockifyAPI(metaclass=Singleton): return False return True + def validate_workspace_perm(self): + test_project = '__test__' + action_url = 'workspaces/{}/projects/'.format(self.workspace_id) + body = { + "name": test_project, "clientId": "", "isPublic": "false", + "estimate": {"type": "AUTO"}, + "color": "#f44336", "billable": "true" + } + response = requests.post( + self.endpoint + action_url, + headers=self.headers, json=body + ) + if response.status_code >= 300: + return False + self.delete_project(test_project) + return True + def set_workspace(self, name=None): if name is None: self.workspace = None diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py index f3b1a49329..84e47005f5 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -26,22 +26,12 @@ class SyncClocify(BaseAction): clockapi = ClockifyAPI() def register(self): - if self.validate_auth_rights() is False: + if self.clockapi.validate_workspace_perm() is False: raise MissingPermision super().register() - def validate_auth_rights(self): - test_project = '__test__' - try: - self.clockapi.add_project(test_project) - except Exception: - return False - self.clockapi.delete_project(test_project) - return True - def discover(self, session, entities, event): ''' Validation ''' - return True def interface(self, session, entities, event): diff --git a/pype/plugins/launcher/actions/StartClockify.py b/pype/plugins/launcher/actions/ClockifyStart.py similarity index 80% rename from pype/plugins/launcher/actions/StartClockify.py rename to pype/plugins/launcher/actions/ClockifyStart.py index 91adfe0185..e1f17f2aa3 100644 --- a/pype/plugins/launcher/actions/StartClockify.py +++ b/pype/plugins/launcher/actions/ClockifyStart.py @@ -1,15 +1,16 @@ from avalon import api, io from pype.clockify import ClockifyAPI from pype.api import Logger -log = Logger.getLogger(__name__, "start_clockify") +log = Logger.getLogger(__name__, "clockify_start") -class StartClockify(api.Action): +class ClockifyStart(api.Action): - name = "start_clockify_timer" - label = "Start Timer - Clockify" + name = "clockify_start_timer" + label = "Clockify - Start Timer" icon = "clockify_icon" order = 500 + clockapi = ClockifyAPI() def is_compatible(self, session): """Return whether the action is compatible with the session""" @@ -18,7 +19,6 @@ class StartClockify(api.Action): return False def process(self, session, **kwargs): - clockapi = ClockifyAPI() project_name = session['AVALON_PROJECT'] asset_name = session['AVALON_ASSET'] task_name = session['AVALON_TASK'] @@ -33,7 +33,7 @@ class StartClockify(api.Action): desc_items.append(asset_name) description = '/'.join(desc_items) - clockapi.start_time_entry( + self.clockapi.start_time_entry( description=description, project_name=project_name, task_name=task_name, From a88c14b7233e08ab26d7cbd48fc1e823e9d9588e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Mar 2019 15:53:33 +0100 Subject: [PATCH 20/99] added action to launcher that syncs to clockify --- pype/plugins/launcher/actions/ClockifySync.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 pype/plugins/launcher/actions/ClockifySync.py diff --git a/pype/plugins/launcher/actions/ClockifySync.py b/pype/plugins/launcher/actions/ClockifySync.py new file mode 100644 index 0000000000..31ae1f3424 --- /dev/null +++ b/pype/plugins/launcher/actions/ClockifySync.py @@ -0,0 +1,61 @@ +from avalon import api, io +from pype.clockify import ClockifyAPI +from pype.api import Logger +log = Logger.getLogger(__name__, "clockify_sync") + + +class ClockifySync(api.Action): + + name = "sync_to_clockify" + label = "Sync to Clockify" + icon = "clockify_icon" + order = 500 + clockapi = ClockifyAPI() + have_permissions = clockapi.validate_workspace_perm() + + def is_compatible(self, session): + """Return whether the action is compatible with the session""" + return self.have_permissions + + def process(self, session, **kwargs): + project_name = session.get('AVALON_PROJECT', None) + + projects_to_sync = [] + if project_name.strip() == '' or project_name is None: + for project in io.projects(): + projects_to_sync.append(project) + else: + project = io.find_one({'type': 'project'}) + projects_to_sync.append(project) + + projects_info = {} + for project in projects_to_sync: + task_types = [task['name'] for task in project['config']['tasks']] + projects_info[project['name']] = task_types + + clockify_projects = self.clockapi.get_projects() + for project_name, task_types in projects_info.items(): + 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 + )) + continue + project_id = response['id'] + else: + project_id = clockify_projects[project_name] + + clockify_project_tasks = self.clockapi.get_tasks( + project_id=project_id + ) + for task_type in task_types: + if task_type not in clockify_project_tasks: + response = self.clockapi.add_task( + task_type, project_id=project_id + ) + if 'id' not in response: + self.log.error('Task {} can\'t be created'.format( + task_type + )) + continue From 698b0da17067991e7fbbe1e07b3c311b320f9497 Mon Sep 17 00:00:00 2001 From: antirotor Date: Fri, 8 Mar 2019 12:55:52 +0100 Subject: [PATCH 21/99] another shot at UV overlapping, wip --- .../publish/validate_mesh_overlapping_uvs.py | 311 +++++++++++++----- 1 file changed, 228 insertions(+), 83 deletions(-) diff --git a/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py b/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py index ca8faf60a9..c28aca5369 100644 --- a/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py +++ b/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py @@ -3,6 +3,10 @@ from maya import cmds import pyblish.api import pype.api import pype.maya.action +import math +import maya.api.OpenMaya as om +from pymel.core import * +from pymel.core.datatypes import * class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin): @@ -20,94 +24,235 @@ class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin): actions = [pype.maya.action.SelectInvalidAction] optional = True + def _createBoundingCircle(self, meshfn): + """ Parameter: meshfn - MFnMesh + Represent a face by a center and radius, i.eself. + center = [center1u, center1v, center2u, center2v, ... ] + radius = [radius1, radius2, ... ] + return (center, radius) + """ + center = [] + radius = [] + for i in xrange(meshfn.numPolygons): # noqa: F405 + # get uvs from face + uarray = [] + varray = [] + for j in range(len(meshfn.getPolygonVertices(i))): + uv = meshfn.getPolygonUV(i, j) + uarray.append(uv[0]) + varray.append(uv[1]) + + # loop through all vertices to construct edges/rays + cu = 0.0 + cv = 0.0 + for j in range(len(uarray)): + cu += uarray[j] + cv += varray[j] + + cu /= len(uarray) + cv /= len(varray) + rsqr = 0.0 + for j in range(len(varray)): + du = uarray[j] - cu + dv = varray[j] - cv + dsqr = du * du + dv * dv + rsqr = dsqr if dsqr > rsqr else rsqr + + center.append(cu) + center.append(cv) + radius.append(math.sqrt(rsqr)) + + return center, radius + + def _createRayGivenFace(self, meshfn, faceId): + """ Represent a face by a series of edges(rays), i.e. + orig = [orig1u, orig1v, orig2u, orig2v, ... ] + vec = [vec1u, vec1v, vec2u, vec2v, ... ] + return false if no valid uv's. + return (true, orig, vec) or (false, None, None) + """ + orig = [] + vec = [] + # get uvs + uarray = [] + varray = [] + for i in range(len(meshfn.getPolygonVertices(faceId))): + uv = meshfn.getPolygonUV(faceId, i) + uarray.append(uv[0]) + varray.append(uv[1]) + + if len(uarray) == 0 or len(varray) == 0: + return (False, None, None) + + # loop throught all vertices to construct edges/rays + u = uarray[-1] + v = varray[-1] + for i in xrange(len(uarray)): # noqa: F405 + orig.append(uarray[i]) + orig.append(varray[i]) + vec.append(u - uarray[i]) + vec.append(v - varray[i]) + u = uarray[i] + v = varray[i] + + return (True, orig, vec) + + def _area(self, orig): + sum = 0.0 + num = len(orig)/2 + for i in xrange(num): # noqa: F405 + idx = 2 * i + idy = (i + 1) % num + idy = 2 * idy + 1 + idy2 = (i + num - 1) % num + idy2 = 2 * idy2 + 1 + sum += orig[idx] * (orig[idy] - orig[idy2]) + + return math.fabs(sum) * 0.5 + + def _checkCrossingEdges(self, + face1Orig, + face1Vec, + face2Orig, + face2Vec): + """ Check if there are crossing edges between two faces. + Return true if there are crossing edges and false otherwise. + A face is represented by a series of edges(rays), i.e. + faceOrig[] = [orig1u, orig1v, orig2u, orig2v, ... ] + faceVec[] = [vec1u, vec1v, vec2u, vec2v, ... ] + """ + face1Size = len(face1Orig) + face2Size = len(face2Orig) + for i in xrange(0, face1Size, 2): + o1x = face1Orig[i] + o1y = face1Orig[i+1] + v1x = face1Vec[i] + v1y = face1Vec[i+1] + n1x = v1y + n1y = -v1x + for j in xrange(0, face2Size, 2): + # Given ray1(O1, V1) and ray2(O2, V2) + # Normal of ray1 is (V1.y, V1.x) + o2x = face2Orig[j] + o2y = face2Orig[j+1] + v2x = face2Vec[j] + v2y = face2Vec[j+1] + n2x = v2y + n2y = -v2x + + # Find t for ray2 + # t = [(o1x-o2x)n1x + (o1y-o2y)n1y] / + # (v2x * n1x + v2y * n1y) + denum = v2x * n1x + v2y * n1y + # Edges are parallel if denum is close to 0. + if math.fabs(denum) < 0.000001: + continue + t2 = ((o1x-o2x) * n1x + (o1y-o2y) * n1y) / denum + if (t2 < 0.00001 or t2 > 0.99999): + continue + + # Find t for ray1 + # t = [(o2x-o1x)n2x + # + (o2y-o1y)n2y] / (v1x * n2x + v1y * n2y) + denum = v1x * n2x + v1y * n2y + # Edges are parallel if denum is close to 0. + if math.fabs(denum) < 0.000001: + continue + t1 = ((o2x-o1x) * n2x + (o2y-o1y) * n2y) / denum + + # Edges intersect + if (t1 > 0.00001 and t1 < 0.99999): + return 1 + + return 0 + + def _getOverlapUVFaces(self, meshName): + """ Return overlapping faces """ + faces = [] + # find polygon mesh node + selList = om.MSelectionList() + selList.add(meshName) + mesh = selList.getDependNode(0) + if mesh.apiType() == om.MFn.kTransform: + dagPath = selList.getDagPath(0) + dagFn = om.MFnDagNode(dagPath) + child = dagFn.child(0) + if child.apiType() != om.MFn.kMesh: + raise Exception("Can't find polygon mesh") + mesh = child + meshfn = om.MFnMesh(mesh) + + center, radius = self._createBoundingCircle(meshfn) + for i in xrange(meshfn.numPolygons): # noqa: F405 + rayb1, face1Orig, face1Vec = self._createRayGivenFace( + meshfn, i) + if not rayb1: + continue + cui = center[2*i] + cvi = center[2*i+1] + ri = radius[i] + # Exclude the degenerate face + # if(area(face1Orig) < 0.000001) continue; + # Loop through face j where j != i + for j in range(i+1, meshfn.numPolygons): + cuj = center[2*j] + cvj = center[2*j+1] + rj = radius[j] + du = cuj - cui + dv = cvj - cvi + dsqr = du * du + dv * dv + # Quick rejection if bounding circles don't overlap + if (dsqr >= (ri + rj) * (ri + rj)): + continue + + rayb2, face2Orig, face2Vec = self._createRayGivenFace( + meshfn, j) + if not rayb2: + continue + # Exclude the degenerate face + # if(area(face2Orig) < 0.000001): continue; + if self._checkCrossingEdges(face1Orig, + face1Vec, + face2Orig, + face2Vec): + face1 = '%s.f[%d]' % (meshfn.name(), i) + face2 = '%s.f[%d]' % (meshfn.name(), j) + if face1 not in faces: + faces.append(face1) + if face2 not in faces: + faces.append(face2) + return faces + @classmethod def _has_overlapping_uvs(cls, node): - allUvSets = cmds.polyUVSet(q=1, auv=1) - # print allUvSets - currentTool = cmds.currentCtx() - cmds.setToolTo('selectSuperContext') - biglist = cmds.ls( - cmds.polyListComponentConversion(node, tf=True), fl=True) - shells = [] - overlappers = [] - bounds = [] - for uvset in allUvSets: - # print uvset - while len(biglist) > 0: - cmds.select(biglist[0], r=True) - # cmds.polySelectConstraint(t=0) - # cmds.polySelectConstraint(sh=1,m=2) - # cmds.polySelectConstraint(sh=0,m=0) - aShell = cmds.ls(sl=True, fl=True) - shells.append(aShell) - biglist = list(set(biglist) - set(aShell)) - cmds.setToolTo(currentTool) - cmds.select(clear=True) - # shells = [ [faces in uv shell 1], [faces in shell 2], [etc] ] + overlapFaces = [] + flipped = [] + oStr = '' + for s in ls(sl=1, fl=1): + curUV = polyUVSet(s, q=1, cuv=1) + for i, uv in enumerate(polyUVSet(s, q=1, auv=1)): + polyUVSet(s, cuv=1, uvSet=uv) + of = getOverlapUVFaces(str(s)) + if of != []: + oStr += s + " has " + str(len(of)) + " overlapped faces in uvset " + uv + '\n' + overlapFaces.extend(of) - for faces in shells: - shellSets = cmds.polyListComponentConversion( - faces, ff=True, tuv=True) - if shellSets != []: - uv = cmds.polyEditUV(shellSets, q=True) + """ + # inverted + flf = [] + for f in ls( PyNode( s ).getShape().f, fl=1 ): + uvPos = polyEditUV( [ polyListComponentConversion( vf, fvf=1, toUV=1 )[0] for vf in ls( polyListComponentConversion( f, tvf=1 ), fl=1 ) ], q=1 ) + uvAB = Vector( [ uvPos[2] - uvPos[0], uvPos[3] - uvPos[1] ] ) + uvBC = Vector( [ uvPos[4] - uvPos[2], uvPos[5] - uvPos[3] ] ) + if uvAB.cross( uvBC ) * Vector([0, 0, 1]) <= 0: flf.append( f ) + if flf != []: + oStr += s + " has " + str( len( flf ) ) + " inverted faces in uvset " + uv + '\n' + flipped.extend( flf ) - uMin = uv[0] - uMax = uv[0] - vMin = uv[1] - vMax = uv[1] - for i in range(len(uv)/2): - if uv[i*2] < uMin: - uMin = uv[i*2] - if uv[i*2] > uMax: - uMax = uv[i*2] - if uv[i*2+1] < vMin: - vMin = uv[i*2+1] - if uv[i*2+1] > vMax: - vMax = uv[i*2+1] - bounds.append([[uMin, uMax], [vMin, vMax]]) - else: - return False - - for a in range(len(shells)): - for b in range(a): - # print "b",b - if bounds != []: - # print bounds - aL = bounds[a][0][0] - aR = bounds[a][0][1] - aT = bounds[a][1][1] - aB = bounds[a][1][0] - - bL = bounds[b][0][0] - bR = bounds[b][0][1] - bT = bounds[b][1][1] - bB = bounds[b][1][0] - - overlaps = True - if aT < bB: # A entirely below B - overlaps = False - - if aB > bT: # A entirely above B - overlaps = False - - if aR < bL: # A entirely right of B - overlaps = False - - if aL > bR: # A entirely left of B - overlaps = False - - if overlaps: - overlappers.extend(shells[a]) - overlappers.extend(shells[b]) - else: - return False - pass - - if overlappers: - return True - else: - return False + polyUVSet( s, cuv=1, uvSet=str( curUV ) ) + oStr += '\n' + """ @classmethod def get_invalid(cls, instance): From 9e9d22f3856ec97eda298e4d9740c0d88d3e1371 Mon Sep 17 00:00:00 2001 From: antirotor Date: Fri, 8 Mar 2019 18:55:54 +0100 Subject: [PATCH 22/99] added comments and docstrings, ready for tests --- .../publish/validate_mesh_overlapping_uvs.py | 115 +++++++++--------- 1 file changed, 57 insertions(+), 58 deletions(-) diff --git a/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py b/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py index c28aca5369..f20d9f9118 100644 --- a/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py +++ b/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py @@ -5,12 +5,11 @@ import pype.api import pype.maya.action import math import maya.api.OpenMaya as om -from pymel.core import * -from pymel.core.datatypes import * +from pymel.core import polyUVSet class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin): - """Validate the current mesh overlapping UVs. + """ Validate the current mesh overlapping UVs. It validates whether the current UVs are overlapping or not. It is optional to warn publisher about it. @@ -25,15 +24,16 @@ class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin): optional = True def _createBoundingCircle(self, meshfn): - """ Parameter: meshfn - MFnMesh - Represent a face by a center and radius, i.eself. - center = [center1u, center1v, center2u, center2v, ... ] - radius = [radius1, radius2, ... ] - return (center, radius) + """ Represent a face by center and radius + + :param meshfn: MFnMesh class + :type meshfn: :class:`maya.api.OpenMaya.MFnMesh` + :returns: (center, radius) + :rtype: tuple """ center = [] radius = [] - for i in xrange(meshfn.numPolygons): # noqa: F405 + for i in xrange(meshfn.numPolygons): # noqa: F821 # get uvs from face uarray = [] varray = [] @@ -66,10 +66,19 @@ class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin): def _createRayGivenFace(self, meshfn, faceId): """ Represent a face by a series of edges(rays), i.e. + + :param meshfn: MFnMesh class + :type meshfn: :class:`maya.api.OpenMaya.MFnMesh` + :param faceId: face id + :type faceId: int + :returns: False if no valid uv's. + ""(True, orig, vec)"" or ""(False, None, None)"" + :rtype: tuple + + .. code-block:: python + orig = [orig1u, orig1v, orig2u, orig2v, ... ] vec = [vec1u, vec1v, vec2u, vec2v, ... ] - return false if no valid uv's. - return (true, orig, vec) or (false, None, None) """ orig = [] vec = [] @@ -87,7 +96,7 @@ class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin): # loop throught all vertices to construct edges/rays u = uarray[-1] v = varray[-1] - for i in xrange(len(uarray)): # noqa: F405 + for i in xrange(len(uarray)): # noqa: F821 orig.append(uarray[i]) orig.append(varray[i]) vec.append(u - uarray[i]) @@ -97,40 +106,39 @@ class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin): return (True, orig, vec) - def _area(self, orig): - sum = 0.0 - num = len(orig)/2 - for i in xrange(num): # noqa: F405 - idx = 2 * i - idy = (i + 1) % num - idy = 2 * idy + 1 - idy2 = (i + num - 1) % num - idy2 = 2 * idy2 + 1 - sum += orig[idx] * (orig[idy] - orig[idy2]) - - return math.fabs(sum) * 0.5 - def _checkCrossingEdges(self, face1Orig, face1Vec, face2Orig, face2Vec): """ Check if there are crossing edges between two faces. - Return true if there are crossing edges and false otherwise. + Return True if there are crossing edges and False otherwise. + + :param face1Orig: origin of face 1 + :type face1Orig: tuple + :param face1Vec: face 1 edges + :type face1Vec: list + :param face2Orig: origin of face 2 + :type face2Orig: tuple + :param face2Vec: face 2 edges + :type face2Vec: list + A face is represented by a series of edges(rays), i.e. - faceOrig[] = [orig1u, orig1v, orig2u, orig2v, ... ] - faceVec[] = [vec1u, vec1v, vec2u, vec2v, ... ] + .. code-block:: python + + faceOrig[] = [orig1u, orig1v, orig2u, orig2v, ... ] + faceVec[] = [vec1u, vec1v, vec2u, vec2v, ... ] """ face1Size = len(face1Orig) face2Size = len(face2Orig) - for i in xrange(0, face1Size, 2): + for i in xrange(0, face1Size, 2): # noqa: F821 o1x = face1Orig[i] o1y = face1Orig[i+1] v1x = face1Vec[i] v1y = face1Vec[i+1] n1x = v1y n1y = -v1x - for j in xrange(0, face2Size, 2): + for j in xrange(0, face2Size, 2): # noqa: F821 # Given ray1(O1, V1) and ray2(O2, V2) # Normal of ray1 is (V1.y, V1.x) o2x = face2Orig[j] @@ -167,7 +175,13 @@ class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin): return 0 def _getOverlapUVFaces(self, meshName): - """ Return overlapping faces """ + """ Return overlapping faces + + :param meshName: name of mesh + :type meshName: str + :returns: list of overlapping faces + :rtype: list + """ faces = [] # find polygon mesh node selList = om.MSelectionList() @@ -183,7 +197,7 @@ class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin): meshfn = om.MFnMesh(mesh) center, radius = self._createBoundingCircle(meshfn) - for i in xrange(meshfn.numPolygons): # noqa: F405 + for i in xrange(meshfn.numPolygons): # noqa: F821 rayb1, face1Orig, face1Vec = self._createRayGivenFace( meshfn, i) if not rayb1: @@ -225,34 +239,19 @@ class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin): @classmethod def _has_overlapping_uvs(cls, node): + """ Check if mesh has overlapping UVs. - overlapFaces = [] - flipped = [] - oStr = '' - for s in ls(sl=1, fl=1): - curUV = polyUVSet(s, q=1, cuv=1) - for i, uv in enumerate(polyUVSet(s, q=1, auv=1)): - polyUVSet(s, cuv=1, uvSet=uv) - of = getOverlapUVFaces(str(s)) - if of != []: - oStr += s + " has " + str(len(of)) + " overlapped faces in uvset " + uv + '\n' - overlapFaces.extend(of) - - """ - # inverted - flf = [] - for f in ls( PyNode( s ).getShape().f, fl=1 ): - uvPos = polyEditUV( [ polyListComponentConversion( vf, fvf=1, toUV=1 )[0] for vf in ls( polyListComponentConversion( f, tvf=1 ), fl=1 ) ], q=1 ) - uvAB = Vector( [ uvPos[2] - uvPos[0], uvPos[3] - uvPos[1] ] ) - uvBC = Vector( [ uvPos[4] - uvPos[2], uvPos[5] - uvPos[3] ] ) - if uvAB.cross( uvBC ) * Vector([0, 0, 1]) <= 0: flf.append( f ) - if flf != []: - oStr += s + " has " + str( len( flf ) ) + " inverted faces in uvset " + uv + '\n' - flipped.extend( flf ) - - polyUVSet( s, cuv=1, uvSet=str( curUV ) ) - oStr += '\n' + :param node: node to check + :type node: str + :returns: True is has overlapping UVs, False otherwise + :rtype: bool """ + for i, uv in enumerate(polyUVSet(node, q=1, auv=1)): + polyUVSet(node, cuv=1, uvSet=uv) + of = cls._getOverlapUVFaces(str(node)) + if of != []: + return True + return False @classmethod def get_invalid(cls, instance): From dbbace8664a69916882885e181a462d85c1f1bad Mon Sep 17 00:00:00 2001 From: antirotor Date: Sat, 9 Mar 2019 00:32:50 +0100 Subject: [PATCH 23/99] fix: separing classes --- .../publish/validate_mesh_overlapping_uvs.py | 435 +++++++++--------- 1 file changed, 220 insertions(+), 215 deletions(-) diff --git a/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py b/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py index f20d9f9118..3aae97b8fd 100644 --- a/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py +++ b/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py @@ -8,6 +8,223 @@ import maya.api.OpenMaya as om from pymel.core import polyUVSet +class GetOverlappingUVs(object): + + def _createBoundingCircle(self, meshfn): + """ Represent a face by center and radius + + :param meshfn: MFnMesh class + :type meshfn: :class:`maya.api.OpenMaya.MFnMesh` + :returns: (center, radius) + :rtype: tuple + """ + center = [] + radius = [] + for i in xrange(meshfn.numPolygons): # noqa: F821 + # get uvs from face + uarray = [] + varray = [] + for j in range(len(meshfn.getPolygonVertices(i))): + uv = meshfn.getPolygonUV(i, j) + uarray.append(uv[0]) + varray.append(uv[1]) + + # loop through all vertices to construct edges/rays + cu = 0.0 + cv = 0.0 + for j in range(len(uarray)): + cu += uarray[j] + cv += varray[j] + + cu /= len(uarray) + cv /= len(varray) + rsqr = 0.0 + for j in range(len(varray)): + du = uarray[j] - cu + dv = varray[j] - cv + dsqr = du * du + dv * dv + rsqr = dsqr if dsqr > rsqr else rsqr + + center.append(cu) + center.append(cv) + radius.append(math.sqrt(rsqr)) + + return center, radius + + def _createRayGivenFace(self, meshfn, faceId): + """ Represent a face by a series of edges(rays), i.e. + + :param meshfn: MFnMesh class + :type meshfn: :class:`maya.api.OpenMaya.MFnMesh` + :param faceId: face id + :type faceId: int + :returns: False if no valid uv's. + ""(True, orig, vec)"" or ""(False, None, None)"" + :rtype: tuple + + .. code-block:: python + + orig = [orig1u, orig1v, orig2u, orig2v, ... ] + vec = [vec1u, vec1v, vec2u, vec2v, ... ] + """ + orig = [] + vec = [] + # get uvs + uarray = [] + varray = [] + for i in range(len(meshfn.getPolygonVertices(faceId))): + uv = meshfn.getPolygonUV(faceId, i) + uarray.append(uv[0]) + varray.append(uv[1]) + + if len(uarray) == 0 or len(varray) == 0: + return (False, None, None) + + # loop throught all vertices to construct edges/rays + u = uarray[-1] + v = varray[-1] + for i in xrange(len(uarray)): # noqa: F821 + orig.append(uarray[i]) + orig.append(varray[i]) + vec.append(u - uarray[i]) + vec.append(v - varray[i]) + u = uarray[i] + v = varray[i] + + return (True, orig, vec) + + def _checkCrossingEdges(self, + face1Orig, + face1Vec, + face2Orig, + face2Vec): + """ Check if there are crossing edges between two faces. + Return True if there are crossing edges and False otherwise. + + :param face1Orig: origin of face 1 + :type face1Orig: tuple + :param face1Vec: face 1 edges + :type face1Vec: list + :param face2Orig: origin of face 2 + :type face2Orig: tuple + :param face2Vec: face 2 edges + :type face2Vec: list + + A face is represented by a series of edges(rays), i.e. + .. code-block:: python + + faceOrig[] = [orig1u, orig1v, orig2u, orig2v, ... ] + faceVec[] = [vec1u, vec1v, vec2u, vec2v, ... ] + """ + face1Size = len(face1Orig) + face2Size = len(face2Orig) + for i in xrange(0, face1Size, 2): # noqa: F821 + o1x = face1Orig[i] + o1y = face1Orig[i+1] + v1x = face1Vec[i] + v1y = face1Vec[i+1] + n1x = v1y + n1y = -v1x + for j in xrange(0, face2Size, 2): # noqa: F821 + # Given ray1(O1, V1) and ray2(O2, V2) + # Normal of ray1 is (V1.y, V1.x) + o2x = face2Orig[j] + o2y = face2Orig[j+1] + v2x = face2Vec[j] + v2y = face2Vec[j+1] + n2x = v2y + n2y = -v2x + + # Find t for ray2 + # t = [(o1x-o2x)n1x + (o1y-o2y)n1y] / + # (v2x * n1x + v2y * n1y) + denum = v2x * n1x + v2y * n1y + # Edges are parallel if denum is close to 0. + if math.fabs(denum) < 0.000001: + continue + t2 = ((o1x-o2x) * n1x + (o1y-o2y) * n1y) / denum + if (t2 < 0.00001 or t2 > 0.99999): + continue + + # Find t for ray1 + # t = [(o2x-o1x)n2x + # + (o2y-o1y)n2y] / (v1x * n2x + v1y * n2y) + denum = v1x * n2x + v1y * n2y + # Edges are parallel if denum is close to 0. + if math.fabs(denum) < 0.000001: + continue + t1 = ((o2x-o1x) * n2x + (o2y-o1y) * n2y) / denum + + # Edges intersect + if (t1 > 0.00001 and t1 < 0.99999): + return 1 + + return 0 + + def _getOverlapUVFaces(self, meshName): + """ Return overlapping faces + + :param meshName: name of mesh + :type meshName: str + :returns: list of overlapping faces + :rtype: list + """ + faces = [] + # find polygon mesh node + selList = om.MSelectionList() + selList.add(meshName) + mesh = selList.getDependNode(0) + if mesh.apiType() == om.MFn.kTransform: + dagPath = selList.getDagPath(0) + dagFn = om.MFnDagNode(dagPath) + child = dagFn.child(0) + if child.apiType() != om.MFn.kMesh: + raise Exception("Can't find polygon mesh") + mesh = child + meshfn = om.MFnMesh(mesh) + + center, radius = self._createBoundingCircle(meshfn) + for i in xrange(meshfn.numPolygons): # noqa: F821 + rayb1, face1Orig, face1Vec = self._createRayGivenFace( + meshfn, i) + if not rayb1: + continue + cui = center[2*i] + cvi = center[2*i+1] + ri = radius[i] + # Exclude the degenerate face + # if(area(face1Orig) < 0.000001) continue; + # Loop through face j where j != i + for j in range(i+1, meshfn.numPolygons): + cuj = center[2*j] + cvj = center[2*j+1] + rj = radius[j] + du = cuj - cui + dv = cvj - cvi + dsqr = du * du + dv * dv + # Quick rejection if bounding circles don't overlap + if (dsqr >= (ri + rj) * (ri + rj)): + continue + + rayb2, face2Orig, face2Vec = self._createRayGivenFace( + meshfn, j) + if not rayb2: + continue + # Exclude the degenerate face + # if(area(face2Orig) < 0.000001): continue; + if self._checkCrossingEdges(face1Orig, + face1Vec, + face2Orig, + face2Vec): + face1 = '%s.f[%d]' % (meshfn.name(), i) + face2 = '%s.f[%d]' % (meshfn.name(), j) + if face1 not in faces: + faces.append(face1) + if face2 not in faces: + faces.append(face2) + return faces + + class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin): """ Validate the current mesh overlapping UVs. @@ -23,220 +240,6 @@ class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin): actions = [pype.maya.action.SelectInvalidAction] optional = True - def _createBoundingCircle(self, meshfn): - """ Represent a face by center and radius - - :param meshfn: MFnMesh class - :type meshfn: :class:`maya.api.OpenMaya.MFnMesh` - :returns: (center, radius) - :rtype: tuple - """ - center = [] - radius = [] - for i in xrange(meshfn.numPolygons): # noqa: F821 - # get uvs from face - uarray = [] - varray = [] - for j in range(len(meshfn.getPolygonVertices(i))): - uv = meshfn.getPolygonUV(i, j) - uarray.append(uv[0]) - varray.append(uv[1]) - - # loop through all vertices to construct edges/rays - cu = 0.0 - cv = 0.0 - for j in range(len(uarray)): - cu += uarray[j] - cv += varray[j] - - cu /= len(uarray) - cv /= len(varray) - rsqr = 0.0 - for j in range(len(varray)): - du = uarray[j] - cu - dv = varray[j] - cv - dsqr = du * du + dv * dv - rsqr = dsqr if dsqr > rsqr else rsqr - - center.append(cu) - center.append(cv) - radius.append(math.sqrt(rsqr)) - - return center, radius - - def _createRayGivenFace(self, meshfn, faceId): - """ Represent a face by a series of edges(rays), i.e. - - :param meshfn: MFnMesh class - :type meshfn: :class:`maya.api.OpenMaya.MFnMesh` - :param faceId: face id - :type faceId: int - :returns: False if no valid uv's. - ""(True, orig, vec)"" or ""(False, None, None)"" - :rtype: tuple - - .. code-block:: python - - orig = [orig1u, orig1v, orig2u, orig2v, ... ] - vec = [vec1u, vec1v, vec2u, vec2v, ... ] - """ - orig = [] - vec = [] - # get uvs - uarray = [] - varray = [] - for i in range(len(meshfn.getPolygonVertices(faceId))): - uv = meshfn.getPolygonUV(faceId, i) - uarray.append(uv[0]) - varray.append(uv[1]) - - if len(uarray) == 0 or len(varray) == 0: - return (False, None, None) - - # loop throught all vertices to construct edges/rays - u = uarray[-1] - v = varray[-1] - for i in xrange(len(uarray)): # noqa: F821 - orig.append(uarray[i]) - orig.append(varray[i]) - vec.append(u - uarray[i]) - vec.append(v - varray[i]) - u = uarray[i] - v = varray[i] - - return (True, orig, vec) - - def _checkCrossingEdges(self, - face1Orig, - face1Vec, - face2Orig, - face2Vec): - """ Check if there are crossing edges between two faces. - Return True if there are crossing edges and False otherwise. - - :param face1Orig: origin of face 1 - :type face1Orig: tuple - :param face1Vec: face 1 edges - :type face1Vec: list - :param face2Orig: origin of face 2 - :type face2Orig: tuple - :param face2Vec: face 2 edges - :type face2Vec: list - - A face is represented by a series of edges(rays), i.e. - .. code-block:: python - - faceOrig[] = [orig1u, orig1v, orig2u, orig2v, ... ] - faceVec[] = [vec1u, vec1v, vec2u, vec2v, ... ] - """ - face1Size = len(face1Orig) - face2Size = len(face2Orig) - for i in xrange(0, face1Size, 2): # noqa: F821 - o1x = face1Orig[i] - o1y = face1Orig[i+1] - v1x = face1Vec[i] - v1y = face1Vec[i+1] - n1x = v1y - n1y = -v1x - for j in xrange(0, face2Size, 2): # noqa: F821 - # Given ray1(O1, V1) and ray2(O2, V2) - # Normal of ray1 is (V1.y, V1.x) - o2x = face2Orig[j] - o2y = face2Orig[j+1] - v2x = face2Vec[j] - v2y = face2Vec[j+1] - n2x = v2y - n2y = -v2x - - # Find t for ray2 - # t = [(o1x-o2x)n1x + (o1y-o2y)n1y] / - # (v2x * n1x + v2y * n1y) - denum = v2x * n1x + v2y * n1y - # Edges are parallel if denum is close to 0. - if math.fabs(denum) < 0.000001: - continue - t2 = ((o1x-o2x) * n1x + (o1y-o2y) * n1y) / denum - if (t2 < 0.00001 or t2 > 0.99999): - continue - - # Find t for ray1 - # t = [(o2x-o1x)n2x - # + (o2y-o1y)n2y] / (v1x * n2x + v1y * n2y) - denum = v1x * n2x + v1y * n2y - # Edges are parallel if denum is close to 0. - if math.fabs(denum) < 0.000001: - continue - t1 = ((o2x-o1x) * n2x + (o2y-o1y) * n2y) / denum - - # Edges intersect - if (t1 > 0.00001 and t1 < 0.99999): - return 1 - - return 0 - - def _getOverlapUVFaces(self, meshName): - """ Return overlapping faces - - :param meshName: name of mesh - :type meshName: str - :returns: list of overlapping faces - :rtype: list - """ - faces = [] - # find polygon mesh node - selList = om.MSelectionList() - selList.add(meshName) - mesh = selList.getDependNode(0) - if mesh.apiType() == om.MFn.kTransform: - dagPath = selList.getDagPath(0) - dagFn = om.MFnDagNode(dagPath) - child = dagFn.child(0) - if child.apiType() != om.MFn.kMesh: - raise Exception("Can't find polygon mesh") - mesh = child - meshfn = om.MFnMesh(mesh) - - center, radius = self._createBoundingCircle(meshfn) - for i in xrange(meshfn.numPolygons): # noqa: F821 - rayb1, face1Orig, face1Vec = self._createRayGivenFace( - meshfn, i) - if not rayb1: - continue - cui = center[2*i] - cvi = center[2*i+1] - ri = radius[i] - # Exclude the degenerate face - # if(area(face1Orig) < 0.000001) continue; - # Loop through face j where j != i - for j in range(i+1, meshfn.numPolygons): - cuj = center[2*j] - cvj = center[2*j+1] - rj = radius[j] - du = cuj - cui - dv = cvj - cvi - dsqr = du * du + dv * dv - # Quick rejection if bounding circles don't overlap - if (dsqr >= (ri + rj) * (ri + rj)): - continue - - rayb2, face2Orig, face2Vec = self._createRayGivenFace( - meshfn, j) - if not rayb2: - continue - # Exclude the degenerate face - # if(area(face2Orig) < 0.000001): continue; - if self._checkCrossingEdges(face1Orig, - face1Vec, - face2Orig, - face2Vec): - face1 = '%s.f[%d]' % (meshfn.name(), i) - face2 = '%s.f[%d]' % (meshfn.name(), j) - if face1 not in faces: - faces.append(face1) - if face2 not in faces: - faces.append(face2) - return faces - @classmethod def _has_overlapping_uvs(cls, node): """ Check if mesh has overlapping UVs. @@ -246,9 +249,11 @@ class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin): :returns: True is has overlapping UVs, False otherwise :rtype: bool """ + ovl = GetOverlappingUVs() + for i, uv in enumerate(polyUVSet(node, q=1, auv=1)): polyUVSet(node, cuv=1, uvSet=uv) - of = cls._getOverlapUVFaces(str(node)) + of = ovl._getOverlapUVFaces(str(node)) if of != []: return True return False From 62e573613a3471575a0909d1695d6e96ca98dd8b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Sat, 9 Mar 2019 17:13:01 +0100 Subject: [PATCH 24/99] removed old code --- pype/ftrack/actions/action_djvview.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index d8e6996db4..64467a748a 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -47,28 +47,6 @@ class DJVViewAction(BaseHandler): }) self.items = items - - items = [] - applications = self.get_applications() - applications = sorted( - applications, key=lambda application: application["label"] - ) - - for application in applications: - self.djv_path = application.get("path", None) - applicationIdentifier = application["identifier"] - label = application["label"] - items.append({ - "actionIdentifier": self.identifier, - "label": label, - "variant": application.get("variant", None), - "description": application.get("description", None), - "icon": application.get("icon", "default"), - "applicationIdentifier": applicationIdentifier - }) - - self.items = items - if self.identifier is None: raise ValueError( 'Action missing identifier.' @@ -347,7 +325,6 @@ class DJVViewAction(BaseHandler): try: # TODO This is proper way to get filepath!!! - # THIS WON'T WORK RIGHT NOW location = component[ 'component_locations' ][0]['location'] From a05c146f4343a04ab6ff32f27f842f69a9e76410 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Sat, 9 Mar 2019 17:14:02 +0100 Subject: [PATCH 25/99] action discovery won't crash if djv view is not installed --- pype/ftrack/actions/action_djvview.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index 64467a748a..1e624965ea 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -27,7 +27,7 @@ class DJVViewAction(BaseHandler): self.djv_path = None self.config_data = None - items = [] + self.items = [] if self.config_data is None: self.load_config_data() @@ -37,7 +37,7 @@ class DJVViewAction(BaseHandler): applicationIdentifier = application["identifier"] label = application["label"] - items.append({ + self.items.append({ "actionIdentifier": self.identifier, "label": label, "variant": application.get("variant", None), @@ -46,7 +46,6 @@ class DJVViewAction(BaseHandler): "applicationIdentifier": applicationIdentifier }) - self.items = items if self.identifier is None: raise ValueError( 'Action missing identifier.' From d72e6ccd893350b86cf5a9da4a78447d52b8f2f1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Sat, 9 Mar 2019 18:50:26 +0100 Subject: [PATCH 26/99] check if djv_path is set before selection validation --- pype/ftrack/actions/action_djvview.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index 1e624965ea..683f8d1d09 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -66,7 +66,8 @@ class DJVViewAction(BaseHandler): def discover(self, event): """Return available actions based on *event*. """ - + if self.djv_path is None: + return if not self.is_valid_selection(event): return From 3fb4f30e9b944a0e6ffdaf0b10e8a89a5d907134 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:43:00 +0100 Subject: [PATCH 27/99] added clockify check to app launch --- pype/ftrack/lib/ftrack_app_handler.py | 42 +++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index fd5b758f22..7d773136e7 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -146,6 +146,25 @@ class AppAction(BaseHandler): entity = entities[0] project_name = entity['project']['full_name'] + # Validate Clockify settings if Clockify is required + clockify_timer = os.environ.get('CLOCKIFY_WORKSPACE', None) + if clockify_timer is not None: + from pype.clockify import ClockifyAPI + clockapi = ClockifyAPI() + if clockapi.verify_api() is False: + title = 'Launch message' + header = '# You Can\'t launch **any Application**' + message = ( + '

You don\'t have set Clockify API' + ' key in Clockify settings

' + ) + items = [ + {'type': 'label', 'value': header}, + {'type': 'label', 'value': message} + ] + self.show_interface(event, items, title) + return False + database = pypelib.get_avalon_database() # Get current environments @@ -293,6 +312,29 @@ class AppAction(BaseHandler): self.log.info('Starting timer for task: ' + task['name']) user.start_timer(task, force=True) + # RUN TIMER IN Clockify + if clockify_timer is not None: + task_name = task['name'] + project_name = task['project']['full_name'] + + def get_parents(entity): + output = [] + if entity.entity_type.lower() == 'project': + return output + output.extend(get_parents(entity['parent'])) + output.append(entity['name']) + + return output + + desc_items = get_parents(task['parent']) + description = '/'.join(desc_items) + + project_id = clockapi.get_project_id(project_name) + task_id = clockapi.get_task_id(task_name, project_id) + clockapi.start_time_entry( + description, project_id, task_id, + ) + # Change status of task to In progress config = get_config_data() From 2360b444041d2caf9cf22f4bb6f027ecce5d4ee8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:48:47 +0100 Subject: [PATCH 28/99] ftrack base handler can handle if items are result --- pype/ftrack/lib/ftrack_base_handler.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index 9d94d50072..91fee3d2fc 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -289,13 +289,15 @@ class BaseHandler(object): } elif isinstance(result, dict): - for key in ('success', 'message'): - if key in result: - continue + items = 'items' in result + if items is False: + for key in ('success', 'message'): + if key in result: + continue - raise KeyError( - 'Missing required key: {0}.'.format(key) - ) + raise KeyError( + 'Missing required key: {0}.'.format(key) + ) else: self.log.error( From 1909d61f09046d9a711663014df0e66562a1239f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:52:45 +0100 Subject: [PATCH 29/99] removed _name attributes from clockify api functions --- pype/clockify/clockify_api.py | 108 ++++++++---------- pype/ftrack/actions/action_clockify_start.py | 7 +- pype/ftrack/actions/action_clockify_sync.py | 6 +- .../plugins/launcher/actions/ClockifyStart.py | 6 +- pype/plugins/launcher/actions/ClockifySync.py | 4 +- 5 files changed, 60 insertions(+), 71 deletions(-) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index b250377756..06acfa7bc5 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -109,8 +109,9 @@ class ClockifyAPI(metaclass=Singleton): workspace["name"]: workspace["id"] for workspace in response.json() } - def get_projects(self, workspace_name=None, workspace_id=None): - workspace_id = self.convert_input(workspace_id, workspace_name) + def get_projects(self, workspace_id): + if workspace_id is None: + workspace_id = self.workspace_id action_url = 'workspaces/{}/projects/'.format(workspace_id) response = requests.get( self.endpoint + action_url, @@ -121,10 +122,9 @@ class ClockifyAPI(metaclass=Singleton): project["name"]: project["id"] for project in response.json() } - def get_tags( - self, workspace_name=None, workspace_id=None - ): - workspace_id = self.convert_input(workspace_id, workspace_name) + def get_tags(self, workspace_id): + if workspace_id is None: + workspace_id = self.workspace_id action_url = 'workspaces/{}/tags/'.format(workspace_id) response = requests.get( self.endpoint + action_url, @@ -135,13 +135,9 @@ class ClockifyAPI(metaclass=Singleton): tag["name"]: tag["id"] for tag in response.json() } - def get_tasks( - self, - workspace_name=None, project_name=None, - workspace_id=None, project_id=None - ): - workspace_id = self.convert_input(workspace_id, workspace_name) - project_id = self.convert_input(project_id, project_name, 'Project') + def get_tasks(self, project_id, workspace_id=None): + if workspace_id is None: + workspace_id = self.add_workspace_id action_url = 'workspaces/{}/projects/{}/tasks/'.format( workspace_id, project_id ) @@ -160,31 +156,29 @@ class ClockifyAPI(metaclass=Singleton): return None return all_workspaces[workspace_name] - def get_project_id( - self, project_name, workspace_name=None, workspace_id=None - ): - workspace_id = self.convert_input(workspace_id, workspace_name) - all_projects = self.get_projects(workspace_id=workspace_id) + def get_project_id(self, project_name, workspace_id=None): + if workspace_id is None: + workspace_id = self.workspace_id + all_projects = self.get_projects(workspace_id) if project_name not in all_projects: return None return all_projects[project_name] - def get_tag_id(self, tag_name, workspace_name=None, workspace_id=None): - workspace_id = self.convert_input(workspace_id, workspace_name) - all_tasks = self.get_tags(workspace_id=workspace_id) + def get_tag_id(self, tag_name, workspace_id=None): + if workspace_id is None: + workspace_id = self.workspace_id + all_tasks = self.get_tags(workspace_id) if tag_name not in all_tasks: return None return all_tasks[tag_name] def get_task_id( - self, task_name, - project_name=None, workspace_name=None, - project_id=None, workspace_id=None + self, task_name, project_id, workspace_id=None ): - workspace_id = self.convert_input(workspace_id, workspace_name) - project_id = self.convert_input(project_id, project_name, 'Project') + if workspace_id is None: + workspace_id = self.workspace_id all_tasks = self.get_tasks( - workspace_id=workspace_id, project_id=project_id + project_id, workspace_id ) if task_name not in all_tasks: return None @@ -194,21 +188,16 @@ class ClockifyAPI(metaclass=Singleton): return str(datetime.datetime.utcnow().isoformat())+'Z' def start_time_entry( - self, description, project_name=None, task_name=None, - billable=True, workspace_name=None, - project_id=None, task_id=None, workspace_id=None + self, description, project_id, task_id, + workspace_id=None, billable=True ): # Workspace - workspace_id = self.convert_input(workspace_id, workspace_name) - # Project - project_id = self.convert_input( - project_id, project_name, 'Project' - ) - # Task - task_id = self.convert_input(task_id, task_name, 'Task', project_id) + if workspace_id is None: + workspace_id = self.workspace_id + print(workspace_id) # Check if is currently run another times and has same values - current = self.get_in_progress(workspace_id=workspace_id) + current = self.get_in_progress(workspace_id) if current is not None: if ( current.get("description", None) == description and @@ -217,7 +206,7 @@ class ClockifyAPI(metaclass=Singleton): ): self.bool_timer_run = True return self.bool_timer_run - self.finish_time_entry(workspace_id=workspace_id) + self.finish_time_entry(workspace_id) # Convert billable to strings if billable: @@ -246,8 +235,9 @@ class ClockifyAPI(metaclass=Singleton): success = True return success - def get_in_progress(self, workspace_name=None, workspace_id=None): - workspace_id = self.convert_input(workspace_id, workspace_name) + def get_in_progress(self, workspace_id=None): + if workspace_id is None: + workspace_id = self.workspace_id action_url = 'workspaces/{}/timeEntries/inProgress'.format( workspace_id ) @@ -261,9 +251,10 @@ class ClockifyAPI(metaclass=Singleton): output = None return output - def finish_time_entry(self, workspace_name=None, workspace_id=None): - workspace_id = self.convert_input(workspace_id, workspace_name) - current = self.get_in_progress(workspace_id=workspace_id) + def finish_time_entry(self, workspace_id=None): + if workspace_id is None: + workspace_id = self.workspace_id + current = self.get_in_progress(workspace_id) current_id = current["id"] action_url = 'workspaces/{}/timeEntries/{}'.format( workspace_id, current_id @@ -285,9 +276,10 @@ class ClockifyAPI(metaclass=Singleton): return response.json() def get_time_entries( - self, quantity=10, workspace_name=None, workspace_id=None + self, workspace_id=None, quantity=10 ): - workspace_id = self.convert_input(workspace_id, workspace_name) + if workspace_id is None: + workspace_id = self.workspace_id action_url = 'workspaces/{}/timeEntries/'.format(workspace_id) response = requests.get( self.endpoint + action_url, @@ -295,8 +287,9 @@ class ClockifyAPI(metaclass=Singleton): ) return response.json()[:quantity] - def remove_time_entry(self, tid, workspace_name=None, workspace_id=None): - workspace_id = self.convert_input(workspace_id, workspace_name) + def remove_time_entry(self, tid, workspace_id=None): + if workspace_id is None: + workspace_id = self.workspace_id action_url = 'workspaces/{}/timeEntries/{}'.format( workspace_id, tid ) @@ -306,8 +299,9 @@ class ClockifyAPI(metaclass=Singleton): ) return response.json() - def add_project(self, name, workspace_name=None, workspace_id=None): - workspace_id = self.convert_input(workspace_id, workspace_name) + def add_project(self, name, workspace_id=None): + if workspace_id is None: + workspace_id = self.workspace_id action_url = 'workspaces/{}/projects/'.format(workspace_id) body = { "name": name, @@ -338,11 +332,10 @@ class ClockifyAPI(metaclass=Singleton): return response.json() def add_task( - self, name, project_name=None, project_id=None, - workspace_name=None, workspace_id=None + self, name, project_id, workspace_id=None ): - workspace_id = self.convert_input(workspace_id, workspace_name) - project_id = self.convert_input(project_id, project_name, 'Project') + if workspace_id is None: + workspace_id = self.workspace_id action_url = 'workspaces/{}/projects/{}/tasks/'.format( workspace_id, project_id ) @@ -358,11 +351,10 @@ class ClockifyAPI(metaclass=Singleton): return response.json() def delete_project( - self, project_name=None, project_id=None, - workspace_name=None, workspace_id=None + self, project_id, workspace_id=None ): - workspace_id = self.convert_input(workspace_id, workspace_name) - project_id = self.convert_input(project_id, project_name, 'Project') + if workspace_id is None: + workspace_id = self.workspace_id action_url = '/workspaces/{}/projects/{}'.format( workspace_id, project_id ) diff --git a/pype/ftrack/actions/action_clockify_start.py b/pype/ftrack/actions/action_clockify_start.py index 7fabf1d0b7..d0f8d96c49 100644 --- a/pype/ftrack/actions/action_clockify_start.py +++ b/pype/ftrack/actions/action_clockify_start.py @@ -44,11 +44,10 @@ class StartClockify(BaseAction): desc_items = get_parents(task['parent']) description = '/'.join(desc_items) - + project_id = self.clockapi.get_project_id(project_name) + task_id = self.clockapi.get_task_id(task_name, project_id) self.clockapi.start_time_entry( - description=description, - project_name=project_name, - task_name=task_name, + description, project_id, task_id ) return True diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py index 84e47005f5..335521c5cb 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -127,13 +127,11 @@ class SyncClocify(BaseAction): else: project_id = clockify_projects[project_name] - clockify_project_tasks = self.clockapi.get_tasks( - project_id=project_id - ) + clockify_project_tasks = self.clockapi.get_tasks(project_id) for task_type in task_types: if task_type not in clockify_project_tasks: response = self.clockapi.add_task( - task_type, project_id=project_id + task_type, project_id ) if 'id' not in response: self.log.error('Task {} can\'t be created'.format( diff --git a/pype/plugins/launcher/actions/ClockifyStart.py b/pype/plugins/launcher/actions/ClockifyStart.py index e1f17f2aa3..c300df3389 100644 --- a/pype/plugins/launcher/actions/ClockifyStart.py +++ b/pype/plugins/launcher/actions/ClockifyStart.py @@ -33,8 +33,8 @@ class ClockifyStart(api.Action): desc_items.append(asset_name) description = '/'.join(desc_items) + project_id = self.clockapi.get_project_id(project_name) + task_id = self.clockapi.get_task_id(task_name, project_id) self.clockapi.start_time_entry( - description=description, - project_name=project_name, - task_name=task_name, + description, project_id, task_id ) diff --git a/pype/plugins/launcher/actions/ClockifySync.py b/pype/plugins/launcher/actions/ClockifySync.py index 31ae1f3424..2c3f61b681 100644 --- a/pype/plugins/launcher/actions/ClockifySync.py +++ b/pype/plugins/launcher/actions/ClockifySync.py @@ -47,12 +47,12 @@ class ClockifySync(api.Action): project_id = clockify_projects[project_name] clockify_project_tasks = self.clockapi.get_tasks( - project_id=project_id + project_id ) for task_type in task_types: if task_type not in clockify_project_tasks: response = self.clockapi.add_task( - task_type, project_id=project_id + task_type, project_id ) if 'id' not in response: self.log.error('Task {} can\'t be created'.format( From 4be701605b05855b39c660d0f2c72d3ec51b05a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:53:57 +0100 Subject: [PATCH 30/99] cosmetic changes in code --- pype/clockify/widget_settings.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pype/clockify/widget_settings.py b/pype/clockify/widget_settings.py index 8b8abb25da..409c7893e4 100644 --- a/pype/clockify/widget_settings.py +++ b/pype/clockify/widget_settings.py @@ -120,13 +120,12 @@ class ClockifySettings(QtWidgets.QWidget): def click_ok(self): api_key = self.input_api_key.text().strip() - if self.optional: - if api_key == '': - self.clockapi.save_api_key(None) - self.clockapi.set_api(api_key) - self.validated = False - self._close_widget() - return + if self.optional is True and api_key == '': + self.clockapi.save_api_key(None) + self.clockapi.set_api(api_key) + self.validated = False + self._close_widget() + return validation = self.clockapi.validate_api_key(api_key) From ded5e0df862a411e6e53a07f18a2d1537ff8ad6a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:54:27 +0100 Subject: [PATCH 31/99] setting widget will close in not optional (not used yet) --- pype/clockify/widget_settings.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pype/clockify/widget_settings.py b/pype/clockify/widget_settings.py index 409c7893e4..02fd4350e6 100644 --- a/pype/clockify/widget_settings.py +++ b/pype/clockify/widget_settings.py @@ -142,8 +142,14 @@ class ClockifySettings(QtWidgets.QWidget): ) def closeEvent(self, event): - event.ignore() - self._close_widget() + if self.optional is True: + event.ignore() + self._close_widget() + else: + self.validated = False def _close_widget(self): - self.hide() + if self.optional is True: + self.hide() + else: + self.close() From 9573669a91b8b6fceffea98eaee7881ad98abdb9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:55:29 +0100 Subject: [PATCH 32/99] verify api added so actions can handle with unlogged users --- pype/clockify/clockify_api.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index 06acfa7bc5..c8cae86a66 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -23,6 +23,12 @@ class ClockifyAPI(metaclass=Singleton): file_name = 'clockify.json' fpath = os.path.join(app_dir, file_name) + def verify_api(self): + for key, value in self.headers.items(): + if value is None or value.strip() == '': + return False + return True + def set_api(self, api_key=None): if api_key is None: api_key = self.get_api_key() From 7dd5c8a8eec4e0a7f83b28e27573b1c398e16f1e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:56:24 +0100 Subject: [PATCH 33/99] workspace is set when user is logged in --- pype/clockify/clockify_api.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index c8cae86a66..f82cc0265d 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -22,6 +22,7 @@ class ClockifyAPI(metaclass=Singleton): app_dir = os.path.normpath(appdirs.user_data_dir('pype-app', 'pype')) file_name = 'clockify.json' fpath = os.path.join(app_dir, file_name) + workspace_id = None def verify_api(self): for key, value in self.headers.items(): @@ -35,6 +36,7 @@ class ClockifyAPI(metaclass=Singleton): if api_key is not None and self.validate_api_key(api_key) is True: self.headers["X-Api-Key"] = api_key + self.set_workspace() return True return False @@ -68,20 +70,23 @@ class ClockifyAPI(metaclass=Singleton): def set_workspace(self, name=None): if name is None: - self.workspace = None - self.workspace_id = None + name = os.environ.get('CLOCKIFY_WORKSPACE', None) + self.workspace = name + self.workspace_id = None + if self.workspace is None: return - result = self.validate_workspace(name) - if result is False: - self.workspace = None - self.workspace_id = None - return False - else: - self.workspace = name + try: + result = self.validate_workspace() + except Exception: + result = False + if result is not False: self.workspace_id = result return True + return False - def validate_workspace(self, name): + def validate_workspace(self, name=None): + if name is None: + name = self.workspace all_workspaces = self.get_workspaces() if name in all_workspaces: return all_workspaces[name] From 695ca47ccde3030e65a3d6313a9cae314f5ceb79 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:57:07 +0100 Subject: [PATCH 34/99] clockify api(singleton) can have master object --- pype/clockify/clockify_api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index f82cc0265d..ab9d1e6f67 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -22,8 +22,12 @@ class ClockifyAPI(metaclass=Singleton): app_dir = os.path.normpath(appdirs.user_data_dir('pype-app', 'pype')) file_name = 'clockify.json' fpath = os.path.join(app_dir, file_name) + master_parent = None workspace_id = None + def set_master(self, master_parent): + self.master_parent = master_parent + def verify_api(self): for key, value in self.headers.items(): if value is None or value.strip() == '': From 7797b7732bf9b1c1b253b3f0f33443b69c80bcd3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 16:58:05 +0100 Subject: [PATCH 35/99] clockify is master of clockify api --- pype/clockify/clockify.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py index 1a67eeba92..2b5e54b0c5 100644 --- a/pype/clockify/clockify.py +++ b/pype/clockify/clockify.py @@ -21,16 +21,14 @@ class ClockifyModule: self.bool_timer_run = False def start_up(self): + self.clockapi.set_master(self) self.bool_api_key_set = self.clockapi.set_api() if self.bool_api_key_set is False: self.show_settings() return - workspace = os.environ.get('CLOCKIFY_WORKSPACE', None) - self.bool_workspace_set = self.clockapi.set_workspace(workspace) + self.bool_workspace_set = self.clockapi.workspace_id is not None if self.bool_workspace_set is False: - # TODO show message to user - print("Nope Workspace: clockify.py - line 29") return self.bool_thread_check_running = True From 8e079571eb1c274e23afa7bd498f549365732a9d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 17:02:09 +0100 Subject: [PATCH 36/99] added clockify availability check to ftrack actions --- pype/ftrack/actions/action_clockify_start.py | 2 ++ pype/ftrack/actions/action_clockify_sync.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/pype/ftrack/actions/action_clockify_start.py b/pype/ftrack/actions/action_clockify_start.py index d0f8d96c49..49501b1183 100644 --- a/pype/ftrack/actions/action_clockify_start.py +++ b/pype/ftrack/actions/action_clockify_start.py @@ -26,6 +26,8 @@ class StartClockify(BaseAction): return False if entities[0].entity_type.lower() != 'task': return False + if self.clockapi.workspace_id is None: + return False return True def launch(self, session, entities, event): diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py index 335521c5cb..3b975679ec 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -26,6 +26,9 @@ class SyncClocify(BaseAction): clockapi = ClockifyAPI() def register(self): + if self.clockapi.workspace_id is None: + raise ValueError('Clockify Workspace or API key are not set!') + if self.clockapi.validate_workspace_perm() is False: raise MissingPermision super().register() From 849137e5b1cab4c3bfee71a571e54ff8c170f50d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 17:53:02 +0100 Subject: [PATCH 37/99] it is possible to not enter workspace id for get projects --- pype/clockify/clockify_api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index ab9d1e6f67..450c99cdea 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -124,7 +124,7 @@ class ClockifyAPI(metaclass=Singleton): workspace["name"]: workspace["id"] for workspace in response.json() } - def get_projects(self, workspace_id): + def get_projects(self, workspace_id=None): if workspace_id is None: workspace_id = self.workspace_id action_url = 'workspaces/{}/projects/'.format(workspace_id) @@ -209,7 +209,6 @@ class ClockifyAPI(metaclass=Singleton): # Workspace if workspace_id is None: workspace_id = self.workspace_id - print(workspace_id) # Check if is currently run another times and has same values current = self.get_in_progress(workspace_id) From 1d5290f4d521262553ee945ef5f410a08514018f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 17:53:21 +0100 Subject: [PATCH 38/99] admin permissions are better checked (hope) --- pype/clockify/clockify_api.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index 450c99cdea..55e3eb4882 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -67,10 +67,18 @@ class ClockifyAPI(metaclass=Singleton): self.endpoint + action_url, headers=self.headers, json=body ) - if response.status_code >= 300: - return False - self.delete_project(test_project) - return True + if response.status_code == 201: + self.delete_project(self.get_project_id(test_project)) + return True + else: + projects = self.get_projects() + if test_project in projects: + try: + self.delete_project(self.get_project_id(test_project)) + return True + except json.decoder.JSONDecodeError: + return False + return False def set_workspace(self, name=None): if name is None: From b5a13468c10702bd98252708f142cbdf663a05fa Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 17:53:46 +0100 Subject: [PATCH 39/99] start timer thread if workspace_id is set --- pype/clockify/clockify_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index 55e3eb4882..efbd79dac2 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -93,6 +93,8 @@ class ClockifyAPI(metaclass=Singleton): result = False if result is not False: self.workspace_id = result + if self.master_parent is not None: + self.master_parent.start_timer_check() return True return False From 41c31e01e9df9160561c04ff55e343f220b8fc00 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 17:54:13 +0100 Subject: [PATCH 40/99] missing permission error is more specific now --- pype/ftrack/lib/ftrack_base_handler.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index 91fee3d2fc..146259b5e8 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -5,8 +5,10 @@ from pype import api as pype class MissingPermision(Exception): - def __init__(self): - super().__init__('Missing permission') + def __init__(self, message=None): + if message is None: + message = 'Ftrack' + super().__init__(message) class BaseHandler(object): @@ -64,10 +66,10 @@ class BaseHandler(object): self.log.info(( '{} "{}" - Registered successfully ({:.4f}sec)' ).format(self.type, label, run_time)) - except MissingPermision: + except MissingPermision as MPE: self.log.info(( - '!{} "{}" - You\'re missing required permissions' - ).format(self.type, label)) + '!{} "{}" - You\'re missing required {} permissions' + ).format(self.type, label, str(MPE))) except NotImplementedError: self.log.error(( '{} "{}" - Register method is not implemented' From 4db427dcaf5c95e48c2f8d0bda22861eee9ea470 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 17:54:46 +0100 Subject: [PATCH 41/99] Missing permission in clockify is more specific --- pype/ftrack/actions/action_clockify_sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py index 3b975679ec..de91e68f50 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -30,7 +30,7 @@ class SyncClocify(BaseAction): raise ValueError('Clockify Workspace or API key are not set!') if self.clockapi.validate_workspace_perm() is False: - raise MissingPermision + raise MissingPermision('Clockify') super().register() def discover(self, session, entities, event): From 82a945aac4234f170b6ce06731dee220e545beff Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 17:55:13 +0100 Subject: [PATCH 42/99] clockify module again check for running timer --- pype/clockify/clockify.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py index 2b5e54b0c5..a22933f700 100644 --- a/pype/clockify/clockify.py +++ b/pype/clockify/clockify.py @@ -1,4 +1,3 @@ -import os import threading from app import style from app.vendor.Qt import QtWidgets @@ -12,6 +11,7 @@ class ClockifyModule: self.parent = parent self.clockapi = ClockifyAPI() self.widget_settings = ClockifySettings(main_parent, self) + self.widget_settings_required = None self.thread_timer_check = None # Bools @@ -31,16 +31,12 @@ class ClockifyModule: if self.bool_workspace_set is False: return - self.bool_thread_check_running = True self.start_timer_check() self.set_menu_visibility() - def change_timer_run(self, bool_run): - self.bool_timer_run = bool_run - self.set_menu_visibility() - def start_timer_check(self): + self.bool_thread_check_running = True if self.thread_timer_check is None: self.thread_timer_check = threading.Thread( target=self.check_running @@ -48,6 +44,12 @@ class ClockifyModule: self.thread_timer_check.daemon = True self.thread_timer_check.start() + def stop_timer_check(self): + self.bool_thread_check_running = True + if self.thread_timer_check is not None: + self.thread_timer_check.join() + self.thread_timer_check = None + def check_running(self): import time while self.bool_thread_check_running is True: @@ -60,6 +62,7 @@ class ClockifyModule: def stop_timer(self): self.clockapi.finish_time_entry() + self.bool_timer_run = False # Definition of Tray menu def tray_menu(self, parent): From 5529c531cf09de85b2c5e38b0323d76a66113739 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 18:04:15 +0100 Subject: [PATCH 43/99] clockify sync has white icon in avalon --- pype/plugins/launcher/actions/ClockifySync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/launcher/actions/ClockifySync.py b/pype/plugins/launcher/actions/ClockifySync.py index 2c3f61b681..c7459542aa 100644 --- a/pype/plugins/launcher/actions/ClockifySync.py +++ b/pype/plugins/launcher/actions/ClockifySync.py @@ -8,7 +8,7 @@ class ClockifySync(api.Action): name = "sync_to_clockify" label = "Sync to Clockify" - icon = "clockify_icon" + icon = "clockify_white_icon" order = 500 clockapi = ClockifyAPI() have_permissions = clockapi.validate_workspace_perm() From 292936c8c1f255df83249130eef66febb4842ad8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 19:33:20 +0100 Subject: [PATCH 44/99] clockify is not using task name but task-type name --- pype/ftrack/actions/action_clockify_start.py | 2 +- pype/ftrack/lib/ftrack_app_handler.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/ftrack/actions/action_clockify_start.py b/pype/ftrack/actions/action_clockify_start.py index 49501b1183..d36ab795a9 100644 --- a/pype/ftrack/actions/action_clockify_start.py +++ b/pype/ftrack/actions/action_clockify_start.py @@ -32,7 +32,7 @@ class StartClockify(BaseAction): def launch(self, session, entities, event): task = entities[0] - task_name = task['name'] + task_name = task['type']['name'] project_name = task['project']['full_name'] def get_parents(entity): diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 7d773136e7..01eca55b5b 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -314,7 +314,7 @@ class AppAction(BaseHandler): # RUN TIMER IN Clockify if clockify_timer is not None: - task_name = task['name'] + task_name = task['type']['name'] project_name = task['project']['full_name'] def get_parents(entity): From d19023f3b2fc1102c5fe595f2b387ba91f6c91db Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 11 Mar 2019 19:37:30 +0100 Subject: [PATCH 45/99] removed frames regex (not used) --- pype/ftrack/actions/action_djvview.py | 33 --------------------------- 1 file changed, 33 deletions(-) diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index 683f8d1d09..9c9c593855 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -184,7 +184,6 @@ class DJVViewAction(BaseHandler): # Launching application if "values" in event["data"]: filename = event['data']['values']['path'] - file_type = filename.split(".")[-1] # TODO Is this proper way? try: @@ -192,38 +191,6 @@ class DJVViewAction(BaseHandler): except Exception: fps = 24 - # TODO issequence is probably already built-in validation in ftrack - isseq = re.findall('%[0-9]*d', filename) - if len(isseq) > 0: - if len(isseq) == 1: - frames = [] - padding = re.findall('%[0-9]*d', filename).pop() - index = filename.find(padding) - - full_file = filename[0:index-1] - file = full_file.split(os.sep)[-1] - folder = os.path.dirname(full_file) - - for fname in os.listdir(path=folder): - if fname.endswith(file_type) and file in fname: - frames.append(int(fname.split(".")[-2])) - - if len(frames) > 0: - start = min(frames) - end = max(frames) - - range = (padding % start) + '-' + (padding % end) - filename = re.sub('%[0-9]*d', range, filename) - else: - msg = ( - 'DJV View - Filename has more than one' - ' sequence identifier.' - ) - return { - 'success': False, - 'message': (msg) - } - cmd = [] # DJV path cmd.append(os.path.normpath(self.djv_path)) From f5e203ab7dcee1785ff917ed9c059048552f86f6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 13:41:56 +0100 Subject: [PATCH 46/99] gathering entities was moved to base handler, collect entities event was removed --- pype/ftrack/actions/event_collect_entities.py | 72 ------------------- pype/ftrack/lib/ftrack_base_handler.py | 38 ++++++---- 2 files changed, 25 insertions(+), 85 deletions(-) delete mode 100644 pype/ftrack/actions/event_collect_entities.py diff --git a/pype/ftrack/actions/event_collect_entities.py b/pype/ftrack/actions/event_collect_entities.py deleted file mode 100644 index d5a34b0153..0000000000 --- a/pype/ftrack/actions/event_collect_entities.py +++ /dev/null @@ -1,72 +0,0 @@ -import ftrack_api -from pype.ftrack import BaseEvent - - -class CollectEntities(BaseEvent): - - priority = 1 - - def _launch(self, event): - entities = self.translate_event(event) - event['data']['entities_object'] = entities - - return - - def translate_event(self, event): - selection = event['data'].get('selection', []) - - entities = list() - for entity in selection: - ent = self.session.get( - self.get_entity_type(entity), - entity.get('entityId') - ) - entities.append(ent) - - return entities - - def get_entity_type(self, entity): - '''Return translated entity type tht can be used with API.''' - # Get entity type and make sure it is lower cased. Most places except - # the component tab in the Sidebar will use lower case notation. - entity_type = entity.get('entityType').replace('_', '').lower() - - for schema in self.session.schemas: - alias_for = schema.get('alias_for') - - if ( - alias_for and isinstance(alias_for, str) and - alias_for.lower() == entity_type - ): - return schema['id'] - - for schema in self.session.schemas: - if schema['id'].lower() == entity_type: - return schema['id'] - - raise ValueError( - 'Unable to translate entity type: {0}.'.format(entity_type) - ) - - def register(self): - self.session.event_hub.subscribe( - 'topic=ftrack.action.discover' - ' and source.user.username={0}'.format(self.session.api_user), - self._launch, - priority=self.priority - ) - - self.session.event_hub.subscribe( - 'topic=ftrack.action.launch' - ' and source.user.username={0}'.format(self.session.api_user), - self._launch, - priority=self.priority - ) - - -def register(session, **kw): - '''Register plugin. Called when used as an plugin.''' - if not isinstance(session, ftrack_api.session.Session): - return - - CollectEntities(session).register() diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index a823394bb9..8d8ec64ec5 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -97,9 +97,12 @@ class BaseHandler(object): self.log.info(('{} "{}": Finished').format(self.type, label)) return result except Exception as e: - self.log.error('{} "{}": Failed ({})'.format( - self.type, label, str(e)) - ) + msg = '{} "{}": Failed ({})'.format(self.type, label, str(e)) + self.log.error(msg) + return { + 'success': False, + 'message': msg + } return wrapper_launch @property @@ -165,22 +168,31 @@ class BaseHandler(object): '''Return *event* translated structure to be used with the API.''' _entities = event['data'].get('entities_object', None) - if _entities is None: - selection = event['data'].get('selection', []) - _entities = [] - for entity in selection: - _entities.append( - self.session.get( - self._get_entity_type(entity), - entity.get('entityId') - ) - ) + if ( + _entities is None or + _entities[0].get('link', None) == ftrack_api.symbol.NOT_SET + ): + _entities = self._get_entities(event) return [ _entities, event ] + def _get_entities(self, event): + self.session._local_cache.clear() + selection = event['data'].get('selection', []) + _entities = [] + for entity in selection: + _entities.append( + self.session.get( + self._get_entity_type(entity), + entity.get('entityId') + ) + ) + event['data']['entities_object'] = _entities + return _entities + def _get_entity_type(self, entity): '''Return translated entity type tht can be used with API.''' # Get entity type and make sure it is lower cased. Most places except From 3c115a048143d614529ce2a2e069da0e0a92d7ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 13:44:47 +0100 Subject: [PATCH 47/99] enhanced error handling in sync to avalon --- .../actions/action_sync_to_avalon_local.py | 28 +++------------ pype/ftrack/events/action_sync_to_avalon.py | 28 +++------------ pype/ftrack/events/event_sync_to_avalon.py | 35 ++++++------------- pype/ftrack/lib/avalon_sync.py | 23 ++++++++++++ 4 files changed, 41 insertions(+), 73 deletions(-) diff --git a/pype/ftrack/actions/action_sync_to_avalon_local.py b/pype/ftrack/actions/action_sync_to_avalon_local.py index 68c55be652..88a25ed3ac 100644 --- a/pype/ftrack/actions/action_sync_to_avalon_local.py +++ b/pype/ftrack/actions/action_sync_to_avalon_local.py @@ -82,15 +82,11 @@ class SyncToAvalon(BaseAction): 'user': user, 'status': 'running', 'data': json.dumps({ - 'description': 'Synch Ftrack to Avalon.' + 'description': 'Sync Ftrack to Avalon.' }) }) - + session.commit() try: - self.log.info( - "Action <" + self.__class__.__name__ + "> is running" - ) - self.importable = [] # get from top entity in hierarchy all parent entities @@ -137,26 +133,11 @@ class SyncToAvalon(BaseAction): ) if 'errors' in result and len(result['errors']) > 0: - items = [] - for error in result['errors']: - for key, message in error.items(): - name = key.lower().replace(' ', '') - info = { - 'label': key, - 'type': 'textarea', - 'name': name, - 'value': message - } - items.append(info) - self.log.error( - '{}: {}'.format(key, message) - ) - title = 'Hey You! Few Errors were raised! (*look below*)' - job['status'] = 'failed' session.commit() - self.show_interface(event, items, title) + ftracklib.show_errors(self, event, result['errors']) + return { 'success': False, 'message': "Sync to avalon FAILED" @@ -167,7 +148,6 @@ class SyncToAvalon(BaseAction): avalon_project = result['project'] job['status'] = 'done' - self.log.info('Synchronization to Avalon was successfull!') except ValueError as ve: job['status'] = 'failed' diff --git a/pype/ftrack/events/action_sync_to_avalon.py b/pype/ftrack/events/action_sync_to_avalon.py index a3ad4d34cf..22358cd775 100644 --- a/pype/ftrack/events/action_sync_to_avalon.py +++ b/pype/ftrack/events/action_sync_to_avalon.py @@ -98,15 +98,11 @@ class Sync_To_Avalon(BaseAction): 'user': user, 'status': 'running', 'data': json.dumps({ - 'description': 'Synch Ftrack to Avalon.' + 'description': 'Sync Ftrack to Avalon.' }) }) - + session.commit() try: - self.log.info( - "Action <" + self.__class__.__name__ + "> is running" - ) - self.importable = [] # get from top entity in hierarchy all parent entities @@ -153,26 +149,11 @@ class Sync_To_Avalon(BaseAction): ) if 'errors' in result and len(result['errors']) > 0: - items = [] - for error in result['errors']: - for key, message in error.items(): - name = key.lower().replace(' ', '') - info = { - 'label': key, - 'type': 'textarea', - 'name': name, - 'value': message - } - items.append(info) - self.log.error( - '{}: {}'.format(key, message) - ) - title = 'Hey You! Few Errors were raised! (*look below*)' - job['status'] = 'failed' session.commit() - self.show_interface(event, items, title) + lib.show_errors(self, event, result['errors']) + return { 'success': False, 'message': "Sync to avalon FAILED" @@ -184,7 +165,6 @@ class Sync_To_Avalon(BaseAction): job['status'] = 'done' session.commit() - self.log.info('Synchronization to Avalon was successfull!') except ValueError as ve: job['status'] = 'failed' diff --git a/pype/ftrack/events/event_sync_to_avalon.py b/pype/ftrack/events/event_sync_to_avalon.py index 1699ea5d3c..32acae12ec 100644 --- a/pype/ftrack/events/event_sync_to_avalon.py +++ b/pype/ftrack/events/event_sync_to_avalon.py @@ -83,23 +83,9 @@ class Sync_to_Avalon(BaseEvent): custom_attributes=custom_attributes ) if 'errors' in result and len(result['errors']) > 0: - items = [] - for error in result['errors']: - for key, message in error.items(): - name = key.lower().replace(' ', '') - info = { - 'label': key, - 'type': 'textarea', - 'name': name, - 'value': message - } - items.append(info) - self.log.error( - '{}: {}'.format(key, message) - ) session.commit() - title = 'Hey You! You raised few Errors! (*look below*)' - self.show_interface(event, items, title) + lib.show_errors(self, event, result['errors']) + return if avalon_project is None: @@ -108,19 +94,18 @@ class Sync_to_Avalon(BaseEvent): except Exception as e: message = str(e) + title = 'Hey You! Unknown Error has been raised! (*look below*)' ftrack_message = ( 'SyncToAvalon event ended with unexpected error' - ' please check log file for more information.' + ' please check log file or contact Administrator' + ' for more information.' ) - items = [{ - 'label': 'Fatal Error', - 'type': 'textarea', - 'name': 'error', - 'value': ftrack_message - }] - title = 'Hey You! Unknown Error has been raised! (*look below*)' + items = [ + {'type': 'label', 'value':'# Fatal Error'}, + {'type': 'label', 'value': '

{}

'.format(ftrack_message)} + ] self.show_interface(event, items, title) - self.log.error(message) + self.log.error('Fatal error during sync: {}'.format(message)) return diff --git a/pype/ftrack/lib/avalon_sync.py b/pype/ftrack/lib/avalon_sync.py index 6c3c9a0be4..7ebd85d71d 100644 --- a/pype/ftrack/lib/avalon_sync.py +++ b/pype/ftrack/lib/avalon_sync.py @@ -541,3 +541,26 @@ def get_config_data(): log.warning("{} - {}".format(msg, str(e))) return data + +def show_errors(obj, event, errors): + title = 'Hey You! You raised few Errors! (*look below*)' + items = [] + splitter = {'type': 'label', 'value': '---'} + for error in errors: + for key, message in error.items(): + error_title = { + 'type': 'label', + 'value': '# {}'.format(key) + } + error_message = { + 'type': 'label', + 'value': '

{}

'.format(message) + } + if len(items) > 0: + items.append(splitter) + items.append(error_title) + items.append(error_message) + obj.log.error( + '{}: {}'.format(key, message) + ) + obj.show_interface(event, items, title) From 3e7f4c2cadca634ff611f1fe58d6ac2b947a7936 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 13:46:47 +0100 Subject: [PATCH 48/99] action can be ended in interface now --- pype/ftrack/lib/ftrack_base_handler.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index 8d8ec64ec5..d47f3300cf 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -260,7 +260,10 @@ class BaseHandler(object): def _interface(self, *args): interface = self.interface(*args) if interface: - if 'items' in interface: + if ( + 'items' in interface or + ('success' in interface and 'message' in interface) + ): return interface return { From 5c291324a3ec80ecae930c82af6cdf731a10f00c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 13:48:55 +0100 Subject: [PATCH 49/99] session rollback is called when event del_avalon_id failed --- pype/ftrack/events/event_del_avalon_id_from_new.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/ftrack/events/event_del_avalon_id_from_new.py b/pype/ftrack/events/event_del_avalon_id_from_new.py index c449739800..858c5a444d 100644 --- a/pype/ftrack/events/event_del_avalon_id_from_new.py +++ b/pype/ftrack/events/event_del_avalon_id_from_new.py @@ -47,6 +47,7 @@ class DelAvalonIdFromNew(BaseEvent): self.session.commit() except Exception: + self.session.rollback() continue From 11909c9aad2228a6e85ad063168b1def527729eb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 13:54:07 +0100 Subject: [PATCH 50/99] launch_log for event don't log start and finish of event launch --- pype/ftrack/lib/ftrack_event_handler.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pype/ftrack/lib/ftrack_event_handler.py b/pype/ftrack/lib/ftrack_event_handler.py index 2cbc3782b8..5479ce1d7a 100644 --- a/pype/ftrack/lib/ftrack_event_handler.py +++ b/pype/ftrack/lib/ftrack_event_handler.py @@ -1,3 +1,4 @@ +import functools from .ftrack_base_handler import BaseHandler @@ -18,6 +19,16 @@ class BaseEvent(BaseHandler): '''Expects a ftrack_api.Session instance''' super().__init__(session) + # Decorator + def launch_log(self, func): + @functools.wraps(func) + def wrapper_launch(*args, **kwargs): + try: + func(*args, **kwargs) + except Exception: + self.log.info('{} Failed'.format(self.__class__.__name__)) + return wrapper_launch + def register(self): '''Registers the event, subscribing the discover and launch topics.''' self.session.event_hub.subscribe( From a1099dcb5142977aafe1b5246c6b1905d95ce360 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 13:55:51 +0100 Subject: [PATCH 51/99] session rollback and cache.clear are called before launch --- pype/ftrack/lib/ftrack_event_handler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/ftrack/lib/ftrack_event_handler.py b/pype/ftrack/lib/ftrack_event_handler.py index 5479ce1d7a..6ac2aa18be 100644 --- a/pype/ftrack/lib/ftrack_event_handler.py +++ b/pype/ftrack/lib/ftrack_event_handler.py @@ -38,6 +38,8 @@ class BaseEvent(BaseHandler): ) def _launch(self, event): + self.session._local_cache.clear() + self.session.rollback() args = self._translate_event( self.session, event ) From c624072030829a8ed9fb9f15b81c2d20852674bd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 13:56:13 +0100 Subject: [PATCH 52/99] job killer has better interface now --- pype/ftrack/actions/action_job_killer.py | 29 +++++++++++++++++------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/pype/ftrack/actions/action_job_killer.py b/pype/ftrack/actions/action_job_killer.py index d8d0e81cb1..440fdc1654 100644 --- a/pype/ftrack/actions/action_job_killer.py +++ b/pype/ftrack/actions/action_job_killer.py @@ -16,7 +16,7 @@ class JobKiller(BaseAction): #: Action label. label = 'Job Killer' #: Action description. - description = 'Killing all running jobs younger than day' + description = 'Killing selected running jobs' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] icon = ( @@ -36,29 +36,42 @@ class JobKiller(BaseAction): jobs = session.query( 'select id, status from Job' ' where status in ("queued", "running")' - ) + ).all() items = [] import json + item_splitter = {'type': 'label', 'value': '---'} for job in jobs: data = json.loads(job['data']) user = job['user']['username'] created = job['created_at'].strftime('%d.%m.%Y %H:%M:%S') - label = '{}/ {}/ {}'.format( + label = '{} - {} - {}'.format( data['description'], created, user ) + item_label = { + 'type': 'label', + 'value': label + } item = { - 'label': label, 'name': job['id'], 'type': 'boolean', 'value': False } + if len(items) > 0: + items.append(item_splitter) + items.append(item_label) items.append(item) - return { - 'items': items, - 'title': title - } + if len(items) == 0: + return { + 'success': False, + 'message': 'Didn\'t found any running jobs' + } + else: + return { + 'items': items, + 'title': title + } def launch(self, session, entities, event): """ GET JOB """ From e287bb5ffcf9a132eff28d84d48d55d5d270627a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 13:56:41 +0100 Subject: [PATCH 53/99] event radio buttons is ignored by default --- pype/ftrack/events/event_radio_buttons.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/ftrack/events/event_radio_buttons.py b/pype/ftrack/events/event_radio_buttons.py index 7af720d95d..fb091f304d 100644 --- a/pype/ftrack/events/event_radio_buttons.py +++ b/pype/ftrack/events/event_radio_buttons.py @@ -2,6 +2,9 @@ import ftrack_api from pype.ftrack import BaseEvent +ignore_me = True + + class Radio_buttons(BaseEvent): def launch(self, session, entities, event): From bfd13bffa19faf4fc6db81a7379386a53081a114 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 14:49:02 +0100 Subject: [PATCH 54/99] ftrack action registration is faster --- pype/ftrack/actions/action_application_loader.py | 5 ++++- pype/ftrack/ftrack_server/ftrack_server.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pype/ftrack/actions/action_application_loader.py b/pype/ftrack/actions/action_application_loader.py index 3202c19d40..2db03d0f8a 100644 --- a/pype/ftrack/actions/action_application_loader.py +++ b/pype/ftrack/actions/action_application_loader.py @@ -55,9 +55,12 @@ def register(session): apps.append(app) apps = sorted(apps, key=lambda x: x['name']) + app_counter = 0 for app in apps: try: registerApp(app, session) - time.sleep(0.05) + if app_counter%5 == 0: + time.sleep(0.1) + app_counter += 1 except Exception as e: log.warning("'{0}' - not proper App ({1})".format(app['name'], e)) diff --git a/pype/ftrack/ftrack_server/ftrack_server.py b/pype/ftrack/ftrack_server/ftrack_server.py index 91caff216e..6c63dcf414 100644 --- a/pype/ftrack/ftrack_server/ftrack_server.py +++ b/pype/ftrack/ftrack_server/ftrack_server.py @@ -118,15 +118,18 @@ class FtrackServer(): if len(functions) < 1: raise Exception + function_counter = 0 for function in functions: try: function['register'](self.session) + if function_counter%7 == 0: + time.sleep(0.1) + function_counter += 1 except Exception as e: msg = '"{}" - register was not successful ({})'.format( function['name'], str(e) ) log.warning(msg) - time.sleep(0.05) def run_server(self): self.session = ftrack_api.Session(auto_connect_event_hub=True,) From ac1f7e72c1ef81bd0016194f1a0b29e6ef98250e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 14:58:01 +0100 Subject: [PATCH 55/99] added socialnotification to ignore list when translating selection in events --- pype/ftrack/lib/ftrack_event_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/ftrack/lib/ftrack_event_handler.py b/pype/ftrack/lib/ftrack_event_handler.py index 6ac2aa18be..a2c4184736 100644 --- a/pype/ftrack/lib/ftrack_event_handler.py +++ b/pype/ftrack/lib/ftrack_event_handler.py @@ -56,7 +56,7 @@ class BaseEvent(BaseHandler): _entities = list() for entity in _selection: - if entity['entityType'] in ['socialfeed']: + if entity['entityType'] in ['socialfeed', 'socialnotification']: continue _entities.append( ( From fb73402251e42b6197cd436e155d471cc8aa28a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 16:27:12 +0100 Subject: [PATCH 56/99] get_entities was separated from translate_event --- pype/ftrack/lib/ftrack_event_handler.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pype/ftrack/lib/ftrack_event_handler.py b/pype/ftrack/lib/ftrack_event_handler.py index a2c4184736..f1829993f6 100644 --- a/pype/ftrack/lib/ftrack_event_handler.py +++ b/pype/ftrack/lib/ftrack_event_handler.py @@ -52,11 +52,20 @@ class BaseEvent(BaseHandler): def _translate_event(self, session, event): '''Return *event* translated structure to be used with the API.''' - _selection = event['data'].get('entities', []) + return [ + self._get_entities(session, event), + event + ] + def _get_entities( + self, session, event, ignore=['socialfeed', 'socialnotification'] + ): + _selection = event['data'].get('entities', []) _entities = list() + if isinstance(ignore, str): + ignore = list(ignore) for entity in _selection: - if entity['entityType'] in ['socialfeed', 'socialnotification']: + if entity['entityType'] in ignore: continue _entities.append( ( @@ -66,8 +75,4 @@ class BaseEvent(BaseHandler): ) ) ) - - return [ - _entities, - event - ] + return _entities From af477178f584dcc541353fcf6ca34550f8495b86 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 16:28:12 +0100 Subject: [PATCH 57/99] event's exceptions are logged out on crash --- pype/ftrack/lib/ftrack_event_handler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pype/ftrack/lib/ftrack_event_handler.py b/pype/ftrack/lib/ftrack_event_handler.py index f1829993f6..62a6d2b490 100644 --- a/pype/ftrack/lib/ftrack_event_handler.py +++ b/pype/ftrack/lib/ftrack_event_handler.py @@ -25,8 +25,10 @@ class BaseEvent(BaseHandler): def wrapper_launch(*args, **kwargs): try: func(*args, **kwargs) - except Exception: - self.log.info('{} Failed'.format(self.__class__.__name__)) + except Exception as e: + self.log.info('{} Failed ({})'.format( + self.__class__.__name__, str(e)) + ) return wrapper_launch def register(self): From b894496f9aa257d7029499183080ac678dc5b57e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 16:28:48 +0100 Subject: [PATCH 58/99] version_to_task fixed, won't crash if status is not found in ftrack --- pype/ftrack/events/event_version_to_task_statuses.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pype/ftrack/events/event_version_to_task_statuses.py b/pype/ftrack/events/event_version_to_task_statuses.py index c5c1d9b664..5baf1396ba 100644 --- a/pype/ftrack/events/event_version_to_task_statuses.py +++ b/pype/ftrack/events/event_version_to_task_statuses.py @@ -45,10 +45,9 @@ class VersionToTaskStatus(BaseEvent): task_status = session.query(query).one() except Exception: self.log.info( - 'During update {}: Status {} was not found'.format( - entity['name'], status_to_set - ) - ) + '!!! status was not found in Ftrack [ {} ]'.format( + status_to_set + )) continue # Proceed if the task status was set From c2eb4599e8febf7f3b0bd46892d78bb9e0cf0a9b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 16:31:51 +0100 Subject: [PATCH 59/99] _translate_event is not launched by default so events are faster --- .../events/event_del_avalon_id_from_new.py | 17 ++------ pype/ftrack/events/event_next_task_update.py | 2 +- pype/ftrack/events/event_radio_buttons.py | 2 +- pype/ftrack/events/event_sync_to_avalon.py | 42 ++++--------------- pype/ftrack/events/event_test.py | 2 +- pype/ftrack/events/event_thumbnail_updates.py | 2 +- .../events/event_version_to_task_statuses.py | 2 +- pype/ftrack/lib/ftrack_event_handler.py | 7 +--- 8 files changed, 18 insertions(+), 58 deletions(-) diff --git a/pype/ftrack/events/event_del_avalon_id_from_new.py b/pype/ftrack/events/event_del_avalon_id_from_new.py index 858c5a444d..d47494d6eb 100644 --- a/pype/ftrack/events/event_del_avalon_id_from_new.py +++ b/pype/ftrack/events/event_del_avalon_id_from_new.py @@ -13,7 +13,7 @@ class DelAvalonIdFromNew(BaseEvent): ''' priority = Sync_to_Avalon.priority - 1 - def launch(self, event): + def launch(self, session, event): created = [] entities = event['data']['entities'] for entity in entities: @@ -31,7 +31,7 @@ class DelAvalonIdFromNew(BaseEvent): get_ca_mongoid() in entity['keys'] and entity_id in created ): - ftrack_entity = self.session.get( + ftrack_entity = session.get( self._get_entity_type(entity), entity_id ) @@ -44,22 +44,13 @@ class DelAvalonIdFromNew(BaseEvent): ftrack_entity['custom_attributes'][ get_ca_mongoid() ] = '' - self.session.commit() + session.commit() except Exception: - self.session.rollback() + session.rollback() continue - def register(self): - '''Registers the event, subscribing the discover and launch topics.''' - self.session.event_hub.subscribe( - 'topic=ftrack.update', - self.launch, - priority=self.priority - ) - - def register(session, **kw): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): diff --git a/pype/ftrack/events/event_next_task_update.py b/pype/ftrack/events/event_next_task_update.py index b6c82b930c..cd26325414 100644 --- a/pype/ftrack/events/event_next_task_update.py +++ b/pype/ftrack/events/event_next_task_update.py @@ -34,7 +34,7 @@ class NextTaskUpdate(BaseEvent): return None - def launch(self, session, entities, event): + def launch(self, session, event): '''Propagates status from version to task when changed''' # self.log.info(event) diff --git a/pype/ftrack/events/event_radio_buttons.py b/pype/ftrack/events/event_radio_buttons.py index fb091f304d..f96d90307d 100644 --- a/pype/ftrack/events/event_radio_buttons.py +++ b/pype/ftrack/events/event_radio_buttons.py @@ -7,7 +7,7 @@ ignore_me = True class Radio_buttons(BaseEvent): - def launch(self, session, entities, event): + def launch(self, session, event): '''Provides a readio button behaviour to any bolean attribute in radio_button group.''' diff --git a/pype/ftrack/events/event_sync_to_avalon.py b/pype/ftrack/events/event_sync_to_avalon.py index 32acae12ec..1deaa3d17e 100644 --- a/pype/ftrack/events/event_sync_to_avalon.py +++ b/pype/ftrack/events/event_sync_to_avalon.py @@ -4,7 +4,12 @@ from pype.ftrack import BaseEvent, lib class Sync_to_Avalon(BaseEvent): - def launch(self, session, entities, event): + ignore_entityType = [ + 'assetversion', 'job', 'user', 'reviewsessionobject', 'timer', + 'socialfeed', 'socialnotification', 'timelog' + ] + + def launch(self, session, event): ca_mongoid = lib.get_ca_mongoid() # If mongo_id textfield has changed: RETURN! # - infinite loop @@ -13,6 +18,7 @@ class Sync_to_Avalon(BaseEvent): if ca_mongoid in ent['keys']: return + entities = self._get_entities(session, event, self.ignore_entityType) ft_project = None # get project for entity in entities: @@ -109,40 +115,6 @@ class Sync_to_Avalon(BaseEvent): return - def _launch(self, event): - self.session.reset() - - args = self._translate_event( - self.session, event - ) - - self.launch( - self.session, *args - ) - return - - def _translate_event(self, session, event): - exceptions = [ - 'assetversion', 'job', 'user', 'reviewsessionobject', 'timer', - 'socialfeed', 'timelog' - ] - _selection = event['data'].get('entities', []) - - _entities = list() - for entity in _selection: - if entity['entityType'] in exceptions: - continue - _entities.append( - ( - session.get( - self._get_entity_type(entity), - entity.get('entityId') - ) - ) - ) - - return [_entities, event] - def register(session, **kw): '''Register plugin. Called when used as an plugin.''' diff --git a/pype/ftrack/events/event_test.py b/pype/ftrack/events/event_test.py index ecefc628f3..46e16cbb95 100644 --- a/pype/ftrack/events/event_test.py +++ b/pype/ftrack/events/event_test.py @@ -13,7 +13,7 @@ class Test_Event(BaseEvent): priority = 10000 - def launch(self, session, entities, event): + def launch(self, session, event): '''just a testing event''' diff --git a/pype/ftrack/events/event_thumbnail_updates.py b/pype/ftrack/events/event_thumbnail_updates.py index 62a194d167..a825088e60 100644 --- a/pype/ftrack/events/event_thumbnail_updates.py +++ b/pype/ftrack/events/event_thumbnail_updates.py @@ -4,7 +4,7 @@ from pype.ftrack import BaseEvent class ThumbnailEvents(BaseEvent): - def launch(self, session, entities, event): + def launch(self, session, event): '''just a testing event''' # self.log.info(event) diff --git a/pype/ftrack/events/event_version_to_task_statuses.py b/pype/ftrack/events/event_version_to_task_statuses.py index 5baf1396ba..dac34b8009 100644 --- a/pype/ftrack/events/event_version_to_task_statuses.py +++ b/pype/ftrack/events/event_version_to_task_statuses.py @@ -4,7 +4,7 @@ from pype.ftrack import BaseEvent class VersionToTaskStatus(BaseEvent): - def launch(self, session, entities, event): + def launch(self, session, event): '''Propagates status from version to task when changed''' session.commit() diff --git a/pype/ftrack/lib/ftrack_event_handler.py b/pype/ftrack/lib/ftrack_event_handler.py index 62a6d2b490..c6c91e7428 100644 --- a/pype/ftrack/lib/ftrack_event_handler.py +++ b/pype/ftrack/lib/ftrack_event_handler.py @@ -40,14 +40,11 @@ class BaseEvent(BaseHandler): ) def _launch(self, event): - self.session._local_cache.clear() self.session.rollback() - args = self._translate_event( - self.session, event - ) + self.session._local_cache.clear() self.launch( - self.session, *args + self.session, event ) return From b653200a8b579f0643d5b34a82f4a5c97a91ee19 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 16:32:26 +0100 Subject: [PATCH 60/99] del_avalon_id_from_new fixed missing key error --- pype/ftrack/events/event_del_avalon_id_from_new.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/ftrack/events/event_del_avalon_id_from_new.py b/pype/ftrack/events/event_del_avalon_id_from_new.py index d47494d6eb..7659191637 100644 --- a/pype/ftrack/events/event_del_avalon_id_from_new.py +++ b/pype/ftrack/events/event_del_avalon_id_from_new.py @@ -20,14 +20,14 @@ class DelAvalonIdFromNew(BaseEvent): try: entity_id = entity['entityId'] - if entity['action'] == 'add': + if entity.get('action', None) == 'add': id_dict = entity['changes']['id'] if id_dict['new'] is not None and id_dict['old'] is None: created.append(id_dict['new']) elif ( - entity['action'] == 'update' and + entity.get('action', None) == 'update' and get_ca_mongoid() in entity['keys'] and entity_id in created ): From a613476f53c4bb2dc9e4bd3cb13c33e9da641813 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 13 Mar 2019 16:33:01 +0100 Subject: [PATCH 61/99] event_next_task_update is not launched on creating/deleting entity --- pype/ftrack/events/event_next_task_update.py | 62 +++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/pype/ftrack/events/event_next_task_update.py b/pype/ftrack/events/event_next_task_update.py index cd26325414..9e4148e30a 100644 --- a/pype/ftrack/events/event_next_task_update.py +++ b/pype/ftrack/events/event_next_task_update.py @@ -42,41 +42,47 @@ class NextTaskUpdate(BaseEvent): for entity in event['data'].get('entities', []): - if (entity['entityType'] == 'task' and - 'statusid' in entity['keys']): + statusid_changes = entity.get('changes', {}).get('statusid', {}) + if ( + entity['entityType'] != 'task' or + 'statusid' not in entity['keys'] or + statusid_changes.get('new', None) is None or + statusid_changes.get('old', None) is None + ): + continue - task = session.get('Task', entity['entityId']) + task = session.get('Task', entity['entityId']) - status = session.get('Status', - entity['changes']['statusid']['new']) - state = status['state']['name'] + status = session.get('Status', + entity['changes']['statusid']['new']) + state = status['state']['name'] - next_task = self.get_next_task(task, session) + next_task = self.get_next_task(task, session) - # Setting next task to Ready, if on NOT READY - if next_task and state == 'Done': - if next_task['status']['name'].lower() == 'not ready': + # Setting next task to Ready, if on NOT READY + if next_task and state == 'Done': + if next_task['status']['name'].lower() == 'not ready': - # Get path to task - path = task['name'] - for p in task['ancestors']: - path = p['name'] + '/' + path + # Get path to task + path = task['name'] + for p in task['ancestors']: + path = p['name'] + '/' + path - # Setting next task status - try: - query = 'Status where name is "{}"'.format('Ready') - status_to_set = session.query(query).one() - next_task['status'] = status_to_set - except Exception as e: - self.log.warning(( - '!!! [ {} ] status couldnt be set: [ {} ]' - ).format(path, e)) - else: - self.log.info(( - '>>> [ {} ] updated to [ Ready ]' - ).format(path)) + # Setting next task status + try: + query = 'Status where name is "{}"'.format('Ready') + status_to_set = session.query(query).one() + next_task['status'] = status_to_set + except Exception as e: + self.log.warning(( + '!!! [ {} ] status couldnt be set: [ {} ]' + ).format(path, e)) + else: + self.log.info(( + '>>> [ {} ] updated to [ Ready ]' + ).format(path)) - session.commit() + session.commit() def register(session, **kw): '''Register plugin. Called when used as an plugin.''' From 9570ba5bc2a6d43ba94aa8c7ade9cc38657a905a Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 14 Mar 2019 16:19:19 +0100 Subject: [PATCH 62/99] add solidangle license to deadline job --- pype/plugins/maya/publish/submit_maya_deadline.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 56e4b1ea32..0a97a9b98f 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -238,6 +238,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): # todo: This is a temporary fix for yeti variables "PEREGRINEL_LICENSE", + "SOLIDANGLE_LICENSE", "ARNOLD_LICENSE" "MAYA_MODULE_PATH", "TOOL_ENV" From a76fabbde5334057a788805ba0a6d59129b0b167 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Mar 2019 17:57:16 +0100 Subject: [PATCH 63/99] fixed next task update so it ignore entities without changes --- pype/ftrack/events/event_next_task_update.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pype/ftrack/events/event_next_task_update.py b/pype/ftrack/events/event_next_task_update.py index 9e4148e30a..15ef10469b 100644 --- a/pype/ftrack/events/event_next_task_update.py +++ b/pype/ftrack/events/event_next_task_update.py @@ -41,8 +41,10 @@ class NextTaskUpdate(BaseEvent): # start of event procedure ---------------------------------- for entity in event['data'].get('entities', []): - - statusid_changes = entity.get('changes', {}).get('statusid', {}) + changes = entity.get('changes', None) + if changes is None: + continue + statusid_changes = changes.get('statusid', {}) if ( entity['entityType'] != 'task' or 'statusid' not in entity['keys'] or From f23b0f0b8adad3fff7261e69aed87038701ee9e8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Mar 2019 18:15:22 +0100 Subject: [PATCH 64/99] better result handling on ftrack ations --- pype/ftrack/lib/ftrack_action_handler.py | 20 +++++++++++++------- pype/ftrack/lib/ftrack_base_handler.py | 20 +++++++++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/pype/ftrack/lib/ftrack_action_handler.py b/pype/ftrack/lib/ftrack_action_handler.py index 2249611a4b..c6d6181c1f 100644 --- a/pype/ftrack/lib/ftrack_action_handler.py +++ b/pype/ftrack/lib/ftrack_action_handler.py @@ -84,14 +84,20 @@ class BaseAction(BaseHandler): def _handle_result(self, session, result, entities, event): '''Validate the returned result from the action callback''' if isinstance(result, bool): - result = { - 'success': result, - 'message': ( - '{0} launched successfully.'.format( - self.label + if result is True: + result = { + 'success': result, + 'message': ( + '{0} launched successfully.'.format(self.label) ) - ) - } + } + else: + result = { + 'success': result, + 'message': ( + '{0} launch failed.'.format(self.label) + ) + } elif isinstance(result, dict): if 'items' in result: diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index d47f3300cf..3bf6be0a0e 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -288,14 +288,20 @@ class BaseHandler(object): def _handle_result(self, session, result, entities, event): '''Validate the returned result from the action callback''' if isinstance(result, bool): - result = { - 'success': result, - 'message': ( - '{0} launched successfully.'.format( - self.label + if result is True: + result = { + 'success': result, + 'message': ( + '{0} launched successfully.'.format(self.label) ) - ) - } + } + else: + result = { + 'success': result, + 'message': ( + '{0} launch failed.'.format(self.label) + ) + } elif isinstance(result, dict): for key in ('success', 'message'): From 91e929033d106a3938c1b9bfdf3627cd49408bf0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Mar 2019 18:26:55 +0100 Subject: [PATCH 65/99] fixed get_tasks bug --- pype/clockify/clockify_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index efbd79dac2..d6897ec299 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -162,7 +162,7 @@ class ClockifyAPI(metaclass=Singleton): def get_tasks(self, project_id, workspace_id=None): if workspace_id is None: - workspace_id = self.add_workspace_id + workspace_id = self.workspace_id action_url = 'workspaces/{}/projects/{}/tasks/'.format( workspace_id, project_id ) From 492f89056ac596be29ac2cc6193e515b105b429f Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 14 Mar 2019 18:30:30 +0100 Subject: [PATCH 66/99] add matetx option to look collector --- pype/plugins/maya/publish/collect_look.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/plugins/maya/publish/collect_look.py b/pype/plugins/maya/publish/collect_look.py index cb15976772..2a2370a133 100644 --- a/pype/plugins/maya/publish/collect_look.py +++ b/pype/plugins/maya/publish/collect_look.py @@ -218,6 +218,7 @@ class CollectLook(pyblish.api.InstancePlugin): # make ftrack publishable instance.data["families"] = ['ftrack'] + instance.data['maketx'] = True def collect(self, instance): From bc8efd319d7b12542235a3aebc4b5bea154c349f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Mar 2019 18:31:38 +0100 Subject: [PATCH 67/99] only actual project is synced --- pype/ftrack/actions/action_clockify_sync.py | 119 ++++++-------------- 1 file changed, 35 insertions(+), 84 deletions(-) diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py index de91e68f50..406d383b6d 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -37,53 +37,7 @@ class SyncClocify(BaseAction): ''' Validation ''' return True - def interface(self, session, entities, event): - if not event['data'].get('values', {}): - title = 'Select projects to sync' - - projects = session.query('Project').all() - - items = [] - all_projects_label = { - 'type': 'label', - 'value': 'All projects' - } - all_projects_value = { - 'name': '__all__', - 'type': 'boolean', - 'value': False - } - line = { - 'type': 'label', - 'value': '___' - } - items.append(all_projects_label) - items.append(all_projects_value) - items.append(line) - for project in projects: - label = project['full_name'] - item_label = { - 'type': 'label', - 'value': label - } - item_value = { - 'name': project['id'], - 'type': 'boolean', - 'value': False - } - items.append(item_label) - items.append(item_value) - - return { - 'items': items, - 'title': title - } - def launch(self, session, entities, event): - values = event['data'].get('values', {}) - if not values: - return - # JOB SETTINGS userId = event['source']['user']['id'] user = session.query('User where id is ' + userId).one() @@ -97,50 +51,47 @@ class SyncClocify(BaseAction): }) session.commit() try: - if values.get('__all__', False) is True: - projects_to_sync = session.query('Project').all() - else: - projects_to_sync = [] - project_query = 'Project where id is "{}"' - for project_id, sync in values.items(): - if sync is True: - projects_to_sync.append(session.query( - project_query.format(project_id) - ).one()) + entity = entities[0] - projects_info = {} - for project in projects_to_sync: - task_types = [] - for task_type in project['project_schema']['_task_type_schema'][ - 'types' - ]: - task_types.append(task_type['name']) - projects_info[project['full_name']] = task_types + 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() - for project_name, task_types in projects_info.items(): - if project_name not in clockify_projects: - response = self.clockapi.add_project(project_name) + + 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 + )) + return { + 'success': False, + 'message': 'Can\'t create project, unexpected error' + } + project_id = response['id'] + else: + project_id = clockify_projects[project_name] + + clockify_project_tasks = self.clockapi.get_tasks(project_id) + for task_type in task_types: + if task_type not in clockify_project_tasks: + response = self.clockapi.add_task( + task_type, project_id + ) if 'id' not in response: - self.log.error('Project {} can\'t be created'.format( - project_name + self.log.error('Task {} can\'t be created'.format( + task_type )) continue - project_id = response['id'] - else: - project_id = clockify_projects[project_name] - - clockify_project_tasks = self.clockapi.get_tasks(project_id) - for task_type in task_types: - if task_type not in clockify_project_tasks: - response = self.clockapi.add_task( - task_type, project_id - ) - if 'id' not in response: - self.log.error('Task {} can\'t be created'.format( - task_type - )) - continue except Exception: job['status'] = 'failed' session.commit() From cb1eae4e30fcfbe60fde31b2a2f16457de1caf77 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Mar 2019 18:39:10 +0100 Subject: [PATCH 68/99] file proxy is not set in djv --- pype/ftrack/actions/action_djvview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index 9c9c593855..80f1105a96 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -198,7 +198,7 @@ class DJVViewAction(BaseHandler): '''layer name''' # cmd.append('-file_layer (value)') ''' Proxy scale: 1/2, 1/4, 1/8''' - cmd.append('-file_proxy 1/2') + # cmd.append('-file_proxy 1/2') ''' Cache: True, False.''' cmd.append('-file_cache True') ''' Start in full screen ''' From 4865ced27b9719b0bc9021ec3f91cffe634b34ec Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Mar 2019 18:39:59 +0100 Subject: [PATCH 69/99] file proxy is not set in djv --- pype/ftrack/actions/action_djvview.py | 2 +- pype/plugins/global/load/open_djv.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index 9c9c593855..80f1105a96 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -198,7 +198,7 @@ class DJVViewAction(BaseHandler): '''layer name''' # cmd.append('-file_layer (value)') ''' Proxy scale: 1/2, 1/4, 1/8''' - cmd.append('-file_proxy 1/2') + # cmd.append('-file_proxy 1/2') ''' Cache: True, False.''' cmd.append('-file_cache True') ''' Start in full screen ''' diff --git a/pype/plugins/global/load/open_djv.py b/pype/plugins/global/load/open_djv.py index 29f8e8ba08..bd49d86d5f 100644 --- a/pype/plugins/global/load/open_djv.py +++ b/pype/plugins/global/load/open_djv.py @@ -81,7 +81,7 @@ class OpenInDJV(api.Loader): '''layer name''' # cmd.append('-file_layer (value)') ''' Proxy scale: 1/2, 1/4, 1/8''' - cmd.append('-file_proxy 1/2') + # cmd.append('-file_proxy 1/2') ''' Cache: True, False.''' cmd.append('-file_cache True') ''' Start in full screen ''' From 788057fc71d9b2c512691df888829213c64d3200 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 14 Mar 2019 19:32:38 +0100 Subject: [PATCH 70/99] prevent errors when None entity is found and add rollback to next task update --- pype/ftrack/events/event_next_task_update.py | 10 +++++----- pype/ftrack/events/event_version_to_task_statuses.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pype/ftrack/events/event_next_task_update.py b/pype/ftrack/events/event_next_task_update.py index 15ef10469b..e677e53fb2 100644 --- a/pype/ftrack/events/event_next_task_update.py +++ b/pype/ftrack/events/event_next_task_update.py @@ -75,16 +75,16 @@ class NextTaskUpdate(BaseEvent): query = 'Status where name is "{}"'.format('Ready') status_to_set = session.query(query).one() next_task['status'] = status_to_set + session.commit() + self.log.info(( + '>>> [ {} ] updated to [ Ready ]' + ).format(path)) except Exception as e: self.log.warning(( '!!! [ {} ] status couldnt be set: [ {} ]' ).format(path, e)) - else: - self.log.info(( - '>>> [ {} ] updated to [ Ready ]' - ).format(path)) + session.rollback() - session.commit() def register(session, **kw): '''Register plugin. Called when used as an plugin.''' diff --git a/pype/ftrack/events/event_version_to_task_statuses.py b/pype/ftrack/events/event_version_to_task_statuses.py index dac34b8009..d1393e622e 100644 --- a/pype/ftrack/events/event_version_to_task_statuses.py +++ b/pype/ftrack/events/event_version_to_task_statuses.py @@ -13,7 +13,7 @@ class VersionToTaskStatus(BaseEvent): # Filter non-assetversions if ( entity['entityType'] == 'assetversion' and - 'statusid' in entity['keys'] + 'statusid' in entity.get('keys', []) ): version = session.get('AssetVersion', entity['entityId']) From e0f6ba6eb2accfd874f6514fe409c73fd700a5c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Mar 2019 09:41:04 +0100 Subject: [PATCH 71/99] task name is added to timer description --- pype/ftrack/actions/action_clockify_start.py | 1 + pype/ftrack/lib/ftrack_app_handler.py | 1 + pype/plugins/launcher/actions/ClockifyStart.py | 1 + 3 files changed, 3 insertions(+) diff --git a/pype/ftrack/actions/action_clockify_start.py b/pype/ftrack/actions/action_clockify_start.py index d36ab795a9..76becb8a1b 100644 --- a/pype/ftrack/actions/action_clockify_start.py +++ b/pype/ftrack/actions/action_clockify_start.py @@ -45,6 +45,7 @@ class StartClockify(BaseAction): return output desc_items = get_parents(task['parent']) + desc_items.append(task['name']) description = '/'.join(desc_items) project_id = self.clockapi.get_project_id(project_name) task_id = self.clockapi.get_task_id(task_name, project_id) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 01eca55b5b..2553cb2976 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -327,6 +327,7 @@ class AppAction(BaseHandler): return output desc_items = get_parents(task['parent']) + desc_items.append(task['name']) description = '/'.join(desc_items) project_id = clockapi.get_project_id(project_name) diff --git a/pype/plugins/launcher/actions/ClockifyStart.py b/pype/plugins/launcher/actions/ClockifyStart.py index c300df3389..ca6d6e1852 100644 --- a/pype/plugins/launcher/actions/ClockifyStart.py +++ b/pype/plugins/launcher/actions/ClockifyStart.py @@ -31,6 +31,7 @@ class ClockifyStart(api.Action): if asset is not None: desc_items = asset.get('data', {}).get('parents', []) desc_items.append(asset_name) + desc_items.append(task_name) description = '/'.join(desc_items) project_id = self.clockapi.get_project_id(project_name) From bc80648db708d33ccfe11a59ed49f1de3b6b1216 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Mar 2019 09:51:00 +0100 Subject: [PATCH 72/99] get_tags fixed workspace_id, start timer can have tags, added add_tag method --- pype/clockify/clockify_api.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index d6897ec299..f5ebac0cef 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -147,7 +147,7 @@ class ClockifyAPI(metaclass=Singleton): project["name"]: project["id"] for project in response.json() } - def get_tags(self, workspace_id): + def get_tags(self, workspace_id=None): if workspace_id is None: workspace_id = self.workspace_id action_url = 'workspaces/{}/tags/'.format(workspace_id) @@ -213,7 +213,7 @@ class ClockifyAPI(metaclass=Singleton): return str(datetime.datetime.utcnow().isoformat())+'Z' def start_time_entry( - self, description, project_id, task_id, + self, description, project_id, task_id=None, tag_ids=[], workspace_id=None, billable=True ): # Workspace @@ -246,7 +246,7 @@ class ClockifyAPI(metaclass=Singleton): "description": description, "projectId": project_id, "taskId": task_id, - "tagIds": None + "tagIds": tag_ids } response = requests.post( self.endpoint + action_url, @@ -374,6 +374,20 @@ class ClockifyAPI(metaclass=Singleton): ) return response.json() + def add_tag(self, name, workspace_id=None): + if workspace_id is None: + workspace_id = self.workspace_id + action_url = 'workspaces/{}/tags'.format(workspace_id) + body = { + "name": name + } + response = requests.post( + self.endpoint + action_url, + headers=self.headers, + json=body + ) + return response.json() + def delete_project( self, project_id, workspace_id=None ): From ef91b6e53deb8e59c9e48ad768ebac5d11a73195 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Mar 2019 09:51:36 +0100 Subject: [PATCH 73/99] task types are stored as tags instead of tasks during sync to clockify --- pype/ftrack/actions/action_clockify_sync.py | 8 +++----- pype/plugins/launcher/actions/ClockifySync.py | 10 +++------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/ftrack/actions/action_clockify_sync.py index 406d383b6d..202bb7b912 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/ftrack/actions/action_clockify_sync.py @@ -81,12 +81,10 @@ class SyncClocify(BaseAction): else: project_id = clockify_projects[project_name] - clockify_project_tasks = self.clockapi.get_tasks(project_id) + clockify_workspace_tags = self.clockapi.get_tags() for task_type in task_types: - if task_type not in clockify_project_tasks: - response = self.clockapi.add_task( - task_type, project_id - ) + 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 diff --git a/pype/plugins/launcher/actions/ClockifySync.py b/pype/plugins/launcher/actions/ClockifySync.py index c7459542aa..d8c69bc768 100644 --- a/pype/plugins/launcher/actions/ClockifySync.py +++ b/pype/plugins/launcher/actions/ClockifySync.py @@ -46,14 +46,10 @@ class ClockifySync(api.Action): else: project_id = clockify_projects[project_name] - clockify_project_tasks = self.clockapi.get_tasks( - project_id - ) + clockify_workspace_tags = self.clockapi.get_tags() for task_type in task_types: - if task_type not in clockify_project_tasks: - response = self.clockapi.add_task( - task_type, project_id - ) + 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 From 7d3a0fe82b0a34ee0751ec05d5af0228b598e924 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Mar 2019 09:52:07 +0100 Subject: [PATCH 74/99] start timer set tag_ids instead of task_id --- pype/ftrack/actions/action_clockify_start.py | 5 +++-- pype/ftrack/lib/ftrack_app_handler.py | 9 +++++---- pype/plugins/launcher/actions/ClockifyStart.py | 5 +++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pype/ftrack/actions/action_clockify_start.py b/pype/ftrack/actions/action_clockify_start.py index 76becb8a1b..b1c60a2525 100644 --- a/pype/ftrack/actions/action_clockify_start.py +++ b/pype/ftrack/actions/action_clockify_start.py @@ -48,9 +48,10 @@ class StartClockify(BaseAction): desc_items.append(task['name']) description = '/'.join(desc_items) project_id = self.clockapi.get_project_id(project_name) - task_id = self.clockapi.get_task_id(task_name, project_id) + tag_ids = [] + tag_ids.append(self.clockapi.get_tag_id(task_name)) self.clockapi.start_time_entry( - description, project_id, task_id + description, project_id, tag_ids=tag_ids ) return True diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 2553cb2976..48531cf014 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -314,7 +314,7 @@ class AppAction(BaseHandler): # RUN TIMER IN Clockify if clockify_timer is not None: - task_name = task['type']['name'] + task_type = task['type']['name'] project_name = task['project']['full_name'] def get_parents(entity): @@ -331,9 +331,10 @@ class AppAction(BaseHandler): description = '/'.join(desc_items) project_id = clockapi.get_project_id(project_name) - task_id = clockapi.get_task_id(task_name, project_id) - clockapi.start_time_entry( - description, project_id, task_id, + tag_ids = [] + tag_ids.append(self.clockapi.get_tag_id(task_type)) + self.clockapi.start_time_entry( + description, project_id, tag_ids=tag_ids ) # Change status of task to In progress diff --git a/pype/plugins/launcher/actions/ClockifyStart.py b/pype/plugins/launcher/actions/ClockifyStart.py index ca6d6e1852..d0d1bb48f3 100644 --- a/pype/plugins/launcher/actions/ClockifyStart.py +++ b/pype/plugins/launcher/actions/ClockifyStart.py @@ -35,7 +35,8 @@ class ClockifyStart(api.Action): description = '/'.join(desc_items) project_id = self.clockapi.get_project_id(project_name) - task_id = self.clockapi.get_task_id(task_name, project_id) + tag_ids = [] + tag_ids.append(self.clockapi.get_tag_id(task_name)) self.clockapi.start_time_entry( - description, project_id, task_id + description, project_id, tag_ids=tag_ids ) From e22e8102e33cf4c33db5993e0dd3ec8f97871f3e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Mar 2019 17:36:05 +0100 Subject: [PATCH 75/99] djv converted from scratch to action --- pype/ftrack/actions/action_djvview.py | 453 ++++++++++---------------- 1 file changed, 172 insertions(+), 281 deletions(-) diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index 80f1105a96..1b602abd2c 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -6,97 +6,58 @@ import logging import subprocess from operator import itemgetter import ftrack_api -from pype.ftrack import BaseHandler +from pype.ftrack import BaseAction from app.api import Logger -from pype import lib +from pype import pypelib + log = Logger.getLogger(__name__) -class DJVViewAction(BaseHandler): +class DJVViewAction(BaseAction): """Launch DJVView action.""" identifier = "djvview-launch-action" label = "DJV View" + description = "DJV View Launcher" icon = "http://a.fsdn.com/allura/p/djv/icon" type = 'Application' def __init__(self, session): '''Expects a ftrack_api.Session instance''' super().__init__(session) - self.variant = None self.djv_path = None self.config_data = None - self.items = [] - if self.config_data is None: - self.load_config_data() + self.load_config_data() + self.set_djv_path() - application = self.get_application() - if application is None: - return - - applicationIdentifier = application["identifier"] - label = application["label"] - self.items.append({ - "actionIdentifier": self.identifier, - "label": label, - "variant": application.get("variant", None), - "description": application.get("description", None), - "icon": application.get("icon", "default"), - "applicationIdentifier": applicationIdentifier - }) - - if self.identifier is None: - raise ValueError( - 'Action missing identifier.' - ) - - def is_valid_selection(self, event): - selection = event["data"].get("selection", []) - - if not selection: - return - - entityType = selection[0]["entityType"] - - if entityType not in ["assetversion", "task"]: - return False - - return True - - def discover(self, event): - """Return available actions based on *event*. """ if self.djv_path is None: return - if not self.is_valid_selection(event): - return - return { - "items": self.items - } + self.allowed_types = self.config_data.get( + 'file_ext', ["img", "mov", "exr"] + ) def register(self): - '''Registers the action, subscribing the discover and launch topics.''' - self.session.event_hub.subscribe( - 'topic=ftrack.action.discover and source.user.username={0}'.format( - self.session.api_user - ), self.discover - ) - launch_subscription = ( - 'topic=ftrack.action.launch' - ' and data.actionIdentifier={0}' - ' and source.user.username={1}' - ) - self.session.event_hub.subscribe( - launch_subscription.format( - self.identifier, - self.session.api_user - ), - self.launch + assert (self.djv_path is not None), ( + 'DJV View is not installed' + ' or paths in presets are not set correctly' ) + super().register() + + def discover(self, session, entities, event): + """Return available actions based on *event*. """ + selection = event["data"].get("selection", []) + if len(selection) != 1: + return False + + entityType = selection[0].get("entityType", None) + if entityType in ["assetversion", "task"]: + return True + return False def load_config_data(self): - path_items = [lib.get_presets_path(), 'djv_view', 'config.json'] + path_items = [pypelib.get_presets_path(), 'djv_view', 'config.json'] filepath = os.path.sep.join(path_items) data = dict() @@ -110,245 +71,175 @@ class DJVViewAction(BaseHandler): self.config_data = data - def get_application(self): - applicationIdentifier = "djvview" - description = "DJV View Launcher" - - possible_paths = self.config_data.get("djv_paths", []) - for path in possible_paths: + def set_djv_path(self): + for path in self.config_data.get("djv_paths", []): if os.path.exists(path): self.djv_path = path break - if self.djv_path is None: - log.debug("DJV View application was not found") - return None + def interface(self, session, entities, event): + if event['data'].get('values', {}): + return - application = { - 'identifier': applicationIdentifier, - 'label': self.label, - 'icon': self.icon, - 'description': description - } - - versionExpression = re.compile(r"(?P\d+.\d+.\d+)") - versionMatch = versionExpression.search(self.djv_path) - if versionMatch: - new_label = '{} {}'.format( - application['label'], versionMatch.group('version') - ) - application['label'] = new_label - - return application - - def translate_event(self, session, event): - '''Return *event* translated structure to be used with the API.''' - - selection = event['data'].get('selection', []) - - entities = list() - for entity in selection: - entities.append( - (session.get( - self.get_entity_type(entity), entity.get('entityId') - )) - ) - - return entities - - def get_entity_type(self, entity): - entity_type = entity.get('entityType').replace('_', '').lower() - - for schema in self.session.schemas: - alias_for = schema.get('alias_for') + entity = entities[0] + versions = [] + entity_type = entity.entity_type.lower() + if entity_type == "assetversion": if ( - alias_for and isinstance(alias_for, str) and - alias_for.lower() == entity_type + entity[ + 'components' + ][0]['file_type'][1:] in self.allowed_types ): - return schema['id'] + versions.append(entity) + else: + master_entity = entity + if entity_type == "task": + master_entity = entity['parent'] - for schema in self.session.schemas: - if schema['id'].lower() == entity_type: - return schema['id'] - - raise ValueError( - 'Unable to translate entity type: {0}.'.format(entity_type) - ) - - def launch(self, event): - """Callback method for DJVView action.""" - session = self.session - entities = self.translate_event(session, event) - - # Launching application - if "values" in event["data"]: - filename = event['data']['values']['path'] - - # TODO Is this proper way? - try: - fps = int(entities[0]['custom_attributes']['fps']) - except Exception: - fps = 24 - - cmd = [] - # DJV path - cmd.append(os.path.normpath(self.djv_path)) - # DJV Options Start ############################################## - '''layer name''' - # cmd.append('-file_layer (value)') - ''' Proxy scale: 1/2, 1/4, 1/8''' - # cmd.append('-file_proxy 1/2') - ''' Cache: True, False.''' - cmd.append('-file_cache True') - ''' Start in full screen ''' - # cmd.append('-window_fullscreen') - ''' Toolbar controls: False, True.''' - # cmd.append("-window_toolbar False") - ''' Window controls: False, True.''' - # cmd.append("-window_playbar False") - ''' Grid overlay: None, 1x1, 10x10, 100x100.''' - # cmd.append("-view_grid None") - ''' Heads up display: True, False.''' - # cmd.append("-view_hud True") - ''' Playback: Stop, Forward, Reverse.''' - cmd.append("-playback Forward") - ''' Frame.''' - # cmd.append("-playback_frame (value)") - cmd.append("-playback_speed " + str(fps)) - ''' Timer: Sleep, Timeout. Value: Sleep.''' - # cmd.append("-playback_timer (value)") - ''' Timer resolution (seconds): 0.001.''' - # cmd.append("-playback_timer_resolution (value)") - ''' Time units: Timecode, Frames.''' - cmd.append("-time_units Frames") - # DJV Options End ################################################ - - # PATH TO COMPONENT - cmd.append(os.path.normpath(filename)) - - try: - # Run DJV with these commands - subprocess.Popen(' '.join(cmd)) - except FileNotFoundError: - return { - 'success': False, - 'message': 'File "{}" was not found.'.format( - os.path.basename(filename) - ) - } - - return { - 'success': True, - 'message': 'DJV View started.' - } - - if 'items' not in event["data"]: - event["data"]['items'] = [] - - try: - for entity in entities: - versions = [] - self.load_config_data() - default_types = ["img", "mov", "exr"] - allowed_types = self.config_data.get('file_ext', default_types) - - if entity.entity_type.lower() == "assetversion": + for asset in master_entity['assets']: + for version in asset['versions']: + # Get only AssetVersion of selected task if ( - entity[ - 'components' - ][0]['file_type'][1:] in allowed_types + entity_type == "task" and + version['task']['id'] != entity['id'] ): - versions.append(entity) + continue + # Get only components with allowed type + filetype = version['components'][0]['file_type'] + if filetype[1:] in self.allowed_types: + versions.append(version) - elif entity.entity_type.lower() == "task": - # AssetVersions are obtainable only from shot! - shotentity = entity['parent'] - - for asset in shotentity['assets']: - for version in asset['versions']: - # Get only AssetVersion of selected task - if version['task']['id'] != entity['id']: - continue - # Get only components with allowed type - filetype = version['components'][0]['file_type'] - if filetype[1:] in allowed_types: - versions.append(version) - - # Raise error if no components were found - if len(versions) < 1: - raise ValueError('There are no Asset Versions to open.') - - for version in versions: - logging.info(version['components']) - for component in version['components']: - label = "v{0} - {1} - {2}" - - label = label.format( - str(version['version']).zfill(3), - version['asset']['type']['name'], - component['name'] - ) - - try: - # TODO This is proper way to get filepath!!! - location = component[ - 'component_locations' - ][0]['location'] - file_path = location.get_filesystem_path(component) - # if component.isSequence(): - # if component.getMembers(): - # frame = int( - # component.getMembers()[0].getName() - # ) - # file_path = file_path % frame - except Exception: - # This works but is NOT proper way - file_path = component[ - 'component_locations' - ][0]['resource_identifier'] - - dirpath = os.path.dirname(file_path) - if os.path.isdir(dirpath): - event["data"]["items"].append( - {"label": label, "value": file_path} - ) - - # Raise error if any component is playable - if len(event["data"]["items"]) == 0: - raise ValueError( - 'There are no Asset Versions with accessible path.' - ) - - except Exception as e: + if len(versions) < 1: return { 'success': False, - 'message': str(e) + 'message': 'There are no Asset Versions to open.' } - return { - "items": [ - { - "label": "Items to view", - "type": "enumerator", - "name": "path", - "data": sorted( - event["data"]['items'], - key=itemgetter("label"), - reverse=True - ) - } - ] - } + items = [] + base_label = "v{0} - {1} - {2}" + default_component = self.config_data.get( + 'default_component', None + ) + last_available = None + select_value = None + for version in versions: + for component in version['components']: + label = base_label.format( + str(version['version']).zfill(3), + version['asset']['type']['name'], + component['name'] + ) + try: + location = component[ + 'component_locations' + ][0]['location'] + file_path = location.get_filesystem_path(component) + except Exception: + file_path = component[ + 'component_locations' + ][0]['resource_identifier'] + + if os.path.isdir(os.path.dirname(file_path)): + last_available = file_path + if component['name'] == default_component: + select_value = file_path + items.append( + {'label': label, 'value': file_path} + ) + + if len(items) == 0: + return { + 'success': False, + 'message': ( + 'There are no Asset Versions with accessible path.' + ) + } + + item = { + 'label': 'Items to view', + 'type': 'enumerator', + 'name': 'path', + 'data': sorted( + items, + key=itemgetter('label'), + reverse=True + ) + } + if select_value is not None: + item['value'] = select_value + else: + item['value'] = last_available + + return {'items': [item]} + + def launch(self, session, entities, event): + """Callback method for DJVView action.""" + + # Launching application + if "values" not in event["data"]: + return + filename = event['data']['values']['path'] + + fps = entities[0].get('custom_attributes', {}).get('fps', None) + + cmd = [] + # DJV path + cmd.append(os.path.normpath(self.djv_path)) + # DJV Options Start ############################################## + # '''layer name''' + # cmd.append('-file_layer (value)') + # ''' Proxy scale: 1/2, 1/4, 1/8''' + # cmd.append('-file_proxy 1/2') + # ''' Cache: True, False.''' + # cmd.append('-file_cache True') + # ''' Start in full screen ''' + # cmd.append('-window_fullscreen') + # ''' Toolbar controls: False, True.''' + # cmd.append("-window_toolbar False") + # ''' Window controls: False, True.''' + # cmd.append("-window_playbar False") + # ''' Grid overlay: None, 1x1, 10x10, 100x100.''' + # cmd.append("-view_grid None") + # ''' Heads up display: True, False.''' + # cmd.append("-view_hud True") + ''' Playback: Stop, Forward, Reverse.''' + cmd.append("-playback Forward") + # ''' Frame.''' + # cmd.append("-playback_frame (value)") + if fps is not None: + cmd.append("-playback_speed {}".format(int(fps))) + # ''' Timer: Sleep, Timeout. Value: Sleep.''' + # cmd.append("-playback_timer (value)") + # ''' Timer resolution (seconds): 0.001.''' + # cmd.append("-playback_timer_resolution (value)") + ''' Time units: Timecode, Frames.''' + cmd.append("-time_units Frames") + # DJV Options End ################################################ + + # PATH TO COMPONENT + cmd.append(os.path.normpath(filename)) + + try: + # Run DJV with these commands + subprocess.Popen(' '.join(cmd)) + except FileNotFoundError: + return { + 'success': False, + 'message': 'File "{}" was not found.'.format( + os.path.basename(filename) + ) + } + + return True def register(session): """Register hooks.""" if not isinstance(session, ftrack_api.session.Session): return - action = DJVViewAction(session) - action.register() + DJVViewAction(session).register() def main(arguments=None): From 6f624c8a2d0e3264c61d5f8959f32675e45e6b93 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Mar 2019 18:17:15 +0100 Subject: [PATCH 76/99] base handler catch assert exceptions --- pype/ftrack/lib/ftrack_base_handler.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index 3bf6be0a0e..6ea30a1a3e 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -68,6 +68,10 @@ class BaseHandler(object): self.log.info(( '!{} "{}" - You\'re missing required permissions' ).format(self.type, label)) + except AssertionError as ae: + self.log.info(( + '!{} "{}" - {}' + ).format(self.type, label, str(ae))) except NotImplementedError: self.log.error(( '{} "{}" - Register method is not implemented' From 9c6dae4474ff67b338713c05e10735bf4b784248 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 15 Mar 2019 23:38:15 +0100 Subject: [PATCH 77/99] update readme --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 634ede742d..7cf8c4c0b6 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ The base studio *config* for [Avalon](https://getavalon.github.io/) -
+Currently this config is dependent on our customised avalon instalation so it won't work with vanilla avalon core. We're working on open sourcing all of the necessary code though. You can still get inspiration or take our individual validators and scripts which should work just fine in other pipelines. + _This configuration acts as a starting point for all pype club clients wth avalon deployment._ + + ### Code convention Below are some of the standard practices applied to this repositories. From b10ac0b279f0c4c5afcc6e7ed57a2e1961ad6dc8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Sat, 16 Mar 2019 09:50:54 +0100 Subject: [PATCH 78/99] fixed bug in start clockify app launch --- pype/ftrack/lib/ftrack_app_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 48531cf014..bd216ff6bf 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -332,8 +332,8 @@ class AppAction(BaseHandler): project_id = clockapi.get_project_id(project_name) tag_ids = [] - tag_ids.append(self.clockapi.get_tag_id(task_type)) - self.clockapi.start_time_entry( + tag_ids.append(clockapi.get_tag_id(task_type)) + clockapi.start_time_entry( description, project_id, tag_ids=tag_ids ) From 9554b76ed4772bdd8c5ef41c6b85fbb2b3194b26 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 19 Mar 2019 15:06:48 +0100 Subject: [PATCH 79/99] hotfix/zero out pivot on loaded models --- pype/plugins/maya/load/load_alembic.py | 3 +++ pype/plugins/maya/load/load_model.py | 11 +++++++++-- pype/plugins/maya/load/load_rig.py | 5 ++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/pype/plugins/maya/load/load_alembic.py b/pype/plugins/maya/load/load_alembic.py index 9e08702521..11ed0c4f64 100644 --- a/pype/plugins/maya/load/load_alembic.py +++ b/pype/plugins/maya/load/load_alembic.py @@ -16,6 +16,7 @@ class AbcLoader(pype.maya.plugin.ReferenceLoader): import maya.cmds as cmds + groupName = "{}:{}".format(namespace, name) cmds.loadPlugin("AbcImport.mll", quiet=True) nodes = cmds.file(self.fname, namespace=namespace, @@ -25,6 +26,8 @@ class AbcLoader(pype.maya.plugin.ReferenceLoader): reference=True, returnNewNodes=True) + cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) + self[:] = nodes return nodes diff --git a/pype/plugins/maya/load/load_model.py b/pype/plugins/maya/load/load_model.py index f29af65b72..9e46e16e92 100644 --- a/pype/plugins/maya/load/load_model.py +++ b/pype/plugins/maya/load/load_model.py @@ -20,12 +20,16 @@ class ModelLoader(pype.maya.plugin.ReferenceLoader): from avalon import maya with maya.maintained_selection(): + + groupName = "{}:{}".format(namespace, name) nodes = cmds.file(self.fname, namespace=namespace, reference=True, returnNewNodes=True, groupReference=True, - groupName="{}:{}".format(namespace, name)) + groupName=groupName) + + cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) self[:] = nodes @@ -141,15 +145,18 @@ class AbcModelLoader(pype.maya.plugin.ReferenceLoader): import maya.cmds as cmds + groupName = "{}:{}".format(namespace, name) cmds.loadPlugin("AbcImport.mll", quiet=True) nodes = cmds.file(self.fname, namespace=namespace, sharedReferenceFile=False, groupReference=True, - groupName="{}:{}".format(namespace, name), + groupName=groupName, reference=True, returnNewNodes=True) + cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) + self[:] = nodes return nodes diff --git a/pype/plugins/maya/load/load_rig.py b/pype/plugins/maya/load/load_rig.py index aa40ca3cc2..5a90783548 100644 --- a/pype/plugins/maya/load/load_rig.py +++ b/pype/plugins/maya/load/load_rig.py @@ -21,12 +21,15 @@ class RigLoader(pype.maya.plugin.ReferenceLoader): def process_reference(self, context, name, namespace, data): + groupName = "{}:{}".format(namespace, name) nodes = cmds.file(self.fname, namespace=namespace, reference=True, returnNewNodes=True, groupReference=True, - groupName="{}:{}".format(namespace, name)) + groupName=groupName) + + cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) # Store for post-process self[:] = nodes From 1bf91ce6b2b743df735d27239053f1ef4e749dba Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 19 Mar 2019 15:46:55 +0100 Subject: [PATCH 80/99] hotfix/collect arnold attributes in may --- pype/plugins/maya/publish/collect_look.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/plugins/maya/publish/collect_look.py b/pype/plugins/maya/publish/collect_look.py index 8befbfeee0..dfefa15fe5 100644 --- a/pype/plugins/maya/publish/collect_look.py +++ b/pype/plugins/maya/publish/collect_look.py @@ -47,6 +47,8 @@ def get_look_attrs(node): for attr in attrs: if attr in SHAPE_ATTRS: result.append(attr) + elif attr.startswith('ai'): + result.append(attr) return result @@ -387,6 +389,8 @@ class CollectLook(pyblish.api.InstancePlugin): # Collect changes to "custom" attributes node_attrs = get_look_attrs(node) + self.log.info(node_attrs) + # Only include if there are any properties we care about if not node_attrs: continue From 4f45e9e2a7070fa936e8fe5a7178768c7486f62a Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 19 Mar 2019 16:29:08 +0100 Subject: [PATCH 81/99] hotfix/check if transfers data exists in frames integrator before assigning it. --- pype/plugins/global/publish/integrate_rendered_frames.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/plugins/global/publish/integrate_rendered_frames.py b/pype/plugins/global/publish/integrate_rendered_frames.py index ae11d33348..1f6dc63d35 100644 --- a/pype/plugins/global/publish/integrate_rendered_frames.py +++ b/pype/plugins/global/publish/integrate_rendered_frames.py @@ -168,6 +168,9 @@ class IntegrateFrames(pyblish.api.InstancePlugin): representations = [] destination_list = [] + if 'transfers' not in instance.data: + instance.data['transfers'] = [] + for files in instance.data["files"]: # Collection # _______ From b6e22e7c477a37c2febcf495f9a51df27186b567 Mon Sep 17 00:00:00 2001 From: antirotor Date: Sat, 23 Mar 2019 23:42:49 +0100 Subject: [PATCH 82/99] feat(maya): colorize outliner based on families --- pype/plugins/maya/load/load_alembic.py | 19 ++++++- pype/plugins/maya/load/load_ass.py | 32 +++++++++++- pype/plugins/maya/load/load_camera.py | 17 +++++++ pype/plugins/maya/load/load_fbx.py | 17 +++++++ pype/plugins/maya/load/load_mayaascii.py | 16 ++++++ pype/plugins/maya/load/load_model.py | 50 ++++++++++++++++++- pype/plugins/maya/load/load_rig.py | 19 ++++++- .../plugins/maya/load/load_vdb_to_redshift.py | 15 ++++++ pype/plugins/maya/load/load_vdb_to_vray.py | 15 ++++++ pype/plugins/maya/load/load_yeti_cache.py | 13 +++++ pype/plugins/maya/load/load_yeti_rig.py | 16 ++++++ 11 files changed, 223 insertions(+), 6 deletions(-) diff --git a/pype/plugins/maya/load/load_alembic.py b/pype/plugins/maya/load/load_alembic.py index 11ed0c4f64..0869ff7f2f 100644 --- a/pype/plugins/maya/load/load_alembic.py +++ b/pype/plugins/maya/load/load_alembic.py @@ -1,4 +1,6 @@ import pype.maya.plugin +import os +import json class AbcLoader(pype.maya.plugin.ReferenceLoader): @@ -26,7 +28,22 @@ class AbcLoader(pype.maya.plugin.ReferenceLoader): reference=True, returnNewNodes=True) - cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) + cmds.makeIdentity(groupName, apply=False, rotate=True, + translate=True, scale=True) + + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('pointcache') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) self[:] = nodes diff --git a/pype/plugins/maya/load/load_ass.py b/pype/plugins/maya/load/load_ass.py index 13ad85473c..e96d404379 100644 --- a/pype/plugins/maya/load/load_ass.py +++ b/pype/plugins/maya/load/load_ass.py @@ -2,6 +2,7 @@ from avalon import api import pype.maya.plugin import os import pymel.core as pm +import json class AssProxyLoader(pype.maya.plugin.ReferenceLoader): @@ -34,7 +35,8 @@ class AssProxyLoader(pype.maya.plugin.ReferenceLoader): groupReference=True, groupName=groupName) - cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) + cmds.makeIdentity(groupName, apply=False, rotate=True, + translate=True, scale=True) # Set attributes proxyShape = pm.ls(nodes, type="mesh")[0] @@ -43,6 +45,19 @@ class AssProxyLoader(pype.maya.plugin.ReferenceLoader): proxyShape.dso.set(path) proxyShape.aiOverrideShaders.set(0) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('ass') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) self[:] = nodes @@ -132,7 +147,6 @@ class AssStandinLoader(api.Loader): import mtoa.ui.arnoldmenu import pymel.core as pm - asset = context['asset']['name'] namespace = namespace or lib.unique_namespace( asset + "_", @@ -146,6 +160,20 @@ class AssStandinLoader(api.Loader): label = "{}:{}".format(namespace, name) root = pm.group(name=label, empty=True) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('ass') + if c is not None: + cmds.setAttr(root + ".useOutlinerColor", 1) + cmds.setAttr(root + ".outlinerColor", + c[0], c[1], c[2]) + # Create transform with shape transform_name = label + "_ASS" # transform = pm.createNode("transform", name=transform_name, diff --git a/pype/plugins/maya/load/load_camera.py b/pype/plugins/maya/load/load_camera.py index eb75c3a63d..0483d3ac76 100644 --- a/pype/plugins/maya/load/load_camera.py +++ b/pype/plugins/maya/load/load_camera.py @@ -1,4 +1,6 @@ import pype.maya.plugin +import os +import json class CameraLoader(pype.maya.plugin.ReferenceLoader): @@ -17,6 +19,7 @@ class CameraLoader(pype.maya.plugin.ReferenceLoader): # Get family type from the context cmds.loadPlugin("AbcImport.mll", quiet=True) + groupName = "{}:{}".format(namespace, name) nodes = cmds.file(self.fname, namespace=namespace, sharedReferenceFile=False, @@ -27,6 +30,20 @@ class CameraLoader(pype.maya.plugin.ReferenceLoader): cameras = cmds.ls(nodes, type="camera") + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('camera') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) + # Check the Maya version, lockTransform has been introduced since # Maya 2016.5 Ext 2 version = int(cmds.about(version=True)) diff --git a/pype/plugins/maya/load/load_fbx.py b/pype/plugins/maya/load/load_fbx.py index 2ee3e5fdbd..f86b9a8dfa 100644 --- a/pype/plugins/maya/load/load_fbx.py +++ b/pype/plugins/maya/load/load_fbx.py @@ -1,4 +1,6 @@ import pype.maya.plugin +import os +import json class FBXLoader(pype.maya.plugin.ReferenceLoader): @@ -28,6 +30,21 @@ class FBXLoader(pype.maya.plugin.ReferenceLoader): groupReference=True, groupName="{}:{}".format(namespace, name)) + groupName = "{}:{}".format(namespace, name) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('fbx') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) + self[:] = nodes return nodes diff --git a/pype/plugins/maya/load/load_mayaascii.py b/pype/plugins/maya/load/load_mayaascii.py index 6f4c6a63a0..7521040c15 100644 --- a/pype/plugins/maya/load/load_mayaascii.py +++ b/pype/plugins/maya/load/load_mayaascii.py @@ -1,4 +1,6 @@ import pype.maya.plugin +import json +import os class MayaAsciiLoader(pype.maya.plugin.ReferenceLoader): @@ -28,6 +30,20 @@ class MayaAsciiLoader(pype.maya.plugin.ReferenceLoader): groupName="{}:{}".format(namespace, name)) self[:] = nodes + groupName = "{}:{}".format(namespace, name) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('mayaAscii') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) return nodes diff --git a/pype/plugins/maya/load/load_model.py b/pype/plugins/maya/load/load_model.py index 9e46e16e92..3eaed71e3f 100644 --- a/pype/plugins/maya/load/load_model.py +++ b/pype/plugins/maya/load/load_model.py @@ -1,5 +1,7 @@ from avalon import api import pype.maya.plugin +import json +import os class ModelLoader(pype.maya.plugin.ReferenceLoader): @@ -19,6 +21,14 @@ class ModelLoader(pype.maya.plugin.ReferenceLoader): import maya.cmds as cmds from avalon import maya + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + with maya.maintained_selection(): groupName = "{}:{}".format(namespace, name) @@ -29,7 +39,14 @@ class ModelLoader(pype.maya.plugin.ReferenceLoader): groupReference=True, groupName=groupName) - cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) + cmds.makeIdentity(groupName, apply=False, rotate=True, + translate=True, scale=True) + + c = colors.get('model') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) self[:] = nodes @@ -68,6 +85,19 @@ class GpuCacheLoader(api.Loader): # Root group label = "{}:{}".format(namespace, name) root = cmds.group(name=label, empty=True) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('model') + if c is not None: + cmds.setAttr(root + ".useOutlinerColor", 1) + cmds.setAttr(root + ".outlinerColor", + c[0], c[1], c[2]) # Create transform with shape transform_name = label + "_GPU" @@ -129,6 +159,7 @@ class GpuCacheLoader(api.Loader): except RuntimeError: pass + class AbcModelLoader(pype.maya.plugin.ReferenceLoader): """Specific loader of Alembic for the studio.animation family""" @@ -155,7 +186,22 @@ class AbcModelLoader(pype.maya.plugin.ReferenceLoader): reference=True, returnNewNodes=True) - cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) + cmds.makeIdentity(groupName, apply=False, rotate=True, + translate=True, scale=True) + + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('model') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) self[:] = nodes diff --git a/pype/plugins/maya/load/load_rig.py b/pype/plugins/maya/load/load_rig.py index 5a90783548..d66a8f9007 100644 --- a/pype/plugins/maya/load/load_rig.py +++ b/pype/plugins/maya/load/load_rig.py @@ -2,6 +2,8 @@ from maya import cmds import pype.maya.plugin from avalon import api, maya +import os +import json class RigLoader(pype.maya.plugin.ReferenceLoader): @@ -29,7 +31,22 @@ class RigLoader(pype.maya.plugin.ReferenceLoader): groupReference=True, groupName=groupName) - cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) + cmds.makeIdentity(groupName, apply=False, rotate=True, + translate=True, scale=True) + + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('rig') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) # Store for post-process self[:] = nodes diff --git a/pype/plugins/maya/load/load_vdb_to_redshift.py b/pype/plugins/maya/load/load_vdb_to_redshift.py index 8ff8bc0326..c4023e2618 100644 --- a/pype/plugins/maya/load/load_vdb_to_redshift.py +++ b/pype/plugins/maya/load/load_vdb_to_redshift.py @@ -1,4 +1,6 @@ from avalon import api +import os +import json class LoadVDBtoRedShift(api.Loader): @@ -48,6 +50,19 @@ class LoadVDBtoRedShift(api.Loader): # Root group label = "{}:{}".format(namespace, name) root = cmds.group(name=label, empty=True) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('vdbcache') + if c is not None: + cmds.setAttr(root + ".useOutlinerColor", 1) + cmds.setAttr(root + ".outlinerColor", + c[0], c[1], c[2]) # Create VR volume_node = cmds.createNode("RedshiftVolumeShape", diff --git a/pype/plugins/maya/load/load_vdb_to_vray.py b/pype/plugins/maya/load/load_vdb_to_vray.py index ac20b0eb43..0abf1bd952 100644 --- a/pype/plugins/maya/load/load_vdb_to_vray.py +++ b/pype/plugins/maya/load/load_vdb_to_vray.py @@ -1,4 +1,6 @@ from avalon import api +import json +import os class LoadVDBtoVRay(api.Loader): @@ -40,6 +42,19 @@ class LoadVDBtoVRay(api.Loader): # Root group label = "{}:{}".format(namespace, name) root = cmds.group(name=label, empty=True) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('vdbcache') + if c is not None: + cmds.setAttr(root + ".useOutlinerColor", 1) + cmds.setAttr(root + ".outlinerColor", + c[0], c[1], c[2]) # Create VR grid_node = cmds.createNode("VRayVolumeGrid", diff --git a/pype/plugins/maya/load/load_yeti_cache.py b/pype/plugins/maya/load/load_yeti_cache.py index 2160924047..908687a5c5 100644 --- a/pype/plugins/maya/load/load_yeti_cache.py +++ b/pype/plugins/maya/load/load_yeti_cache.py @@ -49,6 +49,19 @@ class YetiCacheLoader(api.Loader): group_name = "{}:{}".format(namespace, name) group_node = cmds.group(nodes, name=group_name) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('yeticache') + if c is not None: + cmds.setAttr(group_name + ".useOutlinerColor", 1) + cmds.setAttr(group_name + ".outlinerColor", + c[0], c[1], c[2]) nodes.append(group_node) diff --git a/pype/plugins/maya/load/load_yeti_rig.py b/pype/plugins/maya/load/load_yeti_rig.py index 096b936b41..c821c6ca02 100644 --- a/pype/plugins/maya/load/load_yeti_rig.py +++ b/pype/plugins/maya/load/load_yeti_rig.py @@ -1,4 +1,6 @@ import pype.maya.plugin +import os +import json class YetiRigLoader(pype.maya.plugin.ReferenceLoader): @@ -24,6 +26,20 @@ class YetiRigLoader(pype.maya.plugin.ReferenceLoader): groupReference=True, groupName="{}:{}".format(namespace, name)) + groupName = "{}:{}".format(namespace, name) + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + + c = colors.get('yetiRig') + if c is not None: + cmds.setAttr(groupName + ".useOutlinerColor", 1) + cmds.setAttr(groupName + ".outlinerColor", + c[0], c[1], c[2]) self[:] = nodes self.log.info("Yeti Rig Connection Manager will be available soon") From 3077785c7e295a9ef52f7b32719122743f039b74 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 28 Mar 2019 11:48:17 +0100 Subject: [PATCH 83/99] hotfix. assumed destination not working for unknown reason :) --- .../global/publish/collect_assumed_destination.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pype/plugins/global/publish/collect_assumed_destination.py b/pype/plugins/global/publish/collect_assumed_destination.py index 96f7e4b585..7de358b422 100644 --- a/pype/plugins/global/publish/collect_assumed_destination.py +++ b/pype/plugins/global/publish/collect_assumed_destination.py @@ -8,14 +8,10 @@ class CollectAssumedDestination(pyblish.api.InstancePlugin): """Generate the assumed destination path where the file will be stored""" label = "Collect Assumed Destination" - order = pyblish.api.CollectorOrder + 0.499 + order = pyblish.api.CollectorOrder + 0.498 exclude_families = ["clip"] def process(self, instance): - if [ef for ef in self.exclude_families - if instance.data["family"] in ef]: - return - """Create a destination filepath based on the current data available Example template: @@ -27,6 +23,9 @@ class CollectAssumedDestination(pyblish.api.InstancePlugin): Returns: file path (str) """ + if [ef for ef in self.exclude_families + if instance.data["family"] in ef]: + return # get all the stuff from the database subset_name = instance.data["subset"] From b19f703189571fcf3346c03f90fd6eacb3bf4491 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 28 Mar 2019 14:03:55 +0100 Subject: [PATCH 84/99] change ;thumbnail update logic on ftrack event --- pype/ftrack/events/event_thumbnail_updates.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pype/ftrack/events/event_thumbnail_updates.py b/pype/ftrack/events/event_thumbnail_updates.py index a825088e60..50089e26b8 100644 --- a/pype/ftrack/events/event_thumbnail_updates.py +++ b/pype/ftrack/events/event_thumbnail_updates.py @@ -23,8 +23,12 @@ class ThumbnailEvents(BaseEvent): parent['name'], task['name'])) # Update task thumbnail from published version - if (entity['entityType'] == 'assetversion' and - entity['action'] == 'encoded'): + # if (entity['entityType'] == 'assetversion' and + # entity['action'] == 'encoded'): + if ( + entity['entityType'] == 'assetversion' + and 'thumbid' in entity['keys'] + ): version = session.get('AssetVersion', entity['entityId']) thumbnail = version.get('thumbnail') @@ -40,6 +44,7 @@ class ThumbnailEvents(BaseEvent): pass + def register(session, **kw): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): From a8d4409ce888ab3bc8145f580cb58336dac5a167 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sun, 31 Mar 2019 19:27:31 +0200 Subject: [PATCH 85/99] fix(pype): converting temlates.py into module so self. could be holding singleton data --- pype/templates.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/pype/templates.py b/pype/templates.py index c5578a983c..92a0e2c3c7 100644 --- a/pype/templates.py +++ b/pype/templates.py @@ -1,5 +1,6 @@ import os import re +import sys from avalon import io from avalon import api as avalon from . import lib @@ -7,12 +8,14 @@ from app.api import (Templates, Logger, format) log = Logger.getLogger(__name__, os.getenv("AVALON_APP", "pype-config")) -SESSION = None + +self = sys.modules[__name__] +self.SESSION = None def set_session(): lib.set_io_database() - SESSION = avalon.session + self.SESSION = avalon.session def load_data_from_templates(): @@ -104,9 +107,9 @@ def set_project_code(code): os.environ[KEY]: project code avalon.sesion[KEY]: project code """ - if SESSION is None: + if self.SESSION is None: set_session() - SESSION["AVALON_PROJECTCODE"] = code + self.SESSION["AVALON_PROJECTCODE"] = code os.environ["AVALON_PROJECTCODE"] = code @@ -118,9 +121,9 @@ def get_project_name(): string: project name """ - if SESSION is None: + if self.SESSION is None: set_session() - project_name = SESSION.get("AVALON_PROJECT", None) \ + project_name = self.SESSION.get("AVALON_PROJECT", None) \ or os.getenv("AVALON_PROJECT", None) assert project_name, log.error("missing `AVALON_PROJECT`" "in avalon session " @@ -138,9 +141,9 @@ def get_asset(): Raises: log: error """ - if SESSION is None: + if self.SESSION is None: set_session() - asset = SESSION.get("AVALON_ASSET", None) \ + asset = self.SESSION.get("AVALON_ASSET", None) \ or os.getenv("AVALON_ASSET", None) log.info("asset: {}".format(asset)) assert asset, log.error("missing `AVALON_ASSET`" @@ -159,9 +162,9 @@ def get_task(): Raises: log: error """ - if SESSION is None: + if self.SESSION is None: set_session() - task = SESSION.get("AVALON_TASK", None) \ + task = self.SESSION.get("AVALON_TASK", None) \ or os.getenv("AVALON_TASK", None) assert task, log.error("missing `AVALON_TASK`" "in avalon session " @@ -196,9 +199,9 @@ def set_hierarchy(hierarchy): Args: hierarchy (string): hierarchy path ("silo/folder/seq") """ - if SESSION is None: + if self.SESSION is None: set_session() - SESSION["AVALON_HIERARCHY"] = hierarchy + self.SESSION["AVALON_HIERARCHY"] = hierarchy os.environ["AVALON_HIERARCHY"] = hierarchy @@ -248,10 +251,10 @@ def set_avalon_workdir(project=None, avalon.session[AVALON_WORKDIR]: workdir path """ - if SESSION is None: + if self.SESSION is None: set_session() - awd = SESSION.get("AVALON_WORKDIR", None) \ - or os.getenv("AVALON_WORKDIR", None) + + awd = self.SESSION.get("AVALON_WORKDIR", None) or os.getenv("AVALON_WORKDIR", None) data = get_context_data(project, hierarchy, asset, task) if (not awd) or ("{" not in awd): @@ -259,7 +262,7 @@ def set_avalon_workdir(project=None, awd_filled = os.path.normpath(format(awd, data)) - SESSION["AVALON_WORKDIR"] = awd_filled + self.SESSION["AVALON_WORKDIR"] = awd_filled os.environ["AVALON_WORKDIR"] = awd_filled log.info("`AVALON_WORKDIR` fixed to: {}".format(awd_filled)) From 4e99de691c6faa0a2cef4fde6250d02ad85a0926 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sun, 31 Mar 2019 19:28:59 +0200 Subject: [PATCH 86/99] fix(nuke): fixing path formating to replace "\" in path to "/" --- pype/plugins/nuke/load/load_sequence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/nuke/load/load_sequence.py b/pype/plugins/nuke/load/load_sequence.py index a4a591e657..b80d1a0ca1 100644 --- a/pype/plugins/nuke/load/load_sequence.py +++ b/pype/plugins/nuke/load/load_sequence.py @@ -101,7 +101,7 @@ class LoadSequence(api.Loader): if namespace is None: namespace = context['asset']['name'] - file = self.fname + file = self.fname.replace("\\", "/") log.info("file: {}\n".format(self.fname)) read_name = "Read_" + context["representation"]["context"]["subset"] @@ -112,7 +112,7 @@ class LoadSequence(api.Loader): r = nuke.createNode( "Read", "name {}".format(read_name)) - r["file"].setValue(self.fname) + r["file"].setValue(file) # Set colorspace defined in version data colorspace = context["version"]["data"].get("colorspace", None) From 8cd1bfa55fa3f12c1c3a500f3bee8a9ecf22a7fb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sun, 31 Mar 2019 19:29:52 +0200 Subject: [PATCH 87/99] fix(pype): reading padding info for image sequence path from anatomy instead hardcoding it to ##### --- pype/plugins/global/publish/integrate_rendered_frames.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/integrate_rendered_frames.py b/pype/plugins/global/publish/integrate_rendered_frames.py index 1f6dc63d35..8e7e2a59c4 100644 --- a/pype/plugins/global/publish/integrate_rendered_frames.py +++ b/pype/plugins/global/publish/integrate_rendered_frames.py @@ -243,7 +243,7 @@ class IntegrateFrames(pyblish.api.InstancePlugin): instance.data["transfers"].append([src, dst]) - template_data["frame"] = "#####" + template_data["frame"] = "#" * anatomy.render.padding anatomy_filled = anatomy.format(template_data) path_to_save = anatomy_filled.render.path template = anatomy.render.fullpath From 0c9186dc6551a8011385365f00a0ef48d578e3fd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sun, 31 Mar 2019 19:31:24 +0200 Subject: [PATCH 88/99] fix(pype): fixing `host` attribute to `hosts`, it was not filtering plugins out of context --- pype/plugins/global/publish/extract_jpeg.py | 3 ++- pype/plugins/global/publish/extract_quicktime.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index a99e6bc787..7720c9d56d 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -16,9 +16,10 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): """ label = "Extract Jpeg EXR" + hosts = ["shell"] order = pyblish.api.ExtractorOrder families = ["imagesequence", "render", "write", "source"] - host = ["shell"] + def process(self, instance): start = instance.data.get("startFrame") diff --git a/pype/plugins/global/publish/extract_quicktime.py b/pype/plugins/global/publish/extract_quicktime.py index a226bf7e2a..621078e3c0 100644 --- a/pype/plugins/global/publish/extract_quicktime.py +++ b/pype/plugins/global/publish/extract_quicktime.py @@ -18,7 +18,7 @@ class ExtractQuicktimeEXR(pyblish.api.InstancePlugin): label = "Extract Quicktime EXR" order = pyblish.api.ExtractorOrder families = ["imagesequence", "render", "write", "source"] - host = ["shell"] + hosts = ["shell"] def process(self, instance): fps = instance.data.get("fps") From 90b3a1f78365f3db6876d9ed1b099fa50104d57f Mon Sep 17 00:00:00 2001 From: antirotor Date: Wed, 3 Apr 2019 12:50:58 +0200 Subject: [PATCH 89/99] fix(maya): outliner colorize now respects families in context --- pype/plugins/maya/load/load_alembic.py | 7 +++++- pype/plugins/maya/load/load_ass.py | 7 +++++- pype/plugins/maya/load/load_camera.py | 7 +++++- pype/plugins/maya/load/load_fbx.py | 7 +++++- pype/plugins/maya/load/load_mayaascii.py | 7 +++++- pype/plugins/maya/load/load_model.py | 6 ++++- pype/plugins/maya/load/load_rig.py | 7 +++++- .../plugins/maya/load/load_vdb_to_redshift.py | 7 +++++- pype/plugins/maya/load/load_vdb_to_vray.py | 7 +++++- pype/plugins/maya/load/load_vrayproxy.py | 24 +++++++++++++++++-- pype/plugins/maya/load/load_yeti_cache.py | 7 +++++- 11 files changed, 81 insertions(+), 12 deletions(-) diff --git a/pype/plugins/maya/load/load_alembic.py b/pype/plugins/maya/load/load_alembic.py index 0869ff7f2f..9fd4aa2108 100644 --- a/pype/plugins/maya/load/load_alembic.py +++ b/pype/plugins/maya/load/load_alembic.py @@ -18,6 +18,11 @@ class AbcLoader(pype.maya.plugin.ReferenceLoader): import maya.cmds as cmds + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "animation" + groupName = "{}:{}".format(namespace, name) cmds.loadPlugin("AbcImport.mll", quiet=True) nodes = cmds.file(self.fname, @@ -39,7 +44,7 @@ class AbcLoader(pype.maya.plugin.ReferenceLoader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('pointcache') + c = colors.get(family) if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_ass.py b/pype/plugins/maya/load/load_ass.py index e96d404379..c268ce70c5 100644 --- a/pype/plugins/maya/load/load_ass.py +++ b/pype/plugins/maya/load/load_ass.py @@ -22,6 +22,11 @@ class AssProxyLoader(pype.maya.plugin.ReferenceLoader): from avalon import maya import pymel.core as pm + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "ass" + with maya.maintained_selection(): groupName = "{}:{}".format(namespace, name) @@ -53,7 +58,7 @@ class AssProxyLoader(pype.maya.plugin.ReferenceLoader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('ass') + c = colors.get(family) if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_camera.py b/pype/plugins/maya/load/load_camera.py index 0483d3ac76..989e80e979 100644 --- a/pype/plugins/maya/load/load_camera.py +++ b/pype/plugins/maya/load/load_camera.py @@ -18,6 +18,11 @@ class CameraLoader(pype.maya.plugin.ReferenceLoader): import maya.cmds as cmds # Get family type from the context + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "camera" + cmds.loadPlugin("AbcImport.mll", quiet=True) groupName = "{}:{}".format(namespace, name) nodes = cmds.file(self.fname, @@ -38,7 +43,7 @@ class CameraLoader(pype.maya.plugin.ReferenceLoader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('camera') + c = colors.get(family) if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_fbx.py b/pype/plugins/maya/load/load_fbx.py index f86b9a8dfa..b580257334 100644 --- a/pype/plugins/maya/load/load_fbx.py +++ b/pype/plugins/maya/load/load_fbx.py @@ -19,6 +19,11 @@ class FBXLoader(pype.maya.plugin.ReferenceLoader): import maya.cmds as cmds from avalon import maya + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "fbx" + # Ensure FBX plug-in is loaded cmds.loadPlugin("fbxmaya", quiet=True) @@ -39,7 +44,7 @@ class FBXLoader(pype.maya.plugin.ReferenceLoader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('fbx') + c = colors.get(family) if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_mayaascii.py b/pype/plugins/maya/load/load_mayaascii.py index 7521040c15..549d1dff4c 100644 --- a/pype/plugins/maya/load/load_mayaascii.py +++ b/pype/plugins/maya/load/load_mayaascii.py @@ -21,6 +21,11 @@ class MayaAsciiLoader(pype.maya.plugin.ReferenceLoader): import maya.cmds as cmds from avalon import maya + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "model" + with maya.maintained_selection(): nodes = cmds.file(self.fname, namespace=namespace, @@ -39,7 +44,7 @@ class MayaAsciiLoader(pype.maya.plugin.ReferenceLoader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('mayaAscii') + c = colors.get(family) if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_model.py b/pype/plugins/maya/load/load_model.py index 3eaed71e3f..2e76ce8e28 100644 --- a/pype/plugins/maya/load/load_model.py +++ b/pype/plugins/maya/load/load_model.py @@ -21,6 +21,10 @@ class ModelLoader(pype.maya.plugin.ReferenceLoader): import maya.cmds as cmds from avalon import maya + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "model" preset_file = os.path.join( os.environ.get('PYPE_STUDIO_TEMPLATES'), 'presets', 'tools', @@ -42,7 +46,7 @@ class ModelLoader(pype.maya.plugin.ReferenceLoader): cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) - c = colors.get('model') + c = colors.get(family) if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_rig.py b/pype/plugins/maya/load/load_rig.py index d66a8f9007..1dcff45bb9 100644 --- a/pype/plugins/maya/load/load_rig.py +++ b/pype/plugins/maya/load/load_rig.py @@ -23,6 +23,11 @@ class RigLoader(pype.maya.plugin.ReferenceLoader): def process_reference(self, context, name, namespace, data): + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "rig" + groupName = "{}:{}".format(namespace, name) nodes = cmds.file(self.fname, namespace=namespace, @@ -42,7 +47,7 @@ class RigLoader(pype.maya.plugin.ReferenceLoader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('rig') + c = colors.get(family) if c is not None: cmds.setAttr(groupName + ".useOutlinerColor", 1) cmds.setAttr(groupName + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_vdb_to_redshift.py b/pype/plugins/maya/load/load_vdb_to_redshift.py index c4023e2618..169c3bf34a 100644 --- a/pype/plugins/maya/load/load_vdb_to_redshift.py +++ b/pype/plugins/maya/load/load_vdb_to_redshift.py @@ -19,6 +19,11 @@ class LoadVDBtoRedShift(api.Loader): import avalon.maya.lib as lib from avalon.maya.pipeline import containerise + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "vdbcache" + # Check if the plugin for redshift is available on the pc try: cmds.loadPlugin("redshift4maya", quiet=True) @@ -58,7 +63,7 @@ class LoadVDBtoRedShift(api.Loader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('vdbcache') + c = colors.get(family) if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_vdb_to_vray.py b/pype/plugins/maya/load/load_vdb_to_vray.py index 0abf1bd952..58d6d1b56e 100644 --- a/pype/plugins/maya/load/load_vdb_to_vray.py +++ b/pype/plugins/maya/load/load_vdb_to_vray.py @@ -18,6 +18,11 @@ class LoadVDBtoVRay(api.Loader): import avalon.maya.lib as lib from avalon.maya.pipeline import containerise + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "vdbcache" + # Check if viewport drawing engine is Open GL Core (compat) render_engine = None compatible = "OpenGLCoreProfileCompat" @@ -50,7 +55,7 @@ class LoadVDBtoVRay(api.Loader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('vdbcache') + c = colors.get(family) if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", diff --git a/pype/plugins/maya/load/load_vrayproxy.py b/pype/plugins/maya/load/load_vrayproxy.py index 9396e124ce..a3a114440a 100644 --- a/pype/plugins/maya/load/load_vrayproxy.py +++ b/pype/plugins/maya/load/load_vrayproxy.py @@ -1,6 +1,7 @@ from avalon.maya import lib from avalon import api - +import json +import os import maya.cmds as cmds @@ -20,6 +21,19 @@ class VRayProxyLoader(api.Loader): from avalon.maya.pipeline import containerise from pype.maya.lib import namespaced + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "vrayproxy" + + preset_file = os.path.join( + os.environ.get('PYPE_STUDIO_TEMPLATES'), + 'presets', 'tools', + 'family_colors.json' + ) + with open(preset_file, 'r') as cfile: + colors = json.load(cfile) + asset_name = context['asset']["name"] namespace = namespace or lib.unique_namespace( asset_name + "_", @@ -40,6 +54,12 @@ class VRayProxyLoader(api.Loader): if not nodes: return + c = colors.get(family) + if c is not None: + cmds.setAttr("{0}_{1}.useOutlinerColor".format(name, "GRP"), 1) + cmds.setAttr("{0}_{1}.outlinerColor".format(name, "GRP"), + c[0], c[1], c[2]) + return containerise( name=name, namespace=namespace, @@ -101,7 +121,7 @@ class VRayProxyLoader(api.Loader): # Create nodes vray_mesh = cmds.createNode('VRayMesh', name="{}_VRMS".format(name)) mesh_shape = cmds.createNode("mesh", name="{}_GEOShape".format(name)) - vray_mat = cmds.shadingNode("VRayMeshMaterial", asShader=True, + vray_mat = cmds.shadingNode("VRayMeshMaterial", asShader=True, name="{}_VRMM".format(name)) vray_mat_sg = cmds.sets(name="{}_VRSG".format(name), empty=True, diff --git a/pype/plugins/maya/load/load_yeti_cache.py b/pype/plugins/maya/load/load_yeti_cache.py index 908687a5c5..b19bed1393 100644 --- a/pype/plugins/maya/load/load_yeti_cache.py +++ b/pype/plugins/maya/load/load_yeti_cache.py @@ -23,6 +23,11 @@ class YetiCacheLoader(api.Loader): def load(self, context, name=None, namespace=None, data=None): + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "yeticache" + # Build namespace asset = context["asset"] if namespace is None: @@ -57,7 +62,7 @@ class YetiCacheLoader(api.Loader): with open(preset_file, 'r') as cfile: colors = json.load(cfile) - c = colors.get('yeticache') + c = colors.get(family) if c is not None: cmds.setAttr(group_name + ".useOutlinerColor", 1) cmds.setAttr(group_name + ".outlinerColor", From 366aec21a2964e2fd05426a876144e1d27b5856b Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 3 Apr 2019 14:20:08 +0200 Subject: [PATCH 90/99] add double check on the created namespace. this fixes problem with asseblies not loading becasue of the wron namespace --- pype/plugins/maya/load/load_model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/plugins/maya/load/load_model.py b/pype/plugins/maya/load/load_model.py index 2e76ce8e28..16f3556de7 100644 --- a/pype/plugins/maya/load/load_model.py +++ b/pype/plugins/maya/load/load_model.py @@ -190,6 +190,9 @@ class AbcModelLoader(pype.maya.plugin.ReferenceLoader): reference=True, returnNewNodes=True) + namespace = cmds.referenceQuery(nodes[0], namespace=True) + groupName = "{}:{}".format(namespace, name) + cmds.makeIdentity(groupName, apply=False, rotate=True, translate=True, scale=True) From eb3f8d80a2deae3428e1c3915ba0d0f8db2398f8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 4 Apr 2019 16:55:40 +0200 Subject: [PATCH 91/99] added "app" into all template_data thath fill "work" dir --- pype/ftrack/lib/ftrack_app_handler.py | 3 +++ pype/nuke/lib.py | 4 +++- .../global/publish/collect_assumed_destination.py | 7 ++++--- pype/plugins/global/publish/integrate.py | 10 ++++++---- .../global/publish/integrate_rendered_frames.py | 10 ++++++---- pype/plugins/global/publish/validate_templates.py | 6 ++++-- pype/templates.py | 6 +++--- 7 files changed, 29 insertions(+), 17 deletions(-) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index bd216ff6bf..30acd4d849 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -198,10 +198,13 @@ class AppAction(BaseHandler): if parents: hierarchy = os.path.join(*parents) + application = avalonlib.get_application(os.environ["AVALON_APP_NAME"]) + data = {"project": {"name": entity['project']['full_name'], "code": entity['project']['name']}, "task": entity['name'], "asset": entity['parent']['name'], + "app": application["application_dir"], "hierarchy": hierarchy} try: anatomy = anatomy.format(data) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index 0f29484d9f..2b196db4b4 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -2,7 +2,7 @@ import sys from collections import OrderedDict from pprint import pprint from avalon.vendor.Qt import QtGui -from avalon import api, io +from avalon import api, io, lib import avalon.nuke import pype.api as pype import nuke @@ -88,6 +88,7 @@ def create_write_node(name, data): ) nuke_dataflow_writes = get_dataflow(**data) nuke_colorspace_writes = get_colorspace(**data) + application = lib.get_application(os.environ["AVALON_APP_NAME"]) try: anatomy_filled = format_anatomy({ "subset": data["avalon"]["subset"], @@ -97,6 +98,7 @@ def create_write_node(name, data): "project": {"name": pype.get_project_name(), "code": pype.get_project_code()}, "representation": nuke_dataflow_writes.file_type, + "app": application["application_dir"], }) except Exception as e: log.error("problem with resolving anatomy tepmlate: {}".format(e)) diff --git a/pype/plugins/global/publish/collect_assumed_destination.py b/pype/plugins/global/publish/collect_assumed_destination.py index 7de358b422..2f567ebbb2 100644 --- a/pype/plugins/global/publish/collect_assumed_destination.py +++ b/pype/plugins/global/publish/collect_assumed_destination.py @@ -1,7 +1,7 @@ import os import pyblish.api -from avalon import io, api +from avalon import io, api, lib class CollectAssumedDestination(pyblish.api.InstancePlugin): @@ -67,7 +67,7 @@ class CollectAssumedDestination(pyblish.api.InstancePlugin): if hierarchy: # hierarchy = os.path.sep.join(hierarchy) hierarchy = os.path.join(*hierarchy) - + application = lib.get_application(os.environ["AVALON_APP_NAME"]) template_data = {"root": api.Session["AVALON_PROJECTS"], "project": {"name": project_name, "code": project['data']['code']}, @@ -77,7 +77,8 @@ class CollectAssumedDestination(pyblish.api.InstancePlugin): "subset": subset_name, "version": version_number, "hierarchy": hierarchy, - "representation": "TEMP"} + "representation": "TEMP", + "app": application["application_dir"]} instance.data["template"] = template instance.data["assumedTemplateData"] = template_data diff --git a/pype/plugins/global/publish/integrate.py b/pype/plugins/global/publish/integrate.py index 00096a95ee..5fd8a13670 100644 --- a/pype/plugins/global/publish/integrate.py +++ b/pype/plugins/global/publish/integrate.py @@ -4,7 +4,7 @@ import shutil import errno import pyblish.api -from avalon import api, io +from avalon import api, io, lib from avalon.vendor import filelink @@ -160,7 +160,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): if parents and len(parents) > 0: # hierarchy = os.path.sep.join(hierarchy) hierarchy = os.path.join(*parents) - + application = lib.get_application(os.environ["AVALON_APP_NAME"]) template_data = {"root": root, "project": {"name": PROJECT, "code": project['data']['code']}, @@ -169,7 +169,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "family": instance.data['family'], "subset": subset["name"], "version": int(version["name"]), - "hierarchy": hierarchy} + "hierarchy": hierarchy, + "app": application["application_dir"]} template_publish = project["config"]["template"]["publish"] anatomy = instance.context.data['anatomy'] @@ -260,7 +261,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "subset": subset["name"], "version": version["name"], "hierarchy": hierarchy, - "representation": ext[1:] + "representation": ext[1:], + "app": application["application_dir"] } } diff --git a/pype/plugins/global/publish/integrate_rendered_frames.py b/pype/plugins/global/publish/integrate_rendered_frames.py index 8e7e2a59c4..3a0e89c328 100644 --- a/pype/plugins/global/publish/integrate_rendered_frames.py +++ b/pype/plugins/global/publish/integrate_rendered_frames.py @@ -5,7 +5,7 @@ import clique import errno import pyblish.api -from avalon import api, io +from avalon import api, io, lib log = logging.getLogger(__name__) @@ -148,7 +148,7 @@ class IntegrateFrames(pyblish.api.InstancePlugin): if parents and len(parents) > 0: # hierarchy = os.path.sep.join(hierarchy) hierarchy = os.path.join(*parents) - + application = lib.get_application(os.environ["AVALON_APP_NAME"]) template_data = {"root": root, "project": {"name": PROJECT, "code": project['data']['code']}, @@ -158,7 +158,8 @@ class IntegrateFrames(pyblish.api.InstancePlugin): "family": instance.data['family'], "subset": subset["name"], "version": int(version["name"]), - "hierarchy": hierarchy} + "hierarchy": hierarchy, + "app": application["application_dir"]} # template_publish = project["config"]["template"]["publish"] anatomy = instance.context.data['anatomy'] @@ -272,7 +273,8 @@ class IntegrateFrames(pyblish.api.InstancePlugin): "subset": subset["name"], "version": int(version["name"]), "hierarchy": hierarchy, - "representation": ext[1:] + "representation": ext[1:], + "app": application["application_dir"] } } diff --git a/pype/plugins/global/publish/validate_templates.py b/pype/plugins/global/publish/validate_templates.py index 8f8eb45686..19d70a197c 100644 --- a/pype/plugins/global/publish/validate_templates.py +++ b/pype/plugins/global/publish/validate_templates.py @@ -22,7 +22,8 @@ class ValidateTemplates(pyblish.api.ContextPlugin): "version": 3, "task": "animation", "asset": "sh001", - "hierarchy": "ep101/sq01/sh010"} + "hierarchy": "ep101/sq01/sh010", + "app": "nuke"} anatomy = context.data["anatomy"].format(data) @@ -34,7 +35,8 @@ class ValidateTemplates(pyblish.api.ContextPlugin): "version": 1, "task": "lookdev", "asset": "bob", - "hierarchy": "ep101/sq01/bob"} + "hierarchy": "ep101/sq01/bob", + "app": "nuke"} anatomy = context.data["anatomy"].format(data) self.log.info(anatomy.work.file) diff --git a/pype/templates.py b/pype/templates.py index 92a0e2c3c7..7157cb1399 100644 --- a/pype/templates.py +++ b/pype/templates.py @@ -1,8 +1,7 @@ import os import re import sys -from avalon import io -from avalon import api as avalon +from avalon import io, api as avalon, lib as avalonlib from . import lib from app.api import (Templates, Logger, format) log = Logger.getLogger(__name__, @@ -222,13 +221,14 @@ def get_context_data(project=None, dict: contextual data """ - + application = avalonlib.get_application(os.environ["AVALON_APP_NAME"]) data = { "task": task or get_task(), "asset": asset or get_asset(), "project": {"name": project or get_project_name(), "code": get_project_code()}, "hierarchy": hierarchy or get_hierarchy(), + "app": application["application_dir"] } return data From 88fff9fad1d64ee5f0630ca0ed84ed42a27d2bd5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 4 Apr 2019 16:56:42 +0200 Subject: [PATCH 92/99] created action which creates sw folders in "work" path --- .../actions/action_create_sw_folders.py | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 pype/ftrack/actions/action_create_sw_folders.py diff --git a/pype/ftrack/actions/action_create_sw_folders.py b/pype/ftrack/actions/action_create_sw_folders.py new file mode 100644 index 0000000000..f6b14cb764 --- /dev/null +++ b/pype/ftrack/actions/action_create_sw_folders.py @@ -0,0 +1,155 @@ +import os +import sys +import json +import argparse +import logging + +import ftrack_api +from avalon import lib as avalonlib +from avalon.tools.libraryloader.io_nonsingleton import DbConnector +from pype import lib as pypelib +from pype.ftrack import BaseAction + + +class CreateSWFolders(BaseAction): + '''Edit meta data action.''' + + #: Action identifier. + identifier = 'create.sw.folders' + #: Action label. + label = 'Create SW Folders' + #: Action description. + description = 'Creates folders for all SW in project' + + + def __init__(self, session): + super().__init__(session) + self.avalon_db = DbConnector() + self.avalon_db.install() + + def discover(self, session, entities, event): + ''' Validation ''' + + return True + + def launch(self, session, entities, event): + if len(entities) != 1: + self.log.warning( + 'There are more entities in selection!' + ) + return False + entity = entities[0] + if entity.entity_type.lower() != 'task': + self.log.warning( + 'Selected entity is not Task!' + ) + return False + asset = entity['parent'] + project = asset['project'] + + project_name = project["full_name"] + self.avalon_db.Session['AVALON_PROJECT'] = project_name + av_project = self.avalon_db.find_one({'type': 'project'}) + av_asset = self.avalon_db.find_one({ + 'type': 'asset', + 'name': asset['name'] + }) + + templates = av_project["config"]["template"] + template = templates.get("work", None) + if template is None: + return False + + + data = { + "root": os.environ["AVALON_PROJECTS"], + "project": { + "name": project_name, + "code": project["name"] + }, + "hierarchy": av_asset['data']['hierarchy'], + "asset": asset['name'], + "task": entity['name'], + } + + apps = [] + if '{app}' in template: + # Apps in project + for app in av_project['data']['applications']: + app_data = avalonlib.get_application(app) + app_dir = app_data['application_dir'] + if app_dir not in apps: + apps.append(app_dir) + # Apps in presets + path_items = [pypelib.get_presets_path(), 'tools', 'sw_folders.json'] + filepath = os.path.sep.join(path_items) + + presets = dict() + try: + with open(filepath) as data_file: + presets = json.load(data_file) + except Exception as e: + self.log.warning('Wasn\'t able to load presets') + preset_apps = presets.get(project_name, presets.get('__default__', [])) + for app in preset_apps: + if app not in apps: + apps.append(app) + + # Create folders for apps + for app in apps: + data['app'] = app + self.log.info('Created folder for app {}'.format(app)) + path = os.path.normpath(template.format(**data)) + if os.path.exists(path): + continue + os.makedirs(path) + + return True + + +def register(session, **kw): + '''Register plugin. Called when used as an plugin.''' + + if not isinstance(session, ftrack_api.session.Session): + return + + CreateSWFolders(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 320572a94dc53037dcfe859113f604ce1bb99bcb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 4 Apr 2019 18:20:59 +0200 Subject: [PATCH 93/99] fixed import of lib in djv action --- pype/ftrack/actions/action_djvview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index 1b602abd2c..631a686921 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -8,7 +8,7 @@ from operator import itemgetter import ftrack_api from pype.ftrack import BaseAction from app.api import Logger -from pype import pypelib +from pype import lib as pypelib log = Logger.getLogger(__name__) From a38b2968a574ed47aad6ad4cc9b851427f638a18 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 4 Apr 2019 18:26:51 +0200 Subject: [PATCH 94/99] removed app from publish plugins with template fill --- .../global/publish/collect_assumed_destination.py | 7 +++---- pype/plugins/global/publish/integrate.py | 10 ++++------ .../global/publish/integrate_rendered_frames.py | 10 ++++------ pype/plugins/global/publish/validate_templates.py | 6 ++---- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/pype/plugins/global/publish/collect_assumed_destination.py b/pype/plugins/global/publish/collect_assumed_destination.py index 2f567ebbb2..7de358b422 100644 --- a/pype/plugins/global/publish/collect_assumed_destination.py +++ b/pype/plugins/global/publish/collect_assumed_destination.py @@ -1,7 +1,7 @@ import os import pyblish.api -from avalon import io, api, lib +from avalon import io, api class CollectAssumedDestination(pyblish.api.InstancePlugin): @@ -67,7 +67,7 @@ class CollectAssumedDestination(pyblish.api.InstancePlugin): if hierarchy: # hierarchy = os.path.sep.join(hierarchy) hierarchy = os.path.join(*hierarchy) - application = lib.get_application(os.environ["AVALON_APP_NAME"]) + template_data = {"root": api.Session["AVALON_PROJECTS"], "project": {"name": project_name, "code": project['data']['code']}, @@ -77,8 +77,7 @@ class CollectAssumedDestination(pyblish.api.InstancePlugin): "subset": subset_name, "version": version_number, "hierarchy": hierarchy, - "representation": "TEMP", - "app": application["application_dir"]} + "representation": "TEMP"} instance.data["template"] = template instance.data["assumedTemplateData"] = template_data diff --git a/pype/plugins/global/publish/integrate.py b/pype/plugins/global/publish/integrate.py index 5fd8a13670..00096a95ee 100644 --- a/pype/plugins/global/publish/integrate.py +++ b/pype/plugins/global/publish/integrate.py @@ -4,7 +4,7 @@ import shutil import errno import pyblish.api -from avalon import api, io, lib +from avalon import api, io from avalon.vendor import filelink @@ -160,7 +160,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): if parents and len(parents) > 0: # hierarchy = os.path.sep.join(hierarchy) hierarchy = os.path.join(*parents) - application = lib.get_application(os.environ["AVALON_APP_NAME"]) + template_data = {"root": root, "project": {"name": PROJECT, "code": project['data']['code']}, @@ -169,8 +169,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "family": instance.data['family'], "subset": subset["name"], "version": int(version["name"]), - "hierarchy": hierarchy, - "app": application["application_dir"]} + "hierarchy": hierarchy} template_publish = project["config"]["template"]["publish"] anatomy = instance.context.data['anatomy'] @@ -261,8 +260,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "subset": subset["name"], "version": version["name"], "hierarchy": hierarchy, - "representation": ext[1:], - "app": application["application_dir"] + "representation": ext[1:] } } diff --git a/pype/plugins/global/publish/integrate_rendered_frames.py b/pype/plugins/global/publish/integrate_rendered_frames.py index 3a0e89c328..8e7e2a59c4 100644 --- a/pype/plugins/global/publish/integrate_rendered_frames.py +++ b/pype/plugins/global/publish/integrate_rendered_frames.py @@ -5,7 +5,7 @@ import clique import errno import pyblish.api -from avalon import api, io, lib +from avalon import api, io log = logging.getLogger(__name__) @@ -148,7 +148,7 @@ class IntegrateFrames(pyblish.api.InstancePlugin): if parents and len(parents) > 0: # hierarchy = os.path.sep.join(hierarchy) hierarchy = os.path.join(*parents) - application = lib.get_application(os.environ["AVALON_APP_NAME"]) + template_data = {"root": root, "project": {"name": PROJECT, "code": project['data']['code']}, @@ -158,8 +158,7 @@ class IntegrateFrames(pyblish.api.InstancePlugin): "family": instance.data['family'], "subset": subset["name"], "version": int(version["name"]), - "hierarchy": hierarchy, - "app": application["application_dir"]} + "hierarchy": hierarchy} # template_publish = project["config"]["template"]["publish"] anatomy = instance.context.data['anatomy'] @@ -273,8 +272,7 @@ class IntegrateFrames(pyblish.api.InstancePlugin): "subset": subset["name"], "version": int(version["name"]), "hierarchy": hierarchy, - "representation": ext[1:], - "app": application["application_dir"] + "representation": ext[1:] } } diff --git a/pype/plugins/global/publish/validate_templates.py b/pype/plugins/global/publish/validate_templates.py index 19d70a197c..8f8eb45686 100644 --- a/pype/plugins/global/publish/validate_templates.py +++ b/pype/plugins/global/publish/validate_templates.py @@ -22,8 +22,7 @@ class ValidateTemplates(pyblish.api.ContextPlugin): "version": 3, "task": "animation", "asset": "sh001", - "hierarchy": "ep101/sq01/sh010", - "app": "nuke"} + "hierarchy": "ep101/sq01/sh010"} anatomy = context.data["anatomy"].format(data) @@ -35,8 +34,7 @@ class ValidateTemplates(pyblish.api.ContextPlugin): "version": 1, "task": "lookdev", "asset": "bob", - "hierarchy": "ep101/sq01/bob", - "app": "nuke"} + "hierarchy": "ep101/sq01/bob"} anatomy = context.data["anatomy"].format(data) self.log.info(anatomy.work.file) From 34a915a0633318e445fa5fe810b587c9d2f0bae3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 5 Apr 2019 12:44:53 +0200 Subject: [PATCH 95/99] clockify actions try to import Clockify API and won't crash if not successful --- pype/plugins/launcher/actions/ClockifyStart.py | 11 +++++++++-- pype/plugins/launcher/actions/ClockifySync.py | 14 +++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/pype/plugins/launcher/actions/ClockifyStart.py b/pype/plugins/launcher/actions/ClockifyStart.py index d0d1bb48f3..78a8b4e1b6 100644 --- a/pype/plugins/launcher/actions/ClockifyStart.py +++ b/pype/plugins/launcher/actions/ClockifyStart.py @@ -1,6 +1,10 @@ from avalon import api, io -from pype.clockify import ClockifyAPI from pype.api import Logger +try: + from pype.clockify import ClockifyAPI +except Exception: + pass + log = Logger.getLogger(__name__, "clockify_start") @@ -10,10 +14,13 @@ class ClockifyStart(api.Action): label = "Clockify - Start Timer" icon = "clockify_icon" order = 500 - clockapi = ClockifyAPI() + + exec("try: clockapi = ClockifyAPI()\nexcept: clockapi = None") def is_compatible(self, session): """Return whether the action is compatible with the session""" + if self.clockapi is None: + return False if "AVALON_TASK" in session: return True return False diff --git a/pype/plugins/launcher/actions/ClockifySync.py b/pype/plugins/launcher/actions/ClockifySync.py index d8c69bc768..c50fbc4b25 100644 --- a/pype/plugins/launcher/actions/ClockifySync.py +++ b/pype/plugins/launcher/actions/ClockifySync.py @@ -1,5 +1,8 @@ from avalon import api, io -from pype.clockify import ClockifyAPI +try: + from pype.clockify import ClockifyAPI +except Exception: + pass from pype.api import Logger log = Logger.getLogger(__name__, "clockify_sync") @@ -10,11 +13,16 @@ class ClockifySync(api.Action): label = "Sync to Clockify" icon = "clockify_white_icon" order = 500 - clockapi = ClockifyAPI() - have_permissions = clockapi.validate_workspace_perm() + exec( + "try:\n\tclockapi = ClockifyAPI()" + "\n\thave_permissions = clockapi.validate_workspace_perm()" + "\nexcept:\n\tclockapi = None" + ) def is_compatible(self, session): """Return whether the action is compatible with the session""" + if self.clockapi is None: + return False return self.have_permissions def process(self, session, **kwargs): From cae3c6be4fb2cde8921bded89de78371b86adfd4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 5 Apr 2019 16:25:07 +0200 Subject: [PATCH 96/99] Added action that can add note to multiple asset versions --- pype/ftrack/actions/action_multiple_notes.py | 162 +++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 pype/ftrack/actions/action_multiple_notes.py diff --git a/pype/ftrack/actions/action_multiple_notes.py b/pype/ftrack/actions/action_multiple_notes.py new file mode 100644 index 0000000000..c61f5b1e9c --- /dev/null +++ b/pype/ftrack/actions/action_multiple_notes.py @@ -0,0 +1,162 @@ +import os +import sys +import argparse +import logging +import json +import ftrack_api + +from pype.ftrack import BaseAction + + +class MultipleNotes(BaseAction): + '''Edit meta data action.''' + + #: Action identifier. + identifier = 'multiple.notes' + #: Action label. + label = 'Multiple Notes' + #: Action description. + description = 'Add same note to multiple Asset Versions' + icon = ( + 'https://cdn2.iconfinder.com/data/icons/' + 'mixed-rounded-flat-icon/512/note_1-512.png' + ) + + def discover(self, session, entities, event): + ''' Validation ''' + valid = True + for entity in entities: + if entity.entity_type.lower() != 'assetversion': + valid = False + break + return valid + + def interface(self, session, entities, event): + if not event['data'].get('values', {}): + note_label = { + 'type': 'label', + 'value': '# Enter note: #' + } + + note_value = { + 'name': 'note', + 'type': 'textarea' + } + + category_label = { + 'type': 'label', + 'value': '## Category: ##' + } + + category_data = [] + category_data.append({ + 'label': '- None -', + 'value': 'none' + }) + all_categories = session.query('NoteCategory').all() + for cat in all_categories: + category_data.append({ + 'label': cat['name'], + 'value': cat['id'] + }) + category_value = { + 'type': 'enumerator', + 'name': 'category', + 'data': category_data, + 'value': 'none' + } + + splitter = { + 'type': 'label', + 'value': '{}'.format(200*"-") + } + + items = [] + items.append(note_label) + items.append(note_value) + items.append(splitter) + items.append(category_label) + items.append(category_value) + return items + + def launch(self, session, entities, event): + if 'values' not in event['data']: + return + + values = event['data']['values'] + if len(values) <= 0 or 'note' not in values: + return False + # Get Note text + note_value = values['note'] + if note_value.lower().strip() == '': + return False + # Get User + user = session.query( + 'User where username is "{}"'.format(session.api_user) + ).one() + # Base note data + note_data = { + 'content': note_value, + 'author': user + } + # Get category + category_value = values['category'] + if category_value != 'none': + category = session.query( + 'NoteCategory where id is "{}"'.format(category_value) + ).one() + note_data['category'] = category + # Create notes for entities + for entity in entities: + new_note = session.create('Note', note_data) + entity['notes'].append(new_note) + session.commit() + return True + + +def register(session, **kw): + '''Register plugin. Called when used as an plugin.''' + + if not isinstance(session, ftrack_api.session.Session): + return + + MultipleNotes(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 4b9e647f8ded35452df983a4b0d1ee3bd826f200 Mon Sep 17 00:00:00 2001 From: jezschaj Date: Mon, 8 Apr 2019 10:29:39 +0200 Subject: [PATCH 97/99] feat(nuke): sync version of script with version of write --- setup/nuke/nuke_path/menu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup/nuke/nuke_path/menu.py b/setup/nuke/nuke_path/menu.py index 45f44d0d11..3613bc99f2 100644 --- a/setup/nuke/nuke_path/menu.py +++ b/setup/nuke/nuke_path/menu.py @@ -6,7 +6,7 @@ from pype.api import Logger log = Logger.getLogger(__name__, "nuke") -# nuke.addOnScriptSave(writes_version_sync) -# nuke.addOnScriptSave(onScriptLoad) +nuke.addOnScriptSave(writes_version_sync) +nuke.addOnScriptSave(onScriptLoad) log.info('Automatic syncing of write file knob to script version') From e50c4380f4651cdcce6fefe82a457769b9af1bc9 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 8 Apr 2019 17:27:08 +0200 Subject: [PATCH 98/99] keep extension in maya when versioning up --- .../maya/publish/increment_current_file_deadline.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pype/plugins/maya/publish/increment_current_file_deadline.py b/pype/plugins/maya/publish/increment_current_file_deadline.py index 527f3d781d..6f644adacb 100644 --- a/pype/plugins/maya/publish/increment_current_file_deadline.py +++ b/pype/plugins/maya/publish/increment_current_file_deadline.py @@ -31,10 +31,11 @@ class IncrementCurrentFileDeadline(pyblish.api.ContextPlugin): current_filepath = context.data["currentFile"] new_filepath = version_up(current_filepath) - # Ensure the suffix is .ma because we're saving to `mayaAscii` type - if not new_filepath.endswith(".ma"): - self.log.warning("Refactoring scene to .ma extension") - new_filepath = os.path.splitext(new_filepath)[0] + ".ma" + # # Ensure the suffix is .ma because we're saving to `mayaAscii` type + if new_filepath.endswith(".ma"): + fileType = "mayaAscii" + elif new_filepath.endswith(".mb"): + fileType = "mayaBinary" cmds.file(rename=new_filepath) - cmds.file(save=True, force=True, type="mayaAscii") + cmds.file(save=True, force=True, type=fileType) From 90a13216383d5f68d91870e7ba95ab3472382693 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 9 Apr 2019 10:44:05 +0200 Subject: [PATCH 99/99] fix(nuke): auto sync version was rising exceptions --- pype/nuke/lib.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index 0f29484d9f..5d4d1948ea 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -35,10 +35,12 @@ def writes_version_sync(): for each in nuke.allNodes(): if each.Class() == 'Write': avalon_knob_data = get_avalon_knob_data(each) - if avalon_knob_data['families'] not in ["render"]: - log.info(avalon_knob_data['families']) - continue + try: + if avalon_knob_data['families'] not in ["render"]: + log.info(avalon_knob_data['families']) + continue + node_file = each['file'].value() log.info("node_file: {}".format(node_file))