mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-26 05:42:15 +01:00
Merge branch 'feature/PYPE-548_rest_api_server' into feature/PYPE-540_muster-pools-in-renderglobals
This commit is contained in:
commit
094a60a6ef
12 changed files with 328 additions and 42 deletions
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -81,3 +81,5 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin):
|
|||
).format(__name__)
|
||||
|
||||
instance.data['collection'] = collection
|
||||
|
||||
return
|
||||
|
|
|
|||
5
pype/services/rest_api/__init__.py
Normal file
5
pype/services/rest_api/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .rest_api import RestApiServer
|
||||
|
||||
|
||||
def tray_init(tray_widget, main_widget):
|
||||
return RestApiServer()
|
||||
237
pype/services/rest_api/rest_api.py
Normal file
237
pype/services/rest_api/rest_api.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue