Merge branch 'develop' into enhancement/AY-5539_define-creators-per-task

This commit is contained in:
Petr Kalis 2024-06-07 10:18:21 +02:00 committed by GitHub
commit 4640f26ebd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
707 changed files with 1829 additions and 1261 deletions

View file

@ -8,7 +8,6 @@ import inspect
import logging
import threading
import collections
from uuid import uuid4
from abc import ABCMeta, abstractmethod
@ -50,10 +49,15 @@ IGNORED_MODULES_IN_AYON = set()
# When addon was moved from ayon-core codebase
# - this is used to log the missing addon
MOVED_ADDON_MILESTONE_VERSIONS = {
"aftereffects": VersionInfo(0, 2, 0),
"applications": VersionInfo(0, 2, 0),
"blender": VersionInfo(0, 2, 0),
"celaction": VersionInfo(0, 2, 0),
"clockify": VersionInfo(0, 2, 0),
"flame": VersionInfo(0, 2, 0),
"fusion": VersionInfo(0, 2, 0),
"harmony": VersionInfo(0, 2, 0),
"hiero": VersionInfo(0, 2, 0),
"max": VersionInfo(0, 2, 0),
"photoshop": VersionInfo(0, 2, 0),
"traypublisher": VersionInfo(0, 2, 0),
@ -61,7 +65,9 @@ MOVED_ADDON_MILESTONE_VERSIONS = {
"maya": VersionInfo(0, 2, 0),
"nuke": VersionInfo(0, 2, 0),
"resolve": VersionInfo(0, 2, 0),
"royalrender": VersionInfo(0, 2, 0),
"substancepainter": VersionInfo(0, 2, 0),
"houdini": VersionInfo(0, 3, 0),
}
@ -550,6 +556,9 @@ class AYONAddon(object):
enabled = True
_id = None
# Temporary variable for 'version' property
_missing_version_warned = False
def __init__(self, manager, settings):
self.manager = manager
@ -580,6 +589,26 @@ class AYONAddon(object):
pass
@property
def version(self):
"""Addon version.
Todo:
Should be abstract property (required). Introduced in
ayon-core 0.3.3 .
Returns:
str: Addon version as semver compatible string.
"""
if not self.__class__._missing_version_warned:
self.__class__._missing_version_warned = True
print(
f"DEV WARNING: Addon '{self.name}' does not have"
f" defined version."
)
return "0.0.0"
def initialize(self, settings):
"""Initialization of addon attributes.
@ -695,6 +724,30 @@ class OpenPypeAddOn(OpenPypeModule):
enabled = True
class _AddonReportInfo:
def __init__(
self, class_name, name, version, report_value_by_label
):
self.class_name = class_name
self.name = name
self.version = version
self.report_value_by_label = report_value_by_label
@classmethod
def from_addon(cls, addon, report):
class_name = addon.__class__.__name__
report_value_by_label = {
label: reported.get(class_name)
for label, reported in report.items()
}
return cls(
addon.__class__.__name__,
addon.name,
addon.version,
report_value_by_label
)
class AddonsManager:
"""Manager of addons that helps to load and prepare them to work.
@ -871,10 +924,6 @@ class AddonsManager:
name_alias = getattr(addon, "openpype_alias", None)
if name_alias:
aliased_names.append((name_alias, addon))
enabled_str = "X"
if not addon.enabled:
enabled_str = " "
self.log.debug("[{}] {}".format(enabled_str, name))
now = time.time()
report[addon.__class__.__name__] = now - prev_start_time
@ -886,6 +935,13 @@ class AddonsManager:
exc_info=True
)
for addon_name in sorted(self._addons_by_name.keys()):
addon = self._addons_by_name[addon_name]
enabled_str = "X" if addon.enabled else " "
self.log.debug(
f"[{enabled_str}] {addon.name} ({addon.version})"
)
for item in aliased_names:
name_alias, addon = item
if name_alias not in self._addons_by_name:
@ -1174,39 +1230,55 @@ class AddonsManager:
available_col_names |= set(addon_names.keys())
# Prepare ordered dictionary for columns
cols = collections.OrderedDict()
# Add addon names to first columnt
cols["Addon name"] = list(sorted(
addon.__class__.__name__
addons_info = [
_AddonReportInfo.from_addon(addon, self._report)
for addon in self.addons
if addon.__class__.__name__ in available_col_names
))
]
addons_info.sort(key=lambda x: x.name)
addon_name_rows = [
addon_info.name
for addon_info in addons_info
]
addon_version_rows = [
addon_info.version
for addon_info in addons_info
]
# Add total key (as last addon)
cols["Addon name"].append(self._report_total_key)
addon_name_rows.append(self._report_total_key)
addon_version_rows.append(f"({len(addons_info)})")
cols = collections.OrderedDict()
# Add addon names to first columnt
cols["Addon name"] = addon_name_rows
cols["Version"] = addon_version_rows
# Add columns from report
total_by_addon = {
row: 0
for row in addon_name_rows
}
for label in self._report.keys():
cols[label] = []
total_addon_times = {}
for addon_name in cols["Addon name"]:
total_addon_times[addon_name] = 0
for label, reported in self._report.items():
for addon_name in cols["Addon name"]:
col_time = reported.get(addon_name)
if col_time is None:
cols[label].append("N/A")
rows = []
col_total = 0
for addon_info in addons_info:
value = addon_info.report_value_by_label.get(label)
if value is None:
rows.append("N/A")
continue
cols[label].append("{:.3f}".format(col_time))
total_addon_times[addon_name] += col_time
rows.append("{:.3f}".format(value))
total_by_addon[addon_info.name] += value
col_total += value
total_by_addon[self._report_total_key] += col_total
rows.append("{:.3f}".format(col_total))
cols[label] = rows
# Add to also total column that should sum the row
cols[self._report_total_key] = []
for addon_name in cols["Addon name"]:
cols[self._report_total_key].append(
"{:.3f}".format(total_addon_times[addon_name])
)
cols[self._report_total_key] = [
"{:.3f}".format(total_by_addon[addon_name])
for addon_name in cols["Addon name"]
]
# Prepare column widths and total row count
# - column width is by

View file

@ -1,6 +0,0 @@
from .addon import BlenderAddon
__all__ = (
"BlenderAddon",
)

View file

@ -1,10 +0,0 @@
from .addon import (
HIERO_ROOT_DIR,
HieroAddon,
)
__all__ = (
"HIERO_ROOT_DIR",
"HieroAddon",
)

View file

@ -7,6 +7,8 @@ import six
from ayon_core.lib import Logger
from ayon_core.modules import AYONAddon, IPluginPaths
from .version import __version__
class DeadlineWebserviceError(Exception):
"""
@ -16,6 +18,7 @@ class DeadlineWebserviceError(Exception):
class DeadlineModule(AYONAddon, IPluginPaths):
name = "deadline"
version = __version__
def initialize(self, studio_settings):
# This module is always enabled

View file

@ -79,7 +79,7 @@ class FusionSubmitDeadline(
else:
context.data[key] = True
from ayon_core.hosts.fusion.api.lib import get_frame_path
from ayon_fusion.api.lib import get_frame_path
deadline_url = instance.data["deadline"]["url"]
assert deadline_url, "Requires Deadline Webservice URL"

View file

@ -1,6 +1,9 @@
from .version import __version__
from .addon import JobQueueAddon
__all__ = (
"__version__",
"JobQueueAddon",
)

View file

@ -44,9 +44,12 @@ import platform
from ayon_core.addon import AYONAddon, click_wrap
from ayon_core.settings import get_studio_settings
from .version import __version__
class JobQueueAddon(AYONAddon):
name = "job_queue"
version = __version__
def initialize(self, studio_settings):
addon_settings = studio_settings.get(self.name) or {}

View file

@ -0,0 +1 @@
__version__ = "1.0.0"

View file

@ -7,6 +7,7 @@ from ayon_core.addon import AYONAddon, ITrayAction
class LauncherAction(AYONAddon, ITrayAction):
label = "Launcher"
name = "launcher_tool"
version = "1.0.0"
def initialize(self, settings):

View file

@ -3,6 +3,7 @@ from ayon_core.addon import AYONAddon, ITrayAddon
class LoaderAddon(AYONAddon, ITrayAddon):
name = "loader_tool"
version = "1.0.0"
def initialize(self, settings):
# Tray attributes

View file

@ -4,6 +4,7 @@ from ayon_core.addon import AYONAddon, ITrayAction
class PythonInterpreterAction(AYONAddon, ITrayAction):
label = "Console"
name = "python_interpreter"
version = "1.0.0"
admin_action = True
def initialize(self, settings):

View file

@ -1,7 +1,10 @@
from .version import __version__
from .timers_manager import (
TimersManager
)
__all__ = (
"__version__",
"TimersManager",
)

View file

@ -10,6 +10,7 @@ from ayon_core.addon import (
)
from ayon_core.lib.events import register_event_callback
from .version import __version__
from .exceptions import InvalidContextError
TIMER_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
@ -96,6 +97,7 @@ class TimersManager(
See `ExampleTimersManagerConnector`.
"""
name = "timers_manager"
version = __version__
label = "Timers Service"
_required_methods = (

View file

@ -0,0 +1 @@
__version__ = "0.1.1"

View file

@ -1,8 +1,13 @@
from .version import __version__
from .structures import HostMsgAction
from .webserver_module import (
WebServerAddon
)
__all__ = (
"__version__",
"HostMsgAction",
"WebServerAddon",
)

View file

@ -9,22 +9,18 @@ from qtpy import QtWidgets
from ayon_core.addon import ITrayService
from ayon_core.tools.stdout_broker.window import ConsoleDialog
from .structures import HostMsgAction
log = logging.getLogger(__name__)
# Host listener icon type
class IconType:
IDLE = "idle"
RUNNING = "running"
FAILED = "failed"
class MsgAction:
CONNECTING = "connecting"
INITIALIZED = "initialized"
ADD = "add"
CLOSE = "close"
class HostListener:
def __init__(self, webserver, module):
self._window_per_id = {}
@ -96,22 +92,22 @@ class HostListener:
if msg.type == aiohttp.WSMsgType.TEXT:
host_name, action, text = self._parse_message(msg)
if action == MsgAction.CONNECTING:
if action == HostMsgAction.CONNECTING:
self._action_per_id[host_name] = None
# must be sent to main thread, or action wont trigger
self.module.execute_in_main_thread(
lambda: self._host_is_connecting(host_name, text))
elif action == MsgAction.CLOSE:
elif action == HostMsgAction.CLOSE:
# clean close
self._close(host_name)
await ws.close()
elif action == MsgAction.INITIALIZED:
elif action == HostMsgAction.INITIALIZED:
self.module.execute_in_main_thread(
# must be queued as _host_is_connecting might not
# be triggered/finished yet
lambda: self._set_host_icon(host_name,
IconType.RUNNING))
elif action == MsgAction.ADD:
elif action == HostMsgAction.ADD:
self.module.execute_in_main_thread(
lambda: self._add_text(host_name, text))
elif msg.type == aiohttp.WSMsgType.ERROR:

View file

@ -0,0 +1,6 @@
# Host listener message actions
class HostMsgAction:
CONNECTING = "connecting"
INITIALIZED = "initialized"
ADD = "add"
CLOSE = "close"

View file

@ -0,0 +1 @@
__version__ = "1.0.0"

View file

@ -26,9 +26,12 @@ import socket
from ayon_core import resources
from ayon_core.addon import AYONAddon, ITrayService
from .version import __version__
class WebServerAddon(AYONAddon, ITrayService):
name = "webserver"
version = __version__
label = "WebServer"
webserver_url_env = "AYON_WEBSERVER_URL"

View file

@ -11,7 +11,12 @@ from pyblish.lib import MessageHandler
from ayon_core import AYON_CORE_ROOT
from ayon_core.host import HostBase
from ayon_core.lib import is_in_tests, initialize_ayon_connection, emit_event
from ayon_core.lib import (
is_in_tests,
initialize_ayon_connection,
emit_event,
version_up
)
from ayon_core.addon import load_addons, AddonsManager
from ayon_core.settings import get_project_settings
@ -21,6 +26,8 @@ from .template_data import get_template_data_with_names
from .workfile import (
get_workdir,
get_custom_workfile_template_by_string_context,
get_workfile_template_key_from_context,
get_last_workfile
)
from . import (
register_loader_plugin_path,
@ -579,3 +586,48 @@ def get_process_id():
if _process_id is None:
_process_id = str(uuid.uuid4())
return _process_id
def version_up_current_workfile():
"""Function to increment and save workfile
"""
host = registered_host()
if not host.has_unsaved_changes():
print("No unsaved changes, skipping file save..")
return
project_name = get_current_project_name()
folder_path = get_current_folder_path()
task_name = get_current_task_name()
host_name = get_current_host_name()
template_key = get_workfile_template_key_from_context(
project_name,
folder_path,
task_name,
host_name,
)
anatomy = Anatomy(project_name)
data = get_template_data_with_names(
project_name, folder_path, task_name, host_name
)
data["root"] = anatomy.roots
work_template = anatomy.get_template_item("work", template_key)
# Define saving file extension
extensions = host.get_workfile_extensions()
current_file = host.get_current_workfile()
if current_file:
extensions = [os.path.splitext(current_file)[-1]]
work_root = work_template["directory"].format_strict(data)
file_template = work_template["file"].template
last_workfile_path = get_last_workfile(
work_root, file_template, data, extensions, True
)
new_workfile_path = version_up(last_workfile_path)
if os.path.exists(new_workfile_path):
new_workfile_path = version_up(new_workfile_path)
host.save_workfile(new_workfile_path)

View file

@ -336,17 +336,16 @@ def get_plugin_settings(plugin, project_settings, log, category=None):
settings_category = getattr(plugin, "settings_category", None)
if settings_category:
try:
return (
project_settings
[settings_category]
["publish"]
[plugin.__name__]
)
category_settings = project_settings[settings_category]
except KeyError:
log.warning((
"Couldn't find plugin '{}' settings"
" under settings category '{}'"
).format(plugin.__name__, settings_category))
"Couldn't find settings category '{}' in project settings"
).format(settings_category))
return {}
try:
return category_settings["publish"][plugin.__name__]
except KeyError:
return {}
# Use project settings based on a category name

View file

@ -1,12 +0,0 @@
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 `ayon_core/modules/webserver` as that one is running in Tray,
this one is running in spawn Python process.)

View file

@ -172,12 +172,30 @@ class VersionItem:
def __gt__(self, other):
if not isinstance(other, VersionItem):
return False
if (
other.version == self.version
and self.is_hero
):
# Make sure hero versions are positive
version = abs(self.version)
other_version = abs(other.version)
# Hero version is greater than non-hero
if version == other_version:
return self.is_hero
return version > other_version
def __lt__(self, other):
if not isinstance(other, VersionItem):
return True
return other.version < self.version
# Make sure hero versions are positive
version = abs(self.version)
other_version = abs(other.version)
# Non-hero version is lesser than hero
if version == other_version:
return not self.is_hero
return version < other_version
def __ge__(self, other):
return self.__eq__(other) or self.__gt__(other)
def __le__(self, other):
return self.__eq__(other) or self.__lt__(other)
def to_data(self):
return {

View file

@ -348,10 +348,18 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
return set()
if not self._loaded_products_cache.is_valid:
if isinstance(self._host, ILoadHost):
containers = self._host.get_containers()
else:
containers = self._host.ls()
try:
if isinstance(self._host, ILoadHost):
containers = self._host.get_containers()
else:
containers = self._host.ls()
except BaseException:
self.log.error(
"Failed to collect loaded products.", exc_info=True
)
containers = []
repre_ids = set()
for container in containers:
repre_id = container.get("representation")

View file

@ -321,6 +321,8 @@ class LoaderFoldersWidget(QtWidgets.QWidget):
"""
self._folders_proxy_model.setFilterFixedString(name)
if name:
self._folders_view.expandAll()
def set_merged_products_selection(self, items):
"""

View file

@ -199,7 +199,9 @@ class ProductsModel(QtGui.QStandardItemModel):
product_item = self._product_items_by_id.get(product_id)
if product_item is None:
return None
return list(product_item.version_items.values())
product_items = list(product_item.version_items.values())
product_items.sort(reverse=True)
return product_items
if role == QtCore.Qt.EditRole:
return None

View file

@ -0,0 +1,5 @@
from .broker import StdOutBroker
__all__ = (
"StdOutBroker",
)

View file

@ -1,173 +1,12 @@
import os
import sys
import threading
import collections
import websocket
import json
from datetime import datetime
import warnings
from .broker import StdOutBroker
from ayon_core.lib import Logger
from openpype_modules.webserver.host_console_listener import MsgAction
warnings.warn(
(
"Import of 'StdOutBroker' from 'ayon_core.tools.stdout_broker.app'"
" is deprecated. Please use 'ayon_core.tools.stdout_broker' instead."
),
DeprecationWarning
)
log = Logger.get_logger(__name__)
class StdOutBroker:
"""
Application showing console in Services tray for non python hosts
instead of cmd window.
"""
MAX_LINES = 10000
TIMER_TIMEOUT = 0.200
def __init__(self, host_name):
self.host_name = host_name
self.webserver_client = None
self.original_stdout_write = None
self.original_stderr_write = None
self.log_queue = collections.deque()
date_str = datetime.now().strftime("%d%m%Y%H%M%S")
self.host_id = "{}_{}".format(self.host_name, date_str)
self._std_available = False
self._is_running = False
self._catch_std_outputs()
self._timer = None
@property
def send_to_tray(self):
"""Checks if connected to tray and have access to logs."""
return self.webserver_client and self._std_available
def start(self):
"""Start app, create and start timer"""
if not self._std_available or self._is_running:
return
self._is_running = True
self._create_timer()
self._connect_to_tray()
def stop(self):
"""Disconnect from Tray, process last logs"""
if not self._is_running:
return
self._is_running = False
self._process_queue()
self._disconnect_from_tray()
def host_connected(self):
"""Send to Tray console that host is ready - icon change. """
log.info("Host {} connected".format(self.host_id))
payload = {
"host": self.host_id,
"action": MsgAction.INITIALIZED,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
def _create_timer(self):
timer = threading.Timer(self.TIMER_TIMEOUT, self._timer_callback)
timer.start()
self._timer = timer
def _timer_callback(self):
if not self._is_running:
return
self._process_queue()
self._create_timer()
def _connect_to_tray(self):
"""Connect to Tray webserver to pass console output. """
if not self._std_available: # not content to log
return
ws = websocket.WebSocket()
webserver_url = os.environ.get("AYON_WEBSERVER_URL")
if not webserver_url:
print("Unknown webserver url, cannot connect to pass log")
return
webserver_url = webserver_url.replace("http", "ws")
ws.connect("{}/ws/host_listener".format(webserver_url))
self.webserver_client = ws
payload = {
"host": self.host_id,
"action": MsgAction.CONNECTING,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
def _disconnect_from_tray(self):
"""Send to Tray that host is closing - remove from Services. """
print("Host {} closing".format(self.host_name))
if not self.webserver_client:
return
payload = {
"host": self.host_id,
"action": MsgAction.CLOSE,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
self.webserver_client.close()
def _catch_std_outputs(self):
"""Redirects standard out and error to own functions"""
if sys.stdout:
self.original_stdout_write = sys.stdout.write
sys.stdout.write = self._my_stdout_write
self._std_available = True
if sys.stderr:
self.original_stderr_write = sys.stderr.write
sys.stderr.write = self._my_stderr_write
self._std_available = True
def _my_stdout_write(self, text):
"""Appends outputted text to queue, keep writing to original stdout"""
if self.original_stdout_write is not None:
self.original_stdout_write(text)
if self.send_to_tray:
self.log_queue.append(text)
def _my_stderr_write(self, text):
"""Appends outputted text to queue, keep writing to original stderr"""
if self.original_stderr_write is not None:
self.original_stderr_write(text)
if self.send_to_tray:
self.log_queue.append(text)
def _process_queue(self):
"""Sends lines and purges queue"""
if not self.send_to_tray:
return
lines = tuple(self.log_queue)
self.log_queue.clear()
if lines:
payload = {
"host": self.host_id,
"action": MsgAction.ADD,
"text": "\n".join(lines)
}
self._send(payload)
def _send(self, payload):
"""Worker method to send to existing websocket connection."""
if not self.send_to_tray:
return
try:
self.webserver_client.send(json.dumps(payload))
except ConnectionResetError: # Tray closed
self._connect_to_tray()
__all__ = ("StdOutBroker", )

View file

@ -0,0 +1,174 @@
import os
import sys
import threading
import collections
import json
from datetime import datetime
import websocket
from ayon_core.lib import Logger
from ayon_core.modules.webserver import HostMsgAction
log = Logger.get_logger(__name__)
class StdOutBroker:
"""
Application showing console in Services tray for non python hosts
instead of cmd window.
"""
MAX_LINES = 10000
TIMER_TIMEOUT = 0.200
def __init__(self, host_name):
self.host_name = host_name
self.webserver_client = None
self.original_stdout_write = None
self.original_stderr_write = None
self.log_queue = collections.deque()
date_str = datetime.now().strftime("%d%m%Y%H%M%S")
self.host_id = "{}_{}".format(self.host_name, date_str)
self._std_available = False
self._is_running = False
self._catch_std_outputs()
self._timer = None
@property
def send_to_tray(self):
"""Checks if connected to tray and have access to logs."""
return self.webserver_client and self._std_available
def start(self):
"""Start app, create and start timer"""
if not self._std_available or self._is_running:
return
self._is_running = True
self._create_timer()
self._connect_to_tray()
def stop(self):
"""Disconnect from Tray, process last logs"""
if not self._is_running:
return
self._is_running = False
self._process_queue()
self._disconnect_from_tray()
def host_connected(self):
"""Send to Tray console that host is ready - icon change. """
log.info("Host {} connected".format(self.host_id))
payload = {
"host": self.host_id,
"action": HostMsgAction.INITIALIZED,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
def _create_timer(self):
timer = threading.Timer(self.TIMER_TIMEOUT, self._timer_callback)
timer.start()
self._timer = timer
def _timer_callback(self):
if not self._is_running:
return
self._process_queue()
self._create_timer()
def _connect_to_tray(self):
"""Connect to Tray webserver to pass console output. """
if not self._std_available: # not content to log
return
ws = websocket.WebSocket()
webserver_url = os.environ.get("AYON_WEBSERVER_URL")
if not webserver_url:
print("Unknown webserver url, cannot connect to pass log")
return
webserver_url = webserver_url.replace("http", "ws")
ws.connect("{}/ws/host_listener".format(webserver_url))
self.webserver_client = ws
payload = {
"host": self.host_id,
"action": HostMsgAction.CONNECTING,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
def _disconnect_from_tray(self):
"""Send to Tray that host is closing - remove from Services. """
print("Host {} closing".format(self.host_name))
if not self.webserver_client:
return
payload = {
"host": self.host_id,
"action": HostMsgAction.CLOSE,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
self.webserver_client.close()
def _catch_std_outputs(self):
"""Redirects standard out and error to own functions"""
if sys.stdout:
self.original_stdout_write = sys.stdout.write
sys.stdout.write = self._my_stdout_write
self._std_available = True
if sys.stderr:
self.original_stderr_write = sys.stderr.write
sys.stderr.write = self._my_stderr_write
self._std_available = True
def _my_stdout_write(self, text):
"""Appends outputted text to queue, keep writing to original stdout"""
if self.original_stdout_write is not None:
self.original_stdout_write(text)
if self.send_to_tray:
self.log_queue.append(text)
def _my_stderr_write(self, text):
"""Appends outputted text to queue, keep writing to original stderr"""
if self.original_stderr_write is not None:
self.original_stderr_write(text)
if self.send_to_tray:
self.log_queue.append(text)
def _process_queue(self):
"""Sends lines and purges queue"""
if not self.send_to_tray:
return
lines = tuple(self.log_queue)
self.log_queue.clear()
if lines:
payload = {
"host": self.host_id,
"action": HostMsgAction.ADD,
"text": "\n".join(lines)
}
self._send(payload)
def _send(self, payload):
"""Worker method to send to existing websocket connection."""
if not self.send_to_tray:
return
try:
self.webserver_client.send(json.dumps(payload))
except ConnectionResetError: # Tray closed
self._connect_to_tray()

View file

@ -447,8 +447,10 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
def initialize_addons(self):
self._initializing_addons = True
self.tray_man.initialize_addons()
self._initializing_addons = False
try:
self.tray_man.initialize_addons()
finally:
self._initializing_addons = False
def _click_timer_timeout(self):
self._click_timer.stop()

View file

@ -370,6 +370,8 @@ class FoldersWidget(QtWidgets.QWidget):
"""
self._folders_proxy_model.setFilterFixedString(name)
if name:
self._folders_view.expandAll()
def refresh(self):
"""Refresh folders model.

View file

@ -79,12 +79,12 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
exclude = [
"client/ayon_core/hosts/unreal/integration/*",
"client/ayon_core/hosts/aftereffects/api/extension/js/libs/*",
"client/ayon_core/hosts/hiero/api/startup/*",
"client/ayon_core/modules/deadline/repository/custom/plugins/CelAction/*",
"client/ayon_core/modules/deadline/repository/custom/plugins/HarmonyAYON/*",
"client/ayon_core/modules/click_wrap.py",
"client/ayon_core/scripts/slates/__init__.py"
"client/ayon_core/scripts/slates/__init__.py",
"server_addon/hiero/client/ayon_hiero/api/startup/*",
"server_addon/aftereffects/client/ayon_aftereffects/api/extension/js/libs/*"
]
[tool.ruff.lint.per-file-ignores]

View file

@ -798,7 +798,7 @@ class PublishPuginsModel(BaseSettingsModel):
)
ValidateOutdatedContainers: PluginStateByHostModel = SettingsField(
default_factory=PluginStateByHostModel,
title="Validate Containers"
title="Validate Outdated Containers"
)
ValidateIntent: ValidateIntentModel = SettingsField(
default_factory=ValidateIntentModel,

View file

@ -147,6 +147,15 @@ class WorkfilesLockProfile(BaseSettingsModel):
enabled: bool = SettingsField(True, title="Enabled")
class AYONMenuModel(BaseSettingsModel):
_layout = "expanded"
version_up_current_workfile: bool = SettingsField(
False,
title="Version Up Workfile",
description="Add 'Version Up Workfile' to AYON menu"
)
class WorkfilesToolModel(BaseSettingsModel):
workfile_template_profiles: list[WorkfileTemplateProfile] = SettingsField(
default_factory=list,
@ -297,6 +306,10 @@ class PublishToolModel(BaseSettingsModel):
class GlobalToolsModel(BaseSettingsModel):
ayon_menu: AYONMenuModel = SettingsField(
default_factory=AYONMenuModel,
title="AYON Menu"
)
creator: CreatorToolModel = SettingsField(
default_factory=CreatorToolModel,
title="Creator"
@ -316,6 +329,9 @@ class GlobalToolsModel(BaseSettingsModel):
DEFAULT_TOOLS_VALUES = {
"ayon_menu": {
"version_up_current_workfile": False
},
"creator": {
"product_types_smart_select": [
{

View file

@ -1,3 +1,4 @@
from .version import __version__
from .addon import (
AFTEREFFECTS_ADDON_ROOT,
AfterEffectsAddon,
@ -6,6 +7,8 @@ from .addon import (
__all__ = (
"__version__",
"AFTEREFFECTS_ADDON_ROOT",
"AfterEffectsAddon",
"get_launch_script_path",

View file

@ -2,11 +2,14 @@ import os
from ayon_core.addon import AYONAddon, IHostAddon
from .version import __version__
AFTEREFFECTS_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__))
class AfterEffectsAddon(AYONAddon, IHostAddon):
name = "aftereffects"
version = __version__
host_name = "aftereffects"
def add_implementation_envs(self, env, _app):

View file

@ -25,7 +25,7 @@ download [Anastasiys Extension Manager](https://install.anastasiy.com/)
The easiest way to get the server and After Effects launch is with:
```
python -c ^"import ayon_core.hosts.photoshop;ayon_core.hosts.aftereffects.launch(""c:\Program Files\Adobe\Adobe After Effects 2020\Support Files\AfterFX.exe"")^"
python -c ^"import ayon_core.hosts.photoshop;ayon_aftereffects.launch(""c:\Program Files\Adobe\Adobe After Effects 2020\Support Files\AfterFX.exe"")^"
```
`avalon.aftereffects.launch` launches the application and server, and also closes the server when After Effects exists.

View file

@ -18,8 +18,8 @@ from ayon_core.lib import Logger, is_in_tests
from ayon_core.pipeline import install_host
from ayon_core.addon import AddonsManager
from ayon_core.tools.utils import host_tools, get_ayon_qt_app
from ayon_core.tools.adobe_webserver.app import WebServerTool
from .webserver import WebServerTool
from .ws_stub import get_stub
from .lib import set_settings
@ -35,7 +35,7 @@ def main(*subprocess_args):
"""Main entrypoint to AE launching, called from pre hook."""
sys.excepthook = safe_excepthook
from ayon_core.hosts.aftereffects.api import AfterEffectsHost
from ayon_aftereffects.api import AfterEffectsHost
host = AfterEffectsHost()
install_host(host)
@ -355,7 +355,7 @@ class AfterEffectsRoute(WebSocketRoute):
return "nothing"
def create_placeholder_route(self):
from ayon_core.hosts.aftereffects.api.workfile_template_builder import \
from ayon_aftereffects.api.workfile_template_builder import \
create_placeholder
partial_method = functools.partial(create_placeholder)
@ -365,7 +365,7 @@ class AfterEffectsRoute(WebSocketRoute):
return "nothing"
def update_placeholder_route(self):
from ayon_core.hosts.aftereffects.api.workfile_template_builder import \
from ayon_aftereffects.api.workfile_template_builder import \
update_placeholder
partial_method = functools.partial(update_placeholder)
@ -375,7 +375,7 @@ class AfterEffectsRoute(WebSocketRoute):
return "nothing"
def build_workfile_template_route(self):
from ayon_core.hosts.aftereffects.api.workfile_template_builder import \
from ayon_aftereffects.api.workfile_template_builder import \
build_workfile_template
partial_method = functools.partial(build_workfile_template)

View file

@ -8,7 +8,7 @@ workfile or others.
import os
import sys
from ayon_core.hosts.aftereffects.api.launch_logic import main as host_main
from ayon_aftereffects.api.launch_logic import main as host_main
# Get current file to locate start point of sys.argv
CURRENT_FILE = os.path.abspath(__file__)

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -14,8 +14,6 @@ from ayon_core.pipeline import (
AYON_INSTANCE_ID,
)
from ayon_core.pipeline.load import any_outdated_containers
import ayon_core.hosts.aftereffects
from ayon_core.host import (
HostBase,
IWorkfileHost,
@ -23,6 +21,7 @@ from ayon_core.host import (
IPublishHost
)
from ayon_core.tools.utils import get_ayon_qt_app
from ayon_aftereffects import AFTEREFFECTS_ADDON_ROOT
from .launch_logic import get_stub
from .ws_stub import ConnectionNotEstablishedYet
@ -30,10 +29,7 @@ from .ws_stub import ConnectionNotEstablishedYet
log = Logger.get_logger(__name__)
HOST_DIR = os.path.dirname(
os.path.abspath(ayon_core.hosts.aftereffects.__file__)
)
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PLUGINS_DIR = os.path.join(AFTEREFFECTS_ADDON_ROOT, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")

View file

@ -1,7 +1,11 @@
"""This Webserver tool is python 3 specific.
"""Webserver for communication with AfterEffects.
Don't import directly to avalon.tools or implementation of Python 2 hosts
would break.
Aiohttp (Asyncio) based websocket server used for communication with host
application.
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.
"""
import os
import logging
@ -12,9 +16,7 @@ import socket
from aiohttp import web
from wsrpc_aiohttp import (
WSRPCClient
)
from wsrpc_aiohttp import WSRPCClient
from ayon_core.pipeline import get_global_context

View file

@ -12,7 +12,7 @@ from ayon_core.pipeline.workfile.workfile_template_builder import (
PlaceholderPlugin,
PlaceholderItem
)
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
PLACEHOLDER_SET = "PLACEHOLDERS_SET"
PLACEHOLDER_ID = "openpype.placeholder"

View file

@ -8,7 +8,8 @@ import logging
import attr
from wsrpc_aiohttp import WebSocketAsync
from ayon_core.tools.adobe_webserver.app import WebServerTool
from .webserver import WebServerTool
class ConnectionNotEstablishedYet(Exception):

View file

@ -7,7 +7,7 @@ from ayon_core.lib import (
is_using_ayon_console,
)
from ayon_applications import PreLaunchHook, LaunchTypes
from ayon_core.hosts.aftereffects import get_launch_script_path
from ayon_aftereffects import get_launch_script_path
def get_launch_kwargs(kwargs):

View file

@ -2,16 +2,16 @@ import re
from ayon_core import resources
from ayon_core.lib import BoolDef, UISeparatorDef
from ayon_core.hosts.aftereffects import api
from ayon_core.pipeline import (
Creator,
CreatedInstance,
CreatorError
)
from ayon_core.hosts.aftereffects.api.pipeline import cache_and_get_instances
from ayon_core.hosts.aftereffects.api.lib import set_settings
from ayon_core.lib import prepare_template_data
from ayon_core.pipeline.create import PRODUCT_NAME_ALLOWED_SYMBOLS
from ayon_aftereffects import api
from ayon_aftereffects.api.pipeline import cache_and_get_instances
from ayon_aftereffects.api.lib import set_settings
class RenderCreator(Creator):

View file

@ -1,11 +1,11 @@
import ayon_api
import ayon_core.hosts.aftereffects.api as api
from ayon_core.pipeline import (
AutoCreator,
CreatedInstance
)
from ayon_core.hosts.aftereffects.api.pipeline import cache_and_get_instances
from ayon_aftereffects import api
from ayon_aftereffects.api.pipeline import cache_and_get_instances
class AEWorkfileCreator(AutoCreator):

View file

@ -1,9 +1,9 @@
import re
from ayon_core.pipeline import get_representation_path
from ayon_core.hosts.aftereffects import api
from ayon_core.hosts.aftereffects.api.lib import (
from ayon_aftereffects import api
from ayon_aftereffects.api.lib import (
get_background_layers,
get_unique_layer_name,
)

View file

@ -1,8 +1,8 @@
import re
from ayon_core.pipeline import get_representation_path
from ayon_core.hosts.aftereffects import api
from ayon_core.hosts.aftereffects.api.lib import get_unique_layer_name
from ayon_aftereffects import api
from ayon_aftereffects.api.lib import get_unique_layer_name
class FileLoader(api.AfterEffectsLoader):

View file

@ -1,6 +1,6 @@
import pyblish.api
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class AddPublishHighlight(pyblish.api.InstancePlugin):

View file

@ -2,7 +2,7 @@
"""Close AE after publish. For Webpublishing only."""
import pyblish.api
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class CloseAE(pyblish.api.ContextPlugin):

View file

@ -2,7 +2,7 @@ import os
import pyblish.api
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class CollectAudio(pyblish.api.ContextPlugin):

View file

@ -2,7 +2,7 @@ import os
import pyblish.api
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class CollectCurrentFile(pyblish.api.ContextPlugin):

View file

@ -2,7 +2,7 @@ import os
import re
import pyblish.api
from ayon_core.hosts.aftereffects.api import (
from ayon_aftereffects.api import (
get_stub,
get_extension_manifest_path
)

View file

@ -6,8 +6,8 @@ import pyblish.api
from ayon_core.pipeline import publish
from ayon_core.pipeline.publish import RenderInstance
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
@attr.s
class AERenderInstance(RenderInstance):

View file

@ -14,6 +14,7 @@ class CollectReview(pyblish.api.ContextPlugin):
label = "Collect Review"
hosts = ["aftereffects"]
order = pyblish.api.CollectorOrder + 0.1
settings_category = "aftereffects"
def process(self, context):
for instance in context:

View file

@ -1,8 +1,8 @@
import os
from ayon_core.pipeline import publish
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class ExtractLocalRender(publish.Extractor):
"""Render RenderQueue locally."""

View file

@ -1,7 +1,7 @@
import pyblish.api
from ayon_core.pipeline import publish
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class ExtractSaveScene(pyblish.api.ContextPlugin):

View file

@ -2,7 +2,7 @@ import pyblish.api
from ayon_core.lib import version_up
from ayon_core.pipeline.publish import get_errored_plugins_from_context
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class IncrementWorkfile(pyblish.api.InstancePlugin):

View file

@ -1,5 +1,5 @@
from ayon_core.pipeline import publish
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class RemovePublishHighlight(publish.Extractor):

View file

@ -9,7 +9,7 @@ import pyblish.api
from ayon_core.pipeline import (
PublishXmlValidationError
)
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class ValidateFootageItems(pyblish.api.InstancePlugin):

View file

@ -5,7 +5,7 @@ from ayon_core.pipeline.publish import (
ValidateContentsOrder,
PublishXmlValidationError,
)
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class ValidateInstanceFolderRepair(pyblish.api.Action):

View file

@ -13,7 +13,7 @@ from ayon_core.pipeline import (
PublishXmlValidationError,
OptionalPyblishPluginMixin
)
from ayon_core.hosts.aftereffects.api import get_folder_settings
from ayon_aftereffects.api import get_folder_settings
class ValidateSceneSettings(OptionalPyblishPluginMixin,
@ -60,6 +60,7 @@ class ValidateSceneSettings(OptionalPyblishPluginMixin,
label = "Validate Scene Settings"
families = ["render.farm", "render.local", "render"]
hosts = ["aftereffects"]
settings_category = "aftereffects"
optional = True
skip_timelines_check = [".*"] # * >> skip for all

View file

@ -2,9 +2,11 @@ from ayon_core.pipeline.workfile.workfile_template_builder import (
CreatePlaceholderItem,
PlaceholderCreateMixin
)
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_core.hosts.aftereffects.api.lib import set_settings
import ayon_core.hosts.aftereffects.api.workfile_template_builder as wtb
from ayon_aftereffects.api import (
get_stub,
workfile_template_builder as wtb,
)
from ayon_aftereffects.api.lib import set_settings
class AEPlaceholderCreatePlugin(wtb.AEPlaceholderPlugin,

View file

@ -2,8 +2,10 @@ from ayon_core.pipeline.workfile.workfile_template_builder import (
LoadPlaceholderItem,
PlaceholderLoadMixin
)
from ayon_core.hosts.aftereffects.api import get_stub
import ayon_core.hosts.aftereffects.api.workfile_template_builder as wtb
from ayon_aftereffects.api import (
get_stub,
workfile_template_builder as wtb,
)
class AEPlaceholderLoadPlugin(wtb.AEPlaceholderPlugin, PlaceholderLoadMixin):

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring AYON addon 'aftereffects' version."""
__version__ = "0.2.0"

View file

@ -0,0 +1,6 @@
[project]
name="aftereffects"
description="AYON AfterEffects addon."
[ayon.runtimeDependencies]
wsrpc_aiohttp = "^3.1.1" # websocket server

View file

@ -1,3 +1,10 @@
name = "aftereffects"
title = "AfterEffects"
version = "0.1.4"
version = "0.2.0"
client_dir = "ayon_aftereffects"
ayon_required_addons = {
"core": ">0.3.2",
}
ayon_compatible_addons = {}

Some files were not shown because too many files have changed in this diff Show more