mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into enhancement/OP-2848_move-loader-logic-from-avalon-to-openpype
This commit is contained in:
commit
eb49761887
44 changed files with 720 additions and 211 deletions
19
CHANGELOG.md
19
CHANGELOG.md
|
|
@ -1,16 +1,16 @@
|
|||
# Changelog
|
||||
|
||||
## [3.9.0-nightly.8](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
## [3.9.0](https://github.com/pypeclub/OpenPype/tree/3.9.0) (2022-03-14)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.2...HEAD)
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.2...3.9.0)
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
- AssetCreator: Remove the tool [\#2845](https://github.com/pypeclub/OpenPype/pull/2845)
|
||||
- Houdini: Remove unused code [\#2779](https://github.com/pypeclub/OpenPype/pull/2779)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- General: Subset name filtering in ExtractReview outpus [\#2872](https://github.com/pypeclub/OpenPype/pull/2872)
|
||||
- NewPublisher: Descriptions and Icons in creator dialog [\#2867](https://github.com/pypeclub/OpenPype/pull/2867)
|
||||
- NewPublisher: Changing task on publishing instance [\#2863](https://github.com/pypeclub/OpenPype/pull/2863)
|
||||
- TrayPublisher: Choose project widget is more clear [\#2859](https://github.com/pypeclub/OpenPype/pull/2859)
|
||||
|
|
@ -22,11 +22,12 @@
|
|||
- global: letter box calculated on output as last process [\#2812](https://github.com/pypeclub/OpenPype/pull/2812)
|
||||
- Nuke: adding Reformat to baking mov plugin [\#2811](https://github.com/pypeclub/OpenPype/pull/2811)
|
||||
- Manager: Update all to latest button [\#2805](https://github.com/pypeclub/OpenPype/pull/2805)
|
||||
- General: Set context environments for non host applications [\#2803](https://github.com/pypeclub/OpenPype/pull/2803)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- General: Missing time function [\#2877](https://github.com/pypeclub/OpenPype/pull/2877)
|
||||
- Deadline: Fix plugin name for tile assemble [\#2868](https://github.com/pypeclub/OpenPype/pull/2868)
|
||||
- Nuke: gizmo precollect fix [\#2866](https://github.com/pypeclub/OpenPype/pull/2866)
|
||||
- General: Fix hardlink for windows [\#2864](https://github.com/pypeclub/OpenPype/pull/2864)
|
||||
- General: ffmpeg was crashing on slate merge [\#2860](https://github.com/pypeclub/OpenPype/pull/2860)
|
||||
- WebPublisher: Video file was published with one too many frame [\#2858](https://github.com/pypeclub/OpenPype/pull/2858)
|
||||
|
|
@ -35,6 +36,7 @@
|
|||
- Nuke: slate resolution to input video resolution [\#2853](https://github.com/pypeclub/OpenPype/pull/2853)
|
||||
- WebPublisher: Fix username stored in DB [\#2852](https://github.com/pypeclub/OpenPype/pull/2852)
|
||||
- WebPublisher: Fix wrong number of frames for video file [\#2851](https://github.com/pypeclub/OpenPype/pull/2851)
|
||||
- Nuke: Fix family test in validate\_write\_legacy to work with stillImage [\#2847](https://github.com/pypeclub/OpenPype/pull/2847)
|
||||
- Nuke: fix multiple baking profile farm publishing [\#2842](https://github.com/pypeclub/OpenPype/pull/2842)
|
||||
- Blender: Fixed parameters for FBX export of the camera [\#2840](https://github.com/pypeclub/OpenPype/pull/2840)
|
||||
- Maya: Stop creation of reviews for Cryptomattes [\#2832](https://github.com/pypeclub/OpenPype/pull/2832)
|
||||
|
|
@ -47,23 +49,18 @@
|
|||
- Ftrack: Job killer with missing user [\#2819](https://github.com/pypeclub/OpenPype/pull/2819)
|
||||
- Nuke: Use AVALON\_APP to get value for "app" key [\#2818](https://github.com/pypeclub/OpenPype/pull/2818)
|
||||
- StandalonePublisher: use dynamic groups in subset names [\#2816](https://github.com/pypeclub/OpenPype/pull/2816)
|
||||
- Settings UI: Search case sensitivity [\#2810](https://github.com/pypeclub/OpenPype/pull/2810)
|
||||
- Flame Babypublisher optimalization [\#2806](https://github.com/pypeclub/OpenPype/pull/2806)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
- Refactor: move webserver tool to openpype [\#2876](https://github.com/pypeclub/OpenPype/pull/2876)
|
||||
- General: Move create logic from avalon to OpenPype [\#2854](https://github.com/pypeclub/OpenPype/pull/2854)
|
||||
- General: Add vendors from avalon [\#2848](https://github.com/pypeclub/OpenPype/pull/2848)
|
||||
- General: Basic event system [\#2846](https://github.com/pypeclub/OpenPype/pull/2846)
|
||||
- General: Move change context functions [\#2839](https://github.com/pypeclub/OpenPype/pull/2839)
|
||||
- Tools: Don't use avalon tools code [\#2829](https://github.com/pypeclub/OpenPype/pull/2829)
|
||||
- Move Unreal Implementation to OpenPype [\#2823](https://github.com/pypeclub/OpenPype/pull/2823)
|
||||
- General: Extract template formatting from anatomy [\#2766](https://github.com/pypeclub/OpenPype/pull/2766)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Nuke: gizmo precollect fix [\#2866](https://github.com/pypeclub/OpenPype/pull/2866)
|
||||
- Nuke: Fix family test in validate\_write\_legacy to work with stillImage [\#2847](https://github.com/pypeclub/OpenPype/pull/2847)
|
||||
|
||||
## [3.8.2](https://github.com/pypeclub/OpenPype/tree/3.8.2) (2022-02-07)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.8.2-nightly.3...3.8.2)
|
||||
|
|
|
|||
|
|
@ -5,18 +5,13 @@ import platform
|
|||
import functools
|
||||
import logging
|
||||
|
||||
from openpype.pipeline import (
|
||||
LegacyCreator,
|
||||
register_loader_plugins_path,
|
||||
deregister_loader_plugins_path,
|
||||
)
|
||||
|
||||
from .settings import get_project_settings
|
||||
from .lib import (
|
||||
Anatomy,
|
||||
filter_pyblish_plugins,
|
||||
set_plugin_attributes_from_settings,
|
||||
change_timer_to_current_context
|
||||
change_timer_to_current_context,
|
||||
register_event_callback,
|
||||
)
|
||||
|
||||
pyblish = avalon = _original_discover = None
|
||||
|
|
@ -80,6 +75,10 @@ def install():
|
|||
"""Install Pype to Avalon."""
|
||||
from pyblish.lib import MessageHandler
|
||||
from openpype.modules import load_modules
|
||||
from openpype.pipeline import (
|
||||
LegacyCreator,
|
||||
register_loader_plugins_path,
|
||||
)
|
||||
from avalon import pipeline
|
||||
|
||||
# Make sure modules are loaded
|
||||
|
|
@ -133,16 +132,18 @@ def install():
|
|||
avalon.discover = patched_discover
|
||||
pipeline.discover = patched_discover
|
||||
|
||||
avalon.on("taskChanged", _on_task_change)
|
||||
register_event_callback("taskChanged", _on_task_change)
|
||||
|
||||
|
||||
def _on_task_change(*args):
|
||||
def _on_task_change():
|
||||
change_timer_to_current_context()
|
||||
|
||||
|
||||
@import_wrapper
|
||||
def uninstall():
|
||||
"""Uninstall Pype from Avalon."""
|
||||
from openpype.pipeline import deregister_loader_plugins_path
|
||||
|
||||
log.info("Deregistering global plug-ins..")
|
||||
pyblish.deregister_plugin_path(PUBLISH_PATH)
|
||||
pyblish.deregister_discovery_filter(filter_pyblish_plugins)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from Qt import QtCore
|
|||
from openpype.tools.utils import host_tools
|
||||
|
||||
from avalon import api
|
||||
from avalon.tools.webserver.app import WebServerTool
|
||||
from openpype.tools.adobe_webserver.app import WebServerTool
|
||||
|
||||
from .ws_stub import AfterEffectsServerStub
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from openpype.pipeline import (
|
|||
deregister_loader_plugins_path,
|
||||
)
|
||||
import openpype.hosts.aftereffects
|
||||
from openpype.lib import register_event_callback
|
||||
|
||||
from .launch_logic import get_stub
|
||||
|
||||
|
|
@ -78,7 +79,7 @@ def install():
|
|||
"instanceToggled", on_pyblish_instance_toggled
|
||||
)
|
||||
|
||||
avalon.api.on("application.launched", application_launch)
|
||||
register_event_callback("application.launched", application_launch)
|
||||
|
||||
|
||||
def uninstall():
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import logging
|
|||
import attr
|
||||
|
||||
from wsrpc_aiohttp import WebSocketAsync
|
||||
from avalon.tools.webserver.app import WebServerTool
|
||||
from openpype.tools.adobe_webserver.app import WebServerTool
|
||||
|
||||
|
||||
@attr.s
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ from openpype.pipeline import (
|
|||
deregister_loader_plugins_path,
|
||||
)
|
||||
from openpype.api import Logger
|
||||
from openpype.lib import (
|
||||
register_event_callback,
|
||||
emit_event
|
||||
)
|
||||
import openpype.hosts.blender
|
||||
|
||||
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.blender.__file__))
|
||||
|
|
@ -55,8 +59,9 @@ def install():
|
|||
|
||||
lib.append_user_scripts()
|
||||
|
||||
avalon.api.on("new", on_new)
|
||||
avalon.api.on("open", on_open)
|
||||
register_event_callback("new", on_new)
|
||||
register_event_callback("open", on_open)
|
||||
|
||||
_register_callbacks()
|
||||
_register_events()
|
||||
|
||||
|
|
@ -118,22 +123,22 @@ def set_start_end_frames():
|
|||
scene.render.resolution_y = resolution_y
|
||||
|
||||
|
||||
def on_new(arg1, arg2):
|
||||
def on_new():
|
||||
set_start_end_frames()
|
||||
|
||||
|
||||
def on_open(arg1, arg2):
|
||||
def on_open():
|
||||
set_start_end_frames()
|
||||
|
||||
|
||||
@bpy.app.handlers.persistent
|
||||
def _on_save_pre(*args):
|
||||
avalon.api.emit("before_save", args)
|
||||
emit_event("before.save")
|
||||
|
||||
|
||||
@bpy.app.handlers.persistent
|
||||
def _on_save_post(*args):
|
||||
avalon.api.emit("save", args)
|
||||
emit_event("save")
|
||||
|
||||
|
||||
@bpy.app.handlers.persistent
|
||||
|
|
@ -141,9 +146,9 @@ def _on_load_post(*args):
|
|||
# Detect new file or opening an existing file
|
||||
if bpy.data.filepath:
|
||||
# Likely this was an open operation since it has a filepath
|
||||
avalon.api.emit("open", args)
|
||||
emit_event("open")
|
||||
else:
|
||||
avalon.api.emit("new", args)
|
||||
emit_event("new")
|
||||
|
||||
ops.OpenFileCacher.post_load()
|
||||
|
||||
|
|
@ -174,7 +179,7 @@ def _register_callbacks():
|
|||
log.info("Installed event handler _on_load_post...")
|
||||
|
||||
|
||||
def _on_task_changed(*args):
|
||||
def _on_task_changed():
|
||||
"""Callback for when the task in the context is changed."""
|
||||
|
||||
# TODO (jasper): Blender has no concept of projects or workspace.
|
||||
|
|
@ -191,7 +196,7 @@ def _on_task_changed(*args):
|
|||
def _register_events():
|
||||
"""Install callbacks for specific events."""
|
||||
|
||||
avalon.api.on("taskChanged", _on_task_changed)
|
||||
register_event_callback("taskChanged", _on_task_changed)
|
||||
log.info("Installed event callback for 'taskChanged'...")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import avalon.api
|
|||
from avalon.pipeline import AVALON_CONTAINER_ID
|
||||
|
||||
from openpype import lib
|
||||
from openpype.lib import register_event_callback
|
||||
from openpype.pipeline import (
|
||||
LegacyCreator,
|
||||
register_loader_plugins_path,
|
||||
|
|
@ -134,7 +135,7 @@ def check_inventory():
|
|||
harmony.send({"function": "PypeHarmony.message", "args": msg})
|
||||
|
||||
|
||||
def application_launch():
|
||||
def application_launch(event):
|
||||
"""Event that is executed after Harmony is launched."""
|
||||
# FIXME: This is breaking server <-> client communication.
|
||||
# It is now moved so it it manually called.
|
||||
|
|
@ -192,7 +193,7 @@ def install():
|
|||
"instanceToggled", on_pyblish_instance_toggled
|
||||
)
|
||||
|
||||
avalon.api.on("application.launched", application_launch)
|
||||
register_event_callback("application.launched", application_launch)
|
||||
|
||||
|
||||
def uninstall():
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from pathlib import Path
|
|||
import attr
|
||||
from avalon import api
|
||||
|
||||
from openpype.lib import get_formatted_current_time
|
||||
import openpype.lib.abstract_collect_render
|
||||
import openpype.hosts.harmony.api as harmony
|
||||
from openpype.lib.abstract_collect_render import RenderInstance
|
||||
|
|
@ -138,7 +139,7 @@ class CollectFarmRender(openpype.lib.abstract_collect_render.
|
|||
|
||||
render_instance = HarmonyRenderInstance(
|
||||
version=version,
|
||||
time=api.time(),
|
||||
time=get_formatted_current_time(),
|
||||
source=context.data["currentFile"],
|
||||
label=node.split("/")[1],
|
||||
subset=subset_name,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import os
|
||||
import hiero.core.events
|
||||
import avalon.api as avalon
|
||||
from openpype.api import Logger
|
||||
from .lib import (
|
||||
sync_avalon_data_to_workfile,
|
||||
launch_workfiles_app,
|
||||
selection_changed_timeline,
|
||||
before_project_save
|
||||
before_project_save,
|
||||
register_event_callback
|
||||
)
|
||||
from .tags import add_tags_to_workfile
|
||||
from .menu import update_menu_task_label
|
||||
|
|
@ -126,5 +126,5 @@ def register_events():
|
|||
"""
|
||||
|
||||
# if task changed then change notext of hiero
|
||||
avalon.on("taskChanged", update_menu_task_label)
|
||||
register_event_callback("taskChanged", update_menu_task_label)
|
||||
log.info("Installed event callback for 'taskChanged'..")
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ self = sys.modules[__name__]
|
|||
self._change_context_menu = None
|
||||
|
||||
|
||||
def update_menu_task_label(*args):
|
||||
def update_menu_task_label():
|
||||
"""Update the task label in Avalon menu to current session"""
|
||||
|
||||
object_name = self._change_context_menu
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ import openpype.hosts.houdini
|
|||
from openpype.hosts.houdini.api import lib
|
||||
|
||||
from openpype.lib import (
|
||||
any_outdated
|
||||
register_event_callback,
|
||||
emit_event,
|
||||
any_outdated,
|
||||
)
|
||||
|
||||
from .lib import get_asset_fps
|
||||
|
|
@ -55,11 +57,11 @@ def install():
|
|||
avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH)
|
||||
|
||||
log.info("Installing callbacks ... ")
|
||||
# avalon.on("init", on_init)
|
||||
avalon.api.before("save", before_save)
|
||||
avalon.api.on("save", on_save)
|
||||
avalon.api.on("open", on_open)
|
||||
avalon.api.on("new", on_new)
|
||||
# register_event_callback("init", on_init)
|
||||
register_event_callback("before.save", before_save)
|
||||
register_event_callback("save", on_save)
|
||||
register_event_callback("open", on_open)
|
||||
register_event_callback("new", on_new)
|
||||
|
||||
pyblish.api.register_callback(
|
||||
"instanceToggled", on_pyblish_instance_toggled
|
||||
|
|
@ -105,13 +107,13 @@ def _register_callbacks():
|
|||
|
||||
def on_file_event_callback(event):
|
||||
if event == hou.hipFileEventType.AfterLoad:
|
||||
avalon.api.emit("open", [event])
|
||||
emit_event("open")
|
||||
elif event == hou.hipFileEventType.AfterSave:
|
||||
avalon.api.emit("save", [event])
|
||||
emit_event("save")
|
||||
elif event == hou.hipFileEventType.BeforeSave:
|
||||
avalon.api.emit("before_save", [event])
|
||||
emit_event("before.save")
|
||||
elif event == hou.hipFileEventType.AfterClear:
|
||||
avalon.api.emit("new", [event])
|
||||
emit_event("new")
|
||||
|
||||
|
||||
def get_main_window():
|
||||
|
|
@ -233,11 +235,11 @@ def ls():
|
|||
yield data
|
||||
|
||||
|
||||
def before_save(*args):
|
||||
def before_save():
|
||||
return lib.validate_fps()
|
||||
|
||||
|
||||
def on_save(*args):
|
||||
def on_save():
|
||||
|
||||
log.info("Running callback on save..")
|
||||
|
||||
|
|
@ -246,7 +248,7 @@ def on_save(*args):
|
|||
lib.set_id(node, new_id, overwrite=False)
|
||||
|
||||
|
||||
def on_open(*args):
|
||||
def on_open():
|
||||
|
||||
if not hou.isUIAvailable():
|
||||
log.debug("Batch mode detected, ignoring `on_open` callbacks..")
|
||||
|
|
@ -283,7 +285,7 @@ def on_open(*args):
|
|||
dialog.show()
|
||||
|
||||
|
||||
def on_new(_):
|
||||
def on_new():
|
||||
"""Set project resolution and fps when create a new file"""
|
||||
|
||||
if hou.hipFile.isLoadingHipFile():
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ from avalon.pipeline import AVALON_CONTAINER_ID
|
|||
|
||||
import openpype.hosts.maya
|
||||
from openpype.tools.utils import host_tools
|
||||
from openpype.lib import any_outdated
|
||||
from openpype.lib import (
|
||||
any_outdated,
|
||||
register_event_callback,
|
||||
emit_event
|
||||
)
|
||||
from openpype.lib.path_tools import HostDirmap
|
||||
from openpype.pipeline import (
|
||||
LegacyCreator,
|
||||
|
|
@ -59,7 +63,7 @@ def install():
|
|||
log.info(PUBLISH_PATH)
|
||||
|
||||
log.info("Installing callbacks ... ")
|
||||
avalon.api.on("init", on_init)
|
||||
register_event_callback("init", on_init)
|
||||
|
||||
# Callbacks below are not required for headless mode, the `init` however
|
||||
# is important to load referenced Alembics correctly at rendertime.
|
||||
|
|
@ -73,12 +77,12 @@ def install():
|
|||
|
||||
menu.install()
|
||||
|
||||
avalon.api.on("save", on_save)
|
||||
avalon.api.on("open", on_open)
|
||||
avalon.api.on("new", on_new)
|
||||
avalon.api.before("save", on_before_save)
|
||||
avalon.api.on("taskChanged", on_task_changed)
|
||||
avalon.api.on("before.workfile.save", before_workfile_save)
|
||||
register_event_callback("save", on_save)
|
||||
register_event_callback("open", on_open)
|
||||
register_event_callback("new", on_new)
|
||||
register_event_callback("before.save", on_before_save)
|
||||
register_event_callback("taskChanged", on_task_changed)
|
||||
register_event_callback("workfile.save.before", before_workfile_save)
|
||||
|
||||
|
||||
def _set_project():
|
||||
|
|
@ -141,7 +145,7 @@ def _register_callbacks():
|
|||
|
||||
|
||||
def _on_maya_initialized(*args):
|
||||
avalon.api.emit("init", args)
|
||||
emit_event("init")
|
||||
|
||||
if cmds.about(batch=True):
|
||||
log.warning("Running batch mode ...")
|
||||
|
|
@ -152,15 +156,15 @@ def _on_maya_initialized(*args):
|
|||
|
||||
|
||||
def _on_scene_new(*args):
|
||||
avalon.api.emit("new", args)
|
||||
emit_event("new")
|
||||
|
||||
|
||||
def _on_scene_save(*args):
|
||||
avalon.api.emit("save", args)
|
||||
emit_event("save")
|
||||
|
||||
|
||||
def _on_scene_open(*args):
|
||||
avalon.api.emit("open", args)
|
||||
emit_event("open")
|
||||
|
||||
|
||||
def _before_scene_save(return_code, client_data):
|
||||
|
|
@ -170,7 +174,10 @@ def _before_scene_save(return_code, client_data):
|
|||
# in order to block the operation.
|
||||
OpenMaya.MScriptUtil.setBool(return_code, True)
|
||||
|
||||
avalon.api.emit("before_save", [return_code, client_data])
|
||||
emit_event(
|
||||
"before.save",
|
||||
{"return_code": return_code}
|
||||
)
|
||||
|
||||
|
||||
def uninstall():
|
||||
|
|
@ -347,7 +354,7 @@ def containerise(name,
|
|||
return container
|
||||
|
||||
|
||||
def on_init(_):
|
||||
def on_init():
|
||||
log.info("Running callback on init..")
|
||||
|
||||
def safe_deferred(fn):
|
||||
|
|
@ -388,12 +395,12 @@ def on_init(_):
|
|||
safe_deferred(override_toolbox_ui)
|
||||
|
||||
|
||||
def on_before_save(return_code, _):
|
||||
def on_before_save():
|
||||
"""Run validation for scene's FPS prior to saving"""
|
||||
return lib.validate_fps()
|
||||
|
||||
|
||||
def on_save(_):
|
||||
def on_save():
|
||||
"""Automatically add IDs to new nodes
|
||||
|
||||
Any transform of a mesh, without an existing ID, is given one
|
||||
|
|
@ -411,7 +418,7 @@ def on_save(_):
|
|||
lib.set_id(node, new_id, overwrite=False)
|
||||
|
||||
|
||||
def on_open(_):
|
||||
def on_open():
|
||||
"""On scene open let's assume the containers have changed."""
|
||||
|
||||
from Qt import QtWidgets
|
||||
|
|
@ -459,7 +466,7 @@ def on_open(_):
|
|||
dialog.show()
|
||||
|
||||
|
||||
def on_new(_):
|
||||
def on_new():
|
||||
"""Set project resolution and fps when create a new file"""
|
||||
log.info("Running callback on new..")
|
||||
with lib.suspended_refresh():
|
||||
|
|
@ -475,7 +482,7 @@ def on_new(_):
|
|||
lib.set_context_settings()
|
||||
|
||||
|
||||
def on_task_changed(*args):
|
||||
def on_task_changed():
|
||||
"""Wrapped function of app initialize and maya's on task changed"""
|
||||
# Run
|
||||
menu.update_menu_task_label()
|
||||
|
|
@ -513,7 +520,7 @@ def on_task_changed(*args):
|
|||
|
||||
|
||||
def before_workfile_save(event):
|
||||
workdir_path = event.workdir_path
|
||||
workdir_path = event["workdir_path"]
|
||||
if workdir_path:
|
||||
copy_workspace_mel(workdir_path)
|
||||
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ class ReferenceLoader(Loader):
|
|||
loader=self.__class__.__name__
|
||||
)
|
||||
loaded_containers.append(container)
|
||||
self._organize_containers([ref_node], container)
|
||||
self._organize_containers(nodes, container)
|
||||
c += 1
|
||||
namespace = None
|
||||
|
||||
|
|
@ -250,6 +250,8 @@ class ReferenceLoader(Loader):
|
|||
|
||||
self.log.warning("Ignoring file read error:\n%s", exc)
|
||||
|
||||
self._organize_containers(content, container["objectName"])
|
||||
|
||||
# Reapply alembic settings.
|
||||
if representation["name"] == "abc" and alembic_data:
|
||||
alembic_nodes = cmds.ls(
|
||||
|
|
@ -287,7 +289,6 @@ class ReferenceLoader(Loader):
|
|||
to remove from scene.
|
||||
|
||||
"""
|
||||
|
||||
from maya import cmds
|
||||
|
||||
node = container["objectName"]
|
||||
|
|
@ -321,6 +322,7 @@ class ReferenceLoader(Loader):
|
|||
@staticmethod
|
||||
def _organize_containers(nodes, container):
|
||||
# type: (list, str) -> None
|
||||
"""Put containers in loaded data to correct hierarchy."""
|
||||
for node in nodes:
|
||||
id_attr = "{}.id".format(node)
|
||||
if not cmds.attributeQuery("id", node=node, exists=True):
|
||||
|
|
|
|||
|
|
@ -121,18 +121,10 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
if family == "rig":
|
||||
self._post_process_rig(name, namespace, context, options)
|
||||
else:
|
||||
|
||||
if "translate" in options:
|
||||
cmds.setAttr(group_name + ".t", *options["translate"])
|
||||
|
||||
return new_nodes
|
||||
|
||||
def load(self, context, name=None, namespace=None, options=None):
|
||||
container = super(ReferenceLoader, self).load(
|
||||
context, name, namespace, options)
|
||||
# clean containers if present to AVALON_CONTAINERS
|
||||
self._organize_containers(self[:], container[0])
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import maya.app.renderSetup.model.renderSetup as renderSetup
|
|||
import pyblish.api
|
||||
|
||||
from avalon import api
|
||||
from openpype.lib import get_formatted_current_time
|
||||
from openpype.hosts.maya.api.lib_renderproducts import get as get_layer_render_products # noqa: E501
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
||||
|
|
@ -328,7 +329,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
|
|||
"family": "renderlayer",
|
||||
"families": ["renderlayer"],
|
||||
"asset": asset,
|
||||
"time": api.time(),
|
||||
"time": get_formatted_current_time(),
|
||||
"author": context.data["user"],
|
||||
# Add source to allow tracing back to the scene from
|
||||
# which was submitted originally
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from maya import cmds
|
|||
|
||||
import pyblish.api
|
||||
from avalon import api
|
||||
from openpype.lib import get_formatted_current_time
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
||||
|
||||
|
|
@ -117,7 +118,7 @@ class CollectVrayScene(pyblish.api.InstancePlugin):
|
|||
"family": "vrayscene_layer",
|
||||
"families": ["vrayscene_layer"],
|
||||
"asset": api.Session["AVALON_ASSET"],
|
||||
"time": api.time(),
|
||||
"time": get_formatted_current_time(),
|
||||
"author": context.data["user"],
|
||||
# Add source to allow tracing back to the scene from
|
||||
# which was submitted originally
|
||||
|
|
|
|||
|
|
@ -58,16 +58,11 @@ class ExtractMayaSceneRaw(openpype.api.Extractor):
|
|||
else:
|
||||
members = instance[:]
|
||||
|
||||
loaded_containers = None
|
||||
if set(self.add_for_families).intersection(
|
||||
set(instance.data.get("families")),
|
||||
set(instance.data.get("family").lower())):
|
||||
loaded_containers = self._add_loaded_containers(members)
|
||||
|
||||
selection = members
|
||||
if loaded_containers:
|
||||
self.log.info(loaded_containers)
|
||||
selection += loaded_containers
|
||||
if set(self.add_for_families).intersection(
|
||||
set(instance.data.get("families", []))) or \
|
||||
instance.data.get("family") in self.add_for_families:
|
||||
selection += self._get_loaded_containers(members)
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction ...")
|
||||
|
|
@ -97,15 +92,15 @@ class ExtractMayaSceneRaw(openpype.api.Extractor):
|
|||
self.log.info("Extracted instance '%s' to: %s" % (instance.name, path))
|
||||
|
||||
@staticmethod
|
||||
def _add_loaded_containers(members):
|
||||
def _get_loaded_containers(members):
|
||||
# type: (list) -> list
|
||||
refs_to_include = [
|
||||
cmds.referenceQuery(ref, referenceNode=True)
|
||||
for ref in members
|
||||
if cmds.referenceQuery(ref, isNodeReferenced=True)
|
||||
]
|
||||
refs_to_include = {
|
||||
cmds.referenceQuery(node, referenceNode=True)
|
||||
for node in members
|
||||
if cmds.referenceQuery(node, isNodeReferenced=True)
|
||||
}
|
||||
|
||||
refs_to_include = set(refs_to_include)
|
||||
members_with_refs = refs_to_include.union(members)
|
||||
|
||||
obj_sets = cmds.ls("*.id", long=True, type="objectSet", recursive=True,
|
||||
objectsOnly=True)
|
||||
|
|
@ -121,7 +116,7 @@ class ExtractMayaSceneRaw(openpype.api.Extractor):
|
|||
continue
|
||||
|
||||
set_content = set(cmds.sets(obj_set, query=True))
|
||||
if set_content.intersection(refs_to_include):
|
||||
if set_content.intersection(members_with_refs):
|
||||
loaded_containers.append(obj_set)
|
||||
|
||||
return loaded_containers
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from openpype.api import (
|
|||
BuildWorkfile,
|
||||
get_current_project_settings
|
||||
)
|
||||
from openpype.lib import register_event_callback
|
||||
from openpype.pipeline import (
|
||||
LegacyCreator,
|
||||
register_loader_plugins_path,
|
||||
|
|
@ -107,8 +108,8 @@ def install():
|
|||
avalon.api.register_plugin_path(avalon.api.InventoryAction, INVENTORY_PATH)
|
||||
|
||||
# Register Avalon event for workfiles loading.
|
||||
avalon.api.on("workio.open_file", check_inventory_versions)
|
||||
avalon.api.on("taskChanged", change_context_label)
|
||||
register_event_callback("workio.open_file", check_inventory_versions)
|
||||
register_event_callback("taskChanged", change_context_label)
|
||||
|
||||
pyblish.api.register_callback(
|
||||
"instanceToggled", on_pyblish_instance_toggled)
|
||||
|
|
@ -231,7 +232,7 @@ def _uninstall_menu():
|
|||
menu.removeItem(item.name())
|
||||
|
||||
|
||||
def change_context_label(*args):
|
||||
def change_context_label():
|
||||
menubar = nuke.menu("Nuke")
|
||||
menu = menubar.findItem(MENU_LABEL)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from openpype.api import Logger
|
|||
from openpype.tools.utils import host_tools
|
||||
|
||||
from avalon import api
|
||||
from avalon.tools.webserver.app import WebServerTool
|
||||
from openpype.tools.adobe_webserver.app import WebServerTool
|
||||
|
||||
from .ws_stub import PhotoshopServerStub
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import avalon.api
|
|||
from avalon import pipeline, io
|
||||
|
||||
from openpype.api import Logger
|
||||
from openpype.lib import register_event_callback
|
||||
from openpype.pipeline import (
|
||||
LegacyCreator,
|
||||
register_loader_plugins_path,
|
||||
|
|
@ -79,7 +80,7 @@ def install():
|
|||
"instanceToggled", on_pyblish_instance_toggled
|
||||
)
|
||||
|
||||
avalon.api.on("application.launched", on_application_launch)
|
||||
register_event_callback("application.launched", on_application_launch)
|
||||
|
||||
|
||||
def uninstall():
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@
|
|||
Stub handling connection from server to client.
|
||||
Used anywhere solution is calling client methods.
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
import attr
|
||||
from wsrpc_aiohttp import WebSocketAsync
|
||||
|
||||
from avalon.tools.webserver.app import WebServerTool
|
||||
from openpype.tools.adobe_webserver.app import WebServerTool
|
||||
|
||||
|
||||
@attr.s
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ from aiohttp_json_rpc.protocol import (
|
|||
)
|
||||
from aiohttp_json_rpc.exceptions import RpcError
|
||||
|
||||
from avalon import api
|
||||
from openpype.lib import emit_event
|
||||
from openpype.hosts.tvpaint.tvpaint_plugin import get_plugin_files_path
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -754,7 +754,7 @@ class BaseCommunicator:
|
|||
|
||||
self._on_client_connect()
|
||||
|
||||
api.emit("application.launched")
|
||||
emit_event("application.launched")
|
||||
|
||||
def _on_client_connect(self):
|
||||
self._initial_textfile_write()
|
||||
|
|
@ -938,5 +938,5 @@ class QtCommunicator(BaseCommunicator):
|
|||
|
||||
def _exit(self, *args, **kwargs):
|
||||
super()._exit(*args, **kwargs)
|
||||
api.emit("application.exit")
|
||||
emit_event("application.exit")
|
||||
self.qt_app.exit(self.exit_code)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from avalon.pipeline import AVALON_CONTAINER_ID
|
|||
|
||||
from openpype.hosts import tvpaint
|
||||
from openpype.api import get_current_project_settings
|
||||
from openpype.lib import register_event_callback
|
||||
from openpype.pipeline import (
|
||||
LegacyCreator,
|
||||
register_loader_plugins_path,
|
||||
|
|
@ -89,8 +90,8 @@ def install():
|
|||
if on_instance_toggle not in registered_callbacks:
|
||||
pyblish.api.register_callback("instanceToggled", on_instance_toggle)
|
||||
|
||||
avalon.api.on("application.launched", initial_launch)
|
||||
avalon.api.on("application.exit", application_exit)
|
||||
register_event_callback("application.launched", initial_launch)
|
||||
register_event_callback("application.exit", application_exit)
|
||||
|
||||
|
||||
def uninstall():
|
||||
|
|
|
|||
|
|
@ -14,10 +14,6 @@ PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
|
|||
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
|
||||
|
||||
|
||||
def application_launch():
|
||||
pass
|
||||
|
||||
|
||||
def install():
|
||||
print("Installing Pype config...")
|
||||
|
||||
|
|
@ -25,7 +21,6 @@ def install():
|
|||
log.info(PUBLISH_PATH)
|
||||
|
||||
io.install()
|
||||
avalon.on("application.launched", application_launch)
|
||||
|
||||
|
||||
def uninstall():
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ sys.path.insert(0, python_version_dir)
|
|||
site.addsitedir(python_version_dir)
|
||||
|
||||
|
||||
from .events import (
|
||||
emit_event,
|
||||
register_event_callback
|
||||
)
|
||||
|
||||
from .vendor_bin_utils import (
|
||||
find_executable,
|
||||
get_vendor_bin_path,
|
||||
|
|
@ -24,6 +29,7 @@ from .vendor_bin_utils import (
|
|||
ffprobe_streams,
|
||||
is_oiio_supported
|
||||
)
|
||||
|
||||
from .env_tools import (
|
||||
env_value_to_bool,
|
||||
get_paths_from_environ,
|
||||
|
|
@ -63,7 +69,10 @@ from .anatomy import (
|
|||
Anatomy
|
||||
)
|
||||
|
||||
from .config import get_datetime_data
|
||||
from .config import (
|
||||
get_datetime_data,
|
||||
get_formatted_current_time
|
||||
)
|
||||
|
||||
from .python_module_tools import (
|
||||
import_filepath,
|
||||
|
|
@ -194,6 +203,9 @@ from .openpype_version import (
|
|||
terminal = Terminal
|
||||
|
||||
__all__ = [
|
||||
"emit_event",
|
||||
"register_event_callback",
|
||||
|
||||
"find_executable",
|
||||
"get_openpype_execute_args",
|
||||
"get_pype_execute_args",
|
||||
|
|
@ -309,6 +321,7 @@ __all__ = [
|
|||
"Anatomy",
|
||||
|
||||
"get_datetime_data",
|
||||
"get_formatted_current_time",
|
||||
|
||||
"PypeLogger",
|
||||
"get_default_components",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class RenderInstance(object):
|
|||
|
||||
# metadata
|
||||
version = attr.ib() # instance version
|
||||
time = attr.ib() # time of instance creation (avalon.api.time())
|
||||
time = attr.ib() # time of instance creation (get_formatted_current_time)
|
||||
source = attr.ib() # path to source scene file
|
||||
label = attr.ib() # label to show in GUI
|
||||
subset = attr.ib() # subset name
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from openpype.settings import (
|
|||
)
|
||||
from .anatomy import Anatomy
|
||||
from .profiles_filtering import filter_profiles
|
||||
from .events import emit_event
|
||||
|
||||
# avalon module is not imported at the top
|
||||
# - may not be in path at the time of pype.lib initialization
|
||||
|
|
@ -779,7 +780,6 @@ def update_current_task(task=None, asset=None, app=None, template_key=None):
|
|||
|
||||
"""
|
||||
import avalon.api
|
||||
from avalon.pipeline import emit
|
||||
|
||||
changes = compute_session_changes(
|
||||
avalon.api.Session,
|
||||
|
|
@ -799,7 +799,7 @@ def update_current_task(task=None, asset=None, app=None, template_key=None):
|
|||
os.environ[key] = value
|
||||
|
||||
# Emit session change
|
||||
emit("taskChanged", changes.copy())
|
||||
emit_event("taskChanged", changes.copy())
|
||||
|
||||
return changes
|
||||
|
||||
|
|
|
|||
|
|
@ -74,3 +74,9 @@ def get_datetime_data(datetime_obj=None):
|
|||
"S": str(int(seconds)),
|
||||
"SS": str(seconds),
|
||||
}
|
||||
|
||||
|
||||
def get_formatted_current_time():
|
||||
return datetime.datetime.now().strftime(
|
||||
"%Y%m%dT%H%M%SZ"
|
||||
)
|
||||
|
|
|
|||
268
openpype/lib/events.py
Normal file
268
openpype/lib/events.py
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
"""Events holding data about specific event."""
|
||||
import os
|
||||
import re
|
||||
import inspect
|
||||
import logging
|
||||
import weakref
|
||||
from uuid import uuid4
|
||||
try:
|
||||
from weakref import WeakMethod
|
||||
except Exception:
|
||||
from openpype.lib.python_2_comp import WeakMethod
|
||||
|
||||
|
||||
class EventCallback(object):
|
||||
"""Callback registered to a topic.
|
||||
|
||||
The callback function is registered to a topic. Topic is a string which
|
||||
may contain '*' that will be handled as "any characters".
|
||||
|
||||
# Examples:
|
||||
- "workfile.save" Callback will be triggered if the event topic is
|
||||
exactly "workfile.save" .
|
||||
- "workfile.*" Callback will be triggered an event topic starts with
|
||||
"workfile." so "workfile.save" and "workfile.open"
|
||||
will trigger the callback.
|
||||
- "*" Callback will listen to all events.
|
||||
|
||||
Callback can be function or method. In both cases it should expect one
|
||||
or none arguments. When 1 argument is expected then the processed 'Event'
|
||||
object is passed in.
|
||||
|
||||
The registered callbacks don't keep function in memory so it is not
|
||||
possible to store lambda function as callback.
|
||||
|
||||
Args:
|
||||
topic(str): Topic which will be listened.
|
||||
func(func): Callback to a topic.
|
||||
|
||||
Raises:
|
||||
TypeError: When passed function is not a callable object.
|
||||
"""
|
||||
|
||||
def __init__(self, topic, func):
|
||||
self._log = None
|
||||
self._topic = topic
|
||||
# Replace '*' with any character regex and escape rest of text
|
||||
# - when callback is registered for '*' topic it will receive all
|
||||
# events
|
||||
# - it is possible to register to a partial topis 'my.event.*'
|
||||
# - it will receive all matching event topics
|
||||
# e.g. 'my.event.start' and 'my.event.end'
|
||||
topic_regex_str = "^{}$".format(
|
||||
".+".join(
|
||||
re.escape(part)
|
||||
for part in topic.split("*")
|
||||
)
|
||||
)
|
||||
topic_regex = re.compile(topic_regex_str)
|
||||
self._topic_regex = topic_regex
|
||||
|
||||
# Convert callback into references
|
||||
# - deleted functions won't cause crashes
|
||||
if inspect.ismethod(func):
|
||||
func_ref = WeakMethod(func)
|
||||
elif callable(func):
|
||||
func_ref = weakref.ref(func)
|
||||
else:
|
||||
raise TypeError((
|
||||
"Registered callback is not callable. \"{}\""
|
||||
).format(str(func)))
|
||||
|
||||
# Collect additional data about function
|
||||
# - name
|
||||
# - path
|
||||
# - if expect argument or not
|
||||
func_name = func.__name__
|
||||
func_path = os.path.abspath(inspect.getfile(func))
|
||||
if hasattr(inspect, "signature"):
|
||||
sig = inspect.signature(func)
|
||||
expect_args = len(sig.parameters) > 0
|
||||
else:
|
||||
expect_args = len(inspect.getargspec(func)[0]) > 0
|
||||
|
||||
self._func_ref = func_ref
|
||||
self._func_name = func_name
|
||||
self._func_path = func_path
|
||||
self._expect_args = expect_args
|
||||
self._ref_valid = func_ref is not None
|
||||
self._enabled = True
|
||||
|
||||
def __repr__(self):
|
||||
return "< {} - {} > {}".format(
|
||||
self.__class__.__name__, self._func_name, self._func_path
|
||||
)
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
if self._log is None:
|
||||
self._log = logging.getLogger(self.__class__.__name__)
|
||||
return self._log
|
||||
|
||||
@property
|
||||
def is_ref_valid(self):
|
||||
return self._ref_valid
|
||||
|
||||
def validate_ref(self):
|
||||
if not self._ref_valid:
|
||||
return
|
||||
|
||||
callback = self._func_ref()
|
||||
if not callback:
|
||||
self._ref_valid = False
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
"""Is callback enabled."""
|
||||
return self._enabled
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
"""Change if callback is enabled."""
|
||||
self._enabled = enabled
|
||||
|
||||
def deregister(self):
|
||||
"""Calling this funcion will cause that callback will be removed."""
|
||||
# Fake reference
|
||||
self._ref_valid = False
|
||||
|
||||
def topic_matches(self, topic):
|
||||
"""Check if event topic matches callback's topic."""
|
||||
return self._topic_regex.match(topic)
|
||||
|
||||
def process_event(self, event):
|
||||
"""Process event.
|
||||
|
||||
Args:
|
||||
event(Event): Event that was triggered.
|
||||
"""
|
||||
|
||||
# Skip if callback is not enabled or has invalid reference
|
||||
if not self._ref_valid or not self._enabled:
|
||||
return
|
||||
|
||||
# Get reference
|
||||
callback = self._func_ref()
|
||||
# Check if reference is valid or callback's topic matches the event
|
||||
if not callback:
|
||||
# Change state if is invalid so the callback is removed
|
||||
self._ref_valid = False
|
||||
|
||||
elif self.topic_matches(event.topic):
|
||||
# Try execute callback
|
||||
try:
|
||||
if self._expect_args:
|
||||
callback(event)
|
||||
else:
|
||||
callback()
|
||||
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Failed to execute event callback {}".format(
|
||||
str(repr(self))
|
||||
),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
|
||||
# Inherit from 'object' for Python 2 hosts
|
||||
class Event(object):
|
||||
"""Base event object.
|
||||
|
||||
Can be used for any event because is not specific. Only required argument
|
||||
is topic which defines why event is happening and may be used for
|
||||
filtering.
|
||||
|
||||
Arg:
|
||||
topic (str): Identifier of event.
|
||||
data (Any): Data specific for event. Dictionary is recommended.
|
||||
source (str): Identifier of source.
|
||||
"""
|
||||
_data = {}
|
||||
|
||||
def __init__(self, topic, data=None, source=None):
|
||||
self._id = str(uuid4())
|
||||
self._topic = topic
|
||||
if data is None:
|
||||
data = {}
|
||||
self._data = data
|
||||
self._source = source
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._data[key]
|
||||
|
||||
def get(self, key, *args, **kwargs):
|
||||
return self._data.get(key, *args, **kwargs)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
return self._source
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def topic(self):
|
||||
return self._topic
|
||||
|
||||
def emit(self):
|
||||
"""Emit event and trigger callbacks."""
|
||||
StoredCallbacks.emit_event(self)
|
||||
|
||||
|
||||
class StoredCallbacks:
|
||||
_registered_callbacks = []
|
||||
|
||||
@classmethod
|
||||
def add_callback(cls, topic, callback):
|
||||
callback = EventCallback(topic, callback)
|
||||
cls._registered_callbacks.append(callback)
|
||||
return callback
|
||||
|
||||
@classmethod
|
||||
def emit_event(cls, event):
|
||||
invalid_callbacks = []
|
||||
for callback in cls._registered_callbacks:
|
||||
callback.process_event(event)
|
||||
if not callback.is_ref_valid:
|
||||
invalid_callbacks.append(callback)
|
||||
|
||||
for callback in invalid_callbacks:
|
||||
cls._registered_callbacks.remove(callback)
|
||||
|
||||
|
||||
def register_event_callback(topic, callback):
|
||||
"""Add callback that will be executed on specific topic.
|
||||
|
||||
Args:
|
||||
topic(str): Topic on which will callback be triggered.
|
||||
callback(function): Callback that will be triggered when a topic
|
||||
is triggered. Callback should expect none or 1 argument where
|
||||
`Event` object is passed.
|
||||
|
||||
Returns:
|
||||
EventCallback: Object wrapping the callback. It can be used to
|
||||
enable/disable listening to a topic or remove the callback from
|
||||
the topic completely.
|
||||
"""
|
||||
return StoredCallbacks.add_callback(topic, callback)
|
||||
|
||||
|
||||
def emit_event(topic, data=None, source=None):
|
||||
"""Emit event with topic and data.
|
||||
|
||||
Arg:
|
||||
topic(str): Event's topic.
|
||||
data(dict): Event's additional data. Optional.
|
||||
source(str): Who emitted the topic. Optional.
|
||||
|
||||
Returns:
|
||||
Event: Object of event that was emitted.
|
||||
"""
|
||||
event = Event(topic, data, source)
|
||||
event.emit()
|
||||
return event
|
||||
|
|
@ -1,8 +1,3 @@
|
|||
from .events import (
|
||||
BaseEvent,
|
||||
BeforeWorkfileSave
|
||||
)
|
||||
|
||||
from .attribute_definitions import (
|
||||
AbtractAttrDef,
|
||||
|
||||
|
|
@ -20,9 +15,6 @@ from .attribute_definitions import (
|
|||
|
||||
|
||||
__all__ = (
|
||||
"BaseEvent",
|
||||
"BeforeWorkfileSave",
|
||||
|
||||
"AbtractAttrDef",
|
||||
|
||||
"UIDef",
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
"""Events holding data about specific event."""
|
||||
|
||||
|
||||
# Inherit from 'object' for Python 2 hosts
|
||||
class BaseEvent(object):
|
||||
"""Base event object.
|
||||
|
||||
Can be used to anything because data are not much specific. Only required
|
||||
argument is topic which defines why event is happening and may be used for
|
||||
filtering.
|
||||
|
||||
Arg:
|
||||
topic (str): Identifier of event.
|
||||
data (Any): Data specific for event. Dictionary is recommended.
|
||||
"""
|
||||
_data = {}
|
||||
|
||||
def __init__(self, topic, data=None):
|
||||
self._topic = topic
|
||||
if data is None:
|
||||
data = {}
|
||||
self._data = data
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def topic(self):
|
||||
return self._topic
|
||||
|
||||
@classmethod
|
||||
def emit(cls, *args, **kwargs):
|
||||
"""Create object of event and emit.
|
||||
|
||||
Args:
|
||||
Same args as '__init__' expects which may be class specific.
|
||||
"""
|
||||
from avalon import pipeline
|
||||
|
||||
obj = cls(*args, **kwargs)
|
||||
pipeline.emit(obj.topic, [obj])
|
||||
return obj
|
||||
|
||||
|
||||
class BeforeWorkfileSave(BaseEvent):
|
||||
"""Before workfile changes event data."""
|
||||
def __init__(self, filename, workdir):
|
||||
super(BeforeWorkfileSave, self).__init__("before.workfile.save")
|
||||
self.filename = filename
|
||||
self.workdir_path = workdir
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import pyblish.api
|
||||
from avalon import api
|
||||
from openpype.lib import get_formatted_current_time
|
||||
|
||||
|
||||
class CollectTime(pyblish.api.ContextPlugin):
|
||||
"""Store global time at the time of publish"""
|
||||
|
||||
label = "Collect Current Time"
|
||||
order = pyblish.api.CollectorOrder
|
||||
order = pyblish.api.CollectorOrder - 0.499
|
||||
|
||||
def process(self, context):
|
||||
context.data["time"] = api.time()
|
||||
context.data["time"] = get_formatted_current_time()
|
||||
|
|
|
|||
|
|
@ -103,9 +103,10 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
|
||||
self.log.debug("Matching profile: \"{}\"".format(json.dumps(profile)))
|
||||
|
||||
subset_name = instance.data.get("subset")
|
||||
instance_families = self.families_from_instance(instance)
|
||||
filtered_outputs = self.filter_outputs_by_families(
|
||||
profile, instance_families
|
||||
filtered_outputs = self.filter_output_defs(
|
||||
profile, subset_name, instance_families
|
||||
)
|
||||
# Store `filename_suffix` to save arguments
|
||||
profile_outputs = []
|
||||
|
|
@ -1651,7 +1652,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
return True
|
||||
return False
|
||||
|
||||
def filter_outputs_by_families(self, profile, families):
|
||||
def filter_output_defs(self, profile, subset_name, families):
|
||||
"""Return outputs matching input instance families.
|
||||
|
||||
Output definitions without families filter are marked as valid.
|
||||
|
|
@ -1684,6 +1685,24 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
if not self.families_filter_validation(families, families_filters):
|
||||
continue
|
||||
|
||||
# Subsets name filters
|
||||
subset_filters = [
|
||||
subset_filter
|
||||
for subset_filter in output_filters.get("subsets", [])
|
||||
# Skip empty strings
|
||||
if subset_filter
|
||||
]
|
||||
if subset_name and subset_filters:
|
||||
match = False
|
||||
for subset_filter in subset_filters:
|
||||
compiled = re.compile(subset_filter)
|
||||
if compiled.search(subset_name):
|
||||
match = True
|
||||
break
|
||||
|
||||
if not match:
|
||||
continue
|
||||
|
||||
filtered_outputs[filename_suffix] = output_def
|
||||
|
||||
return filtered_outputs
|
||||
|
|
|
|||
|
|
@ -87,7 +87,8 @@
|
|||
"render",
|
||||
"review",
|
||||
"ftrack"
|
||||
]
|
||||
],
|
||||
"subsets": []
|
||||
},
|
||||
"overscan_crop": "",
|
||||
"overscan_color": [
|
||||
|
|
|
|||
|
|
@ -291,6 +291,15 @@
|
|||
"label": "Families",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"key": "subsets",
|
||||
"label": "Subsets",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
237
openpype/tools/adobe_webserver/app.py
Normal file
237
openpype/tools/adobe_webserver/app.py
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
"""This Webserver tool is python 3 specific.
|
||||
|
||||
Don't import directly to avalon.tools or implementation of Python 2 hosts
|
||||
would break.
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
import urllib
|
||||
import threading
|
||||
import asyncio
|
||||
import socket
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from wsrpc_aiohttp import (
|
||||
WSRPCClient
|
||||
)
|
||||
|
||||
from avalon import api
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebServerTool:
|
||||
"""
|
||||
Basic POC implementation of asychronic websocket RPC server.
|
||||
Uses class in external_app_1.py to mimic implementation for single
|
||||
external application.
|
||||
'test_client' folder contains two test implementations of client
|
||||
"""
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
WebServerTool._instance = self
|
||||
|
||||
self.client = None
|
||||
self.handlers = {}
|
||||
self.on_stop_callbacks = []
|
||||
|
||||
port = None
|
||||
host_name = "localhost"
|
||||
websocket_url = os.getenv("WEBSOCKET_URL")
|
||||
if websocket_url:
|
||||
parsed = urllib.parse.urlparse(websocket_url)
|
||||
port = parsed.port
|
||||
host_name = parsed.netloc.split(":")[0]
|
||||
if not port:
|
||||
port = 8098 # fallback
|
||||
|
||||
self.port = port
|
||||
self.host_name = host_name
|
||||
|
||||
self.app = web.Application()
|
||||
|
||||
# add route with multiple methods for single "external app"
|
||||
self.webserver_thread = WebServerThread(self, self.port)
|
||||
|
||||
def add_route(self, *args, **kwargs):
|
||||
self.app.router.add_route(*args, **kwargs)
|
||||
|
||||
def add_static(self, *args, **kwargs):
|
||||
self.app.router.add_static(*args, **kwargs)
|
||||
|
||||
def start_server(self):
|
||||
if self.webserver_thread and not self.webserver_thread.is_alive():
|
||||
self.webserver_thread.start()
|
||||
|
||||
def stop_server(self):
|
||||
self.stop()
|
||||
|
||||
async def send_context_change(self, host):
|
||||
"""
|
||||
Calls running webserver to inform about context change
|
||||
|
||||
Used when new PS/AE should be triggered,
|
||||
but one already running, without
|
||||
this publish would point to old context.
|
||||
"""
|
||||
client = WSRPCClient(os.getenv("WEBSOCKET_URL"),
|
||||
loop=asyncio.get_event_loop())
|
||||
await client.connect()
|
||||
|
||||
project = api.Session["AVALON_PROJECT"]
|
||||
asset = api.Session["AVALON_ASSET"]
|
||||
task = api.Session["AVALON_TASK"]
|
||||
log.info("Sending context change to {}-{}-{}".format(project,
|
||||
asset,
|
||||
task))
|
||||
|
||||
await client.call('{}.set_context'.format(host),
|
||||
project=project, asset=asset, task=task)
|
||||
await client.close()
|
||||
|
||||
def port_occupied(self, host_name, port):
|
||||
"""
|
||||
Check if 'url' is already occupied.
|
||||
|
||||
This could mean, that app is already running and we are trying open it
|
||||
again. In that case, use existing running webserver.
|
||||
Check here is easier than capturing exception from thread.
|
||||
"""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
result = True
|
||||
try:
|
||||
sock.bind((host_name, port))
|
||||
result = False
|
||||
except:
|
||||
print("Port is in use")
|
||||
|
||||
return result
|
||||
|
||||
def call(self, func):
|
||||
log.debug("websocket.call {}".format(func))
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
func,
|
||||
self.webserver_thread.loop
|
||||
)
|
||||
result = future.result()
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_instance():
|
||||
if WebServerTool._instance is None:
|
||||
WebServerTool()
|
||||
return WebServerTool._instance
|
||||
|
||||
@property
|
||||
def is_running(self):
|
||||
if not self.webserver_thread:
|
||||
return False
|
||||
return self.webserver_thread.is_running
|
||||
|
||||
def stop(self):
|
||||
if not self.is_running:
|
||||
return
|
||||
try:
|
||||
log.debug("Stopping websocket server")
|
||||
self.webserver_thread.is_running = False
|
||||
self.webserver_thread.stop()
|
||||
except Exception:
|
||||
log.warning(
|
||||
"Error has happened during Killing websocket server",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
def thread_stopped(self):
|
||||
for callback in self.on_stop_callbacks:
|
||||
callback()
|
||||
|
||||
|
||||
class WebServerThread(threading.Thread):
|
||||
""" Listener for websocket rpc requests.
|
||||
|
||||
It would be probably better to "attach" this to main thread (as for
|
||||
example Harmony needs to run something on main thread), but currently
|
||||
it creates separate thread and separate asyncio event loop
|
||||
"""
|
||||
def __init__(self, module, port):
|
||||
super(WebServerThread, self).__init__()
|
||||
|
||||
self.is_running = False
|
||||
self.port = port
|
||||
self.module = module
|
||||
self.loop = None
|
||||
self.runner = None
|
||||
self.site = None
|
||||
self.tasks = []
|
||||
|
||||
def run(self):
|
||||
self.is_running = True
|
||||
|
||||
try:
|
||||
log.info("Starting web server")
|
||||
self.loop = asyncio.new_event_loop() # create new loop for thread
|
||||
asyncio.set_event_loop(self.loop)
|
||||
|
||||
self.loop.run_until_complete(self.start_server())
|
||||
|
||||
websocket_url = "ws://localhost:{}/ws".format(self.port)
|
||||
|
||||
log.debug(
|
||||
"Running Websocket server on URL: \"{}\"".format(websocket_url)
|
||||
)
|
||||
|
||||
asyncio.ensure_future(self.check_shutdown(), loop=self.loop)
|
||||
self.loop.run_forever()
|
||||
except Exception:
|
||||
self.is_running = False
|
||||
log.warning(
|
||||
"Websocket Server service has failed", exc_info=True
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
self.loop.close() # optional
|
||||
|
||||
self.is_running = False
|
||||
self.module.thread_stopped()
|
||||
log.info("Websocket server stopped")
|
||||
|
||||
async def start_server(self):
|
||||
""" Starts runner and TCPsite """
|
||||
self.runner = web.AppRunner(self.module.app)
|
||||
await self.runner.setup()
|
||||
self.site = web.TCPSite(self.runner, 'localhost', self.port)
|
||||
await self.site.start()
|
||||
|
||||
def stop(self):
|
||||
"""Sets is_running flag to false, 'check_shutdown' shuts server down"""
|
||||
self.is_running = False
|
||||
|
||||
async def check_shutdown(self):
|
||||
""" Future that is running and checks if server should be running
|
||||
periodically.
|
||||
"""
|
||||
while self.is_running:
|
||||
while self.tasks:
|
||||
task = self.tasks.pop(0)
|
||||
log.debug("waiting for task {}".format(task))
|
||||
await task
|
||||
log.debug("returned value {}".format(task.result))
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
log.debug("Starting shutdown")
|
||||
await self.site.stop()
|
||||
log.debug("Site stopped")
|
||||
await self.runner.cleanup()
|
||||
log.debug("Runner stopped")
|
||||
tasks = [task for task in asyncio.all_tasks() if
|
||||
task is not asyncio.current_task()]
|
||||
list(map(lambda task: task.cancel(), tasks)) # cancel all the tasks
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
log.debug(f'Finished awaiting cancelled tasks, results: {results}...')
|
||||
await self.loop.shutdown_asyncgens()
|
||||
# to really make sure everything else has time to stop
|
||||
await asyncio.sleep(0.07)
|
||||
self.loop.stop()
|
||||
12
openpype/tools/adobe_webserver/readme.txt
Normal file
12
openpype/tools/adobe_webserver/readme.txt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
Adobe webserver
|
||||
---------------
|
||||
Aiohttp (Asyncio) based websocket server used for communication with host
|
||||
applications, currently only for Adobe (but could be used for any non python
|
||||
DCC which has websocket client).
|
||||
|
||||
This webserver is started in spawned Python process that opens DCC during
|
||||
its launch, waits for connection from DCC and handles communication going
|
||||
forward. Server is closed before Python process is killed.
|
||||
|
||||
(Different from `openpype/modules/webserver` as that one is running in Tray,
|
||||
this one is running in spawn Python process.)
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import sys
|
||||
|
||||
from Qt import QtWidgets, QtCore
|
||||
from avalon import api, io, pipeline
|
||||
from avalon import api, io
|
||||
|
||||
from openpype import style
|
||||
from openpype.lib import register_event_callback
|
||||
from openpype.tools.utils import (
|
||||
lib,
|
||||
PlaceholderLineEdit
|
||||
|
|
@ -25,17 +26,6 @@ module = sys.modules[__name__]
|
|||
module.window = None
|
||||
|
||||
|
||||
# Register callback on task change
|
||||
# - callback can't be defined in Window as it is weak reference callback
|
||||
# so `WeakSet` will remove it immediately
|
||||
def on_context_task_change(*args, **kwargs):
|
||||
if module.window:
|
||||
module.window.on_context_task_change(*args, **kwargs)
|
||||
|
||||
|
||||
pipeline.on("taskChanged", on_context_task_change)
|
||||
|
||||
|
||||
class LoaderWindow(QtWidgets.QDialog):
|
||||
"""Asset loader interface"""
|
||||
|
||||
|
|
@ -194,6 +184,8 @@ class LoaderWindow(QtWidgets.QDialog):
|
|||
|
||||
self._first_show = True
|
||||
|
||||
register_event_callback("taskChanged", self.on_context_task_change)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(LoaderWindow, self).resizeEvent(event)
|
||||
self._overlay_frame.resize(self.size())
|
||||
|
|
|
|||
|
|
@ -9,10 +9,9 @@ import datetime
|
|||
|
||||
import Qt
|
||||
from Qt import QtWidgets, QtCore
|
||||
from avalon import io, api, pipeline
|
||||
from avalon import io, api
|
||||
|
||||
from openpype import style
|
||||
from openpype.pipeline.lib import BeforeWorkfileSave
|
||||
from openpype.tools.utils.lib import (
|
||||
qt_app_context
|
||||
)
|
||||
|
|
@ -21,6 +20,7 @@ from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget
|
|||
from openpype.tools.utils.tasks_widget import TasksWidget
|
||||
from openpype.tools.utils.delegates import PrettyTimeDelegate
|
||||
from openpype.lib import (
|
||||
emit_event,
|
||||
Anatomy,
|
||||
get_workfile_doc,
|
||||
create_workfile_doc,
|
||||
|
|
@ -823,7 +823,11 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
return
|
||||
|
||||
# Trigger before save event
|
||||
BeforeWorkfileSave.emit(work_filename, self._workdir_path)
|
||||
emit_event(
|
||||
"workfile.save.before",
|
||||
{"filename": work_filename, "workdir_path": self._workdir_path},
|
||||
source="workfiles.tool"
|
||||
)
|
||||
|
||||
# Make sure workfiles root is updated
|
||||
# - this triggers 'workio.work_root(...)' which may change value of
|
||||
|
|
@ -853,7 +857,11 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
api.Session["AVALON_PROJECT"]
|
||||
)
|
||||
# Trigger after save events
|
||||
pipeline.emit("after.workfile.save", [filepath])
|
||||
emit_event(
|
||||
"workfile.save.after",
|
||||
{"filename": work_filename, "workdir_path": self._workdir_path},
|
||||
source="workfiles.tool"
|
||||
)
|
||||
|
||||
self.workfile_created.emit(filepath)
|
||||
# Refresh files model
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.9.0-nightly.8"
|
||||
__version__ = "3.9.0"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "OpenPype"
|
||||
version = "3.9.0-nightly.8" # OpenPype
|
||||
version = "3.9.0" # OpenPype
|
||||
description = "Open VFX and Animation pipeline with support."
|
||||
authors = ["OpenPype Team <info@openpype.io>"]
|
||||
license = "MIT License"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit ffe9e910f1f382e222d457d8e4a8426c41ed43ae
|
||||
Subproject commit 7753d15507afadc143b7d49db8fcfaa6a29fed91
|
||||
|
|
@ -15,7 +15,7 @@ sidebar_label: AfterEffects
|
|||
|
||||
## Setup
|
||||
|
||||
To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select AfterEffects in menu. Then go to `{path to pype}/repos/avalon-core/avalon/aftereffects/extension.zxp`.
|
||||
To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select AfterEffects in menu. Then go to `{path to pype}hosts/aftereffects/api/extension.zxp`.
|
||||
|
||||
Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself.
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ sidebar_label: Photoshop
|
|||
|
||||
## Setup
|
||||
|
||||
To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select Photoshop in menu. Then go to `{path to pype}/repos/avalon-core/avalon/photoshop/extension.zxp`. Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself.
|
||||
To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select Photoshop in menu. Then go to `{path to pype}hosts/photoshop/api/extension.zxp`. Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself.
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue