diff --git a/pype/muster/muster.py b/pype/muster/muster.py index b73ae49a32..2041ae5512 100644 --- a/pype/muster/muster.py +++ b/pype/muster/muster.py @@ -37,6 +37,12 @@ class MusterModule: # nothing to do pass + def process_modules(self, modules): + if "RestApiServer" in modules: + modules["RestApiServer"].register_callback( + "muster/show_login", self.show_login, "post" + ) + # Definition of Tray menu def tray_menu(self, parent): """ diff --git a/pype/plugins/ftrack/publish/integrate_remove_components.py b/pype/plugins/ftrack/publish/integrate_remove_components.py index a215ee1b97..bad50f7200 100644 --- a/pype/plugins/ftrack/publish/integrate_remove_components.py +++ b/pype/plugins/ftrack/publish/integrate_remove_components.py @@ -17,6 +17,9 @@ class IntegrateCleanComponentData(pyblish.api.InstancePlugin): for comp in instance.data['representations']: self.log.debug('component {}'.format(comp)) + + if "%" in comp['published_path'] or "#" in comp['published_path']: + continue if comp.get('thumbnail') or ("thumbnail" in comp.get('tags', [])): os.remove(comp['published_path']) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index e5d8007d70..61881b2a34 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -307,7 +307,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if repre.get("frameStart"): frame_start_padding = len(str( repre.get("frameEnd"))) - index_frame_start = repre.get("frameStart") + index_frame_start = int(repre.get("frameStart")) dst_padding_exp = src_padding_exp for i in src_collection.indexes: @@ -322,7 +322,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dst_padding = dst_padding_exp % index_frame_start index_frame_start += 1 - dst = "{0}{1}{2}".format(dst_head, dst_padding, dst_tail) + dst = "{0}{1}{2}".format(dst_head, dst_padding, dst_tail).replace("..", ".") self.log.debug("destination: `{}`".format(dst)) src = os.path.join(stagingdir, src_file_name) self.log.debug("source: {}".format(src)) @@ -357,7 +357,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): src = os.path.join(stagingdir, fname) anatomy_filled = anatomy.format(template_data) dst = os.path.normpath( - anatomy_filled[template_name]["path"]) + anatomy_filled[template_name]["path"]).replace("..", ".") instance.data["transfers"].append([src, dst]) @@ -440,6 +440,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): Returns: None """ + src = os.path.normpath(src) + dst = os.path.normpath(dst) self.log.debug("Copying file .. {} -> {}".format(src, dst)) dirname = os.path.dirname(dst) diff --git a/pype/plugins/nuke/publish/collect_active_viewer.py b/pype/plugins/nuke/_publish_unused/collect_active_viewer.py similarity index 83% rename from pype/plugins/nuke/publish/collect_active_viewer.py rename to pype/plugins/nuke/_publish_unused/collect_active_viewer.py index 5dc17d8768..5a6cc02b88 100644 --- a/pype/plugins/nuke/publish/collect_active_viewer.py +++ b/pype/plugins/nuke/_publish_unused/collect_active_viewer.py @@ -11,5 +11,4 @@ class CollectActiveViewer(pyblish.api.ContextPlugin): hosts = ["nuke"] def process(self, context): - context.data["ViewerProcess"] = nuke.ViewerProcess.node() context.data["ActiveViewer"] = nuke.activeViewer() diff --git a/pype/plugins/nuke/publish/validate_active_viewer.py b/pype/plugins/nuke/_publish_unused/validate_active_viewer.py similarity index 65% rename from pype/plugins/nuke/publish/validate_active_viewer.py rename to pype/plugins/nuke/_publish_unused/validate_active_viewer.py index bcf7cab6b3..618a7f1502 100644 --- a/pype/plugins/nuke/publish/validate_active_viewer.py +++ b/pype/plugins/nuke/_publish_unused/validate_active_viewer.py @@ -16,3 +16,9 @@ class ValidateActiveViewer(pyblish.api.ContextPlugin): assert viewer_process_node, ( "Missing active viewer process! Please click on output write node and push key number 1-9" ) + active_viewer = context.data["ActiveViewer"] + active_input = active_viewer.activeInput() + + assert active_input is not None, ( + "Missing active viewer input! Please click on output write node and push key number 1-9" + ) diff --git a/pype/plugins/nuke/load/load_luts.py b/pype/plugins/nuke/load/load_luts.py index 7e1302fffe..4f7c19a588 100644 --- a/pype/plugins/nuke/load/load_luts.py +++ b/pype/plugins/nuke/load/load_luts.py @@ -14,6 +14,7 @@ class LoadLuts(api.Loader): order = 0 icon = "cc" color = style.colors.light + ignore_attr = ["useLifetime"] def load(self, context, name, namespace, data): """ @@ -83,6 +84,8 @@ class LoadLuts(api.Loader): for ef_name, ef_val in nodes_order.items(): node = nuke.createNode(ef_val["class"]) for k, v in ef_val["node"].items(): + if k in self.ignore_attr: + continue if isinstance(v, list) and len(v) > 4: node[k].setAnimated() for i, value in enumerate(v): @@ -194,6 +197,8 @@ class LoadLuts(api.Loader): for ef_name, ef_val in nodes_order.items(): node = nuke.createNode(ef_val["class"]) for k, v in ef_val["node"].items(): + if k in self.ignore_attr: + continue if isinstance(v, list) and len(v) > 3: node[k].setAnimated() for i, value in enumerate(v): diff --git a/pype/plugins/nuke/load/load_luts_ip.py b/pype/plugins/nuke/load/load_luts_ip.py index b0a30d78e4..b30f84cc42 100644 --- a/pype/plugins/nuke/load/load_luts_ip.py +++ b/pype/plugins/nuke/load/load_luts_ip.py @@ -14,6 +14,7 @@ class LoadLutsInputProcess(api.Loader): order = 0 icon = "eye" color = style.colors.alert + ignore_attr = ["useLifetime"] def load(self, context, name, namespace, data): """ @@ -83,6 +84,8 @@ class LoadLutsInputProcess(api.Loader): for ef_name, ef_val in nodes_order.items(): node = nuke.createNode(ef_val["class"]) for k, v in ef_val["node"].items(): + if k in self.ignore_attr: + continue if isinstance(v, list) and len(v) > 4: node[k].setAnimated() for i, value in enumerate(v): @@ -196,6 +199,8 @@ class LoadLutsInputProcess(api.Loader): for ef_name, ef_val in nodes_order.items(): node = nuke.createNode(ef_val["class"]) for k, v in ef_val["node"].items(): + if k in self.ignore_attr: + continue if isinstance(v, list) and len(v) > 3: node[k].setAnimated() for i, value in enumerate(v): diff --git a/pype/plugins/nuke/publish/extract_ouput_node.py b/pype/plugins/nuke/publish/extract_ouput_node.py index 4d7533f010..a144761e5f 100644 --- a/pype/plugins/nuke/publish/extract_ouput_node.py +++ b/pype/plugins/nuke/publish/extract_ouput_node.py @@ -15,21 +15,17 @@ class CreateOutputNode(pyblish.api.ContextPlugin): def process(self, context): # capture selection state with maintained_selection(): - # deselect all allNodes - self.log.info(context.data["ActiveViewer"]) + active_node = [node for inst in context[:] + for node in inst[:] + if "ak:family" in node.knobs()] - active_viewer = context.data["ActiveViewer"] - active_input = active_viewer.activeInput() - active_node = active_viewer.node() - - - last_viewer_node = active_node.input(active_input) - - name = last_viewer_node.name() - self.log.info("Node name: {}".format(name)) + if active_node: + self.log.info(active_node) + active_node = active_node[0] + self.log.info(active_node) + active_node['selected'].setValue(True) # select only instance render node - last_viewer_node['selected'].setValue(True) output_node = nuke.createNode("Output") # deselect all and select the original selection diff --git a/pype/plugins/nuke/publish/extract_review_data.py b/pype/plugins/nuke/publish/extract_review_data.py index 40c3e37434..08eba5bb1e 100644 --- a/pype/plugins/nuke/publish/extract_review_data.py +++ b/pype/plugins/nuke/publish/extract_review_data.py @@ -3,7 +3,6 @@ import nuke import pyblish.api import pype - class ExtractReviewData(pype.api.Extractor): """Extracts movie and thumbnail with baked in luts @@ -48,9 +47,9 @@ class ExtractReviewData(pype.api.Extractor): assert instance.data['representations'][0]['files'], "Instance data files should't be empty!" - import nuke temporary_nodes = [] - stagingDir = instance.data['representations'][0]["stagingDir"].replace("\\", "/") + stagingDir = instance.data[ + 'representations'][0]["stagingDir"].replace("\\", "/") self.log.debug("StagingDir `{0}`...".format(stagingDir)) collection = instance.data.get("collection", None) @@ -70,16 +69,24 @@ class ExtractReviewData(pype.api.Extractor): first_frame = instance.data.get("frameStart", None) last_frame = instance.data.get("frameEnd", None) - node = previous_node = nuke.createNode("Read") + rnode = nuke.createNode("Read") - node["file"].setValue( + rnode["file"].setValue( os.path.join(stagingDir, fname).replace("\\", "/")) - node["first"].setValue(first_frame) - node["origfirst"].setValue(first_frame) - node["last"].setValue(last_frame) - node["origlast"].setValue(last_frame) - temporary_nodes.append(node) + rnode["first"].setValue(first_frame) + rnode["origfirst"].setValue(first_frame) + rnode["last"].setValue(last_frame) + rnode["origlast"].setValue(last_frame) + temporary_nodes.append(rnode) + previous_node = rnode + + # get input process and connect it to baking + ipn = self.get_view_process_node() + if ipn is not None: + ipn.setInput(0, previous_node) + previous_node = ipn + temporary_nodes.append(ipn) reformat_node = nuke.createNode("Reformat") @@ -95,22 +102,10 @@ class ExtractReviewData(pype.api.Extractor): previous_node = reformat_node temporary_nodes.append(reformat_node) - viewer_process_node = instance.context.data.get("ViewerProcess") - dag_node = None - if viewer_process_node: - dag_node = nuke.createNode(viewer_process_node.Class()) - dag_node.setInput(0, previous_node) - previous_node = dag_node - temporary_nodes.append(dag_node) - # Copy viewer process values - excludedKnobs = ["name", "xpos", "ypos"] - for item in viewer_process_node.knobs().keys(): - if item not in excludedKnobs and item in dag_node.knobs(): - x1 = viewer_process_node[item] - x2 = dag_node[item] - x2.fromScript(x1.toScript(False)) - else: - self.log.warning("No viewer node found.") + dag_node = nuke.createNode("OCIODisplay") + dag_node.setInput(0, previous_node) + previous_node = dag_node + temporary_nodes.append(dag_node) # create write node write_node = nuke.createNode("Write") @@ -164,3 +159,28 @@ class ExtractReviewData(pype.api.Extractor): # Clean up for node in temporary_nodes: nuke.delete(node) + + def get_view_process_node(self): + + # Select only the target node + if nuke.selectedNodes(): + [n.setSelected(False) for n in nuke.selectedNodes()] + + for v in [n for n in nuke.allNodes() + if "Viewer" in n.Class()]: + ip = v['input_process'].getValue() + ipn = v['input_process_node'].getValue() + if "VIEWER_INPUT" not in ipn and ip: + ipn_orig = nuke.toNode(ipn) + ipn_orig.setSelected(True) + + if ipn_orig: + nuke.nodeCopy('%clipboard%') + + [n.setSelected(False) for n in nuke.selectedNodes()] # Deselect all + + nuke.nodePaste('%clipboard%') + + ipn = nuke.selectedNode() + + return ipn diff --git a/pype/plugins/nuke/publish/validate_rendered_frames.py b/pype/plugins/nuke/publish/validate_rendered_frames.py index 85cbe7b2c0..3887b5d5b7 100644 --- a/pype/plugins/nuke/publish/validate_rendered_frames.py +++ b/pype/plugins/nuke/publish/validate_rendered_frames.py @@ -81,3 +81,5 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): ).format(__name__) instance.data['collection'] = collection + + return diff --git a/pype/services/rest_api/__init__.py b/pype/services/rest_api/__init__.py new file mode 100644 index 0000000000..c11ecfd761 --- /dev/null +++ b/pype/services/rest_api/__init__.py @@ -0,0 +1,5 @@ +from .rest_api import RestApiServer + + +def tray_init(tray_widget, main_widget): + return RestApiServer() diff --git a/pype/services/rest_api/rest_api.py b/pype/services/rest_api/rest_api.py new file mode 100644 index 0000000000..894ac8e986 --- /dev/null +++ b/pype/services/rest_api/rest_api.py @@ -0,0 +1,237 @@ +import os +import json +import enum +import collections +import threading +from inspect import signature +import socket +import http.server +from http import HTTPStatus +import socketserver + +from Qt import QtCore + +from pypeapp import config, Logger + +log = Logger().get_logger("RestApiServer") + + +class RestMethods(enum.Enum): + GET = "GET" + POST = "POST" + PUT = "PUT" + PATCH = "PATCH" + DELETE = "DELETE" + + def __repr__(self): + return str(self.value) + + def __eq__(self, other): + if isinstance(other, str): + return self.value == other + return self == other + + def __hash__(self): + return enum.Enum.__hash__(self) + + def __str__(self): + return str(self.value) + + +class Handler(http.server.SimpleHTTPRequestHandler): + + def do_GET(self): + self.process_request(RestMethods.GET) + + def do_POST(self): + """Common code for POST. + + This trigger callbacks on specific paths. + + If request contain data and callback func has arg data are sent to + callback too. + + Send back return values of callbacks. + """ + self.process_request(RestMethods.POST) + + def process_request(self, rest_method): + """Because processing is technically the same for now so it is used + the same way + """ + content_length = int(self.headers["Content-Length"]) + in_data_str = self.rfile.read(content_length) + in_data = None + if in_data_str: + in_data = json.loads(in_data_str) + + registered_callbacks = self.server.registered_callbacks[rest_method] + + path_items = [part.lower() for part in self.path.split("/") if part] + + results = [] + for check_path, callbacks in registered_callbacks.items(): + check_path_items = check_path.split("/") + if check_path_items == path_items: + log.debug( + "Triggering callbacks for path \"{}\"".format(check_path) + ) + for callback in callbacks: + try: + params = signature(callback).parameters + if len(params) > 0 and in_data: + result = callback(in_data) + else: + result = callback() + + if result: + results.append(result) + except Exception: + log.error( + "Callback on path \"{}\" failed".format(check_path), + exc_info=True + ) + + any_result = len(results) > 0 + self.send_response(HTTPStatus.OK) + if any_result: + self.send_header("Content-type", "application/json") + self.end_headers() + + if not any_result: + return + + if len(results) == 1: + json_message = str(results[0]) + else: + index = 1 + messages = {} + for result in results: + if isinstance(result, str): + value = result + else: + value = json.dumps(result) + messages["callback{}".format(str(index))] = value + + json_message = json.dumps(messages) + + self.wfile.write(json_message.encode()) + + +class AdditionalArgsTCPServer(socketserver.TCPServer): + def __init__(self, registered_callbacks, *args, **kwargs): + self.registered_callbacks = registered_callbacks + super(AdditionalArgsTCPServer, self).__init__(*args, **kwargs) + + +class RestApiServer(QtCore.QThread): + """ Listener for REST requests. + + It is possible to register callbacks for url paths. + Be careful about crossreferencing to different QThreads it is not allowed. + """ + + def __init__(self): + super(RestApiServer, self).__init__() + self.registered_callbacks = { + RestMethods.GET: collections.defaultdict(list), + RestMethods.POST: collections.defaultdict(list), + RestMethods.PUT: collections.defaultdict(list), + RestMethods.PATCH: collections.defaultdict(list), + RestMethods.DELETE: collections.defaultdict(list) + } + + self.qaction = None + self.failed_icon = None + self._is_running = False + try: + self.presets = config.get_presets().get( + "services", {}).get( + "rest_api", {} + ) + except Exception: + self.presets = {"default_port": 8011, "exclude_ports": []} + + self.port = self.find_port() + + def set_qaction(self, qaction, failed_icon): + self.qaction = qaction + self.failed_icon = failed_icon + + def register_callback(self, path, callback, rest_method=RestMethods.POST): + if isinstance(path, (list, set)): + path = "/".join([part.lower() for part in path]) + elif isinstance(path, str): + path = "/".join( + [part.lower() for part in str(path).split("/") if part] + ) + + if isinstance(rest_method, str): + rest_method = str(rest_method).upper() + + if path in self.registered_callbacks[rest_method]: + log.warning( + "Path \"{}\" has already registered callback.".format(path) + ) + else: + log.debug( + "Registering callback for path \"{}\"".format(path) + ) + self.registered_callbacks[rest_method][path].append(callback) + + def tray_start(self): + self.start() + + @property + def is_running(self): + return self._is_running + + def stop(self): + self._is_running = False + + def run(self): + self._is_running = True + if not self.registered_callbacks: + log.info("Any registered callbacks for Rest Api server.") + return + + try: + log.debug( + "Running Rest Api server on URL:" + " \"http://localhost:{}\"".format(self.port) + ) + with AdditionalArgsTCPServer( + self.registered_callbacks, + ("", self.port), + Handler + ) as httpd: + while self._is_running: + httpd.handle_request() + except Exception: + log.warning( + "Rest Api Server service has failed", exc_info=True + ) + self._is_running = False + if self.qaction and self.failed_icon: + self.qaction.setIcon(self.failed_icon) + + def find_port(self): + start_port = self.presets["default_port"] + exclude_ports = self.presets["exclude_ports"] + found_port = None + # port check takes time so it's lowered to 100 ports + for port in range(start_port, start_port+100): + if port in exclude_ports: + continue + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + result = sock.connect_ex(("localhost", port)) + if result != 0: + found_port = port + if found_port is not None: + break + if found_port is None: + return None + os.environ["PYPE_REST_API_URL"] = "http://localhost:{}".format( + found_port + ) + return found_port