Merge branch 'feature/PYPE-548_rest_api_server' into feature/PYPE-540_muster-pools-in-renderglobals

This commit is contained in:
Ondrej Samohel 2019-10-07 16:11:26 +02:00
commit 094a60a6ef
12 changed files with 328 additions and 42 deletions

View file

@ -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):
"""

View file

@ -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'])

View file

@ -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)

View file

@ -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()

View file

@ -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"
)

View file

@ -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):

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -81,3 +81,5 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin):
).format(__name__)
instance.data['collection'] = collection
return

View file

@ -0,0 +1,5 @@
from .rest_api import RestApiServer
def tray_init(tray_widget, main_widget):
return RestApiServer()

View file

@ -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