From ec419f556906c4e3a804b02b1c139cb82e1b91ee Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:12:26 +0200 Subject: [PATCH 01/22] added 'ensure_is_process_ready' method to addon base class --- client/ayon_core/addon/base.py | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index b9ecff4233..c85f38b32a 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -67,6 +67,56 @@ MOVED_ADDON_MILESTONE_VERSIONS = { } +class ProcessPreparationError(Exception): + """Exception that can be used when process preparation failed. + + The message is showed to user (either as UI dialog or printed). If + different error is raised a "generic" error message is showed to user + with option to copy error message to clipboard. + + """ + pass + + +class ProcessContext: + """Context of child process. + + Notes: + This class is used to pass context to child process. It can be used + to use different behavior of addon based on information in + the context. + The context can be enhanced in future versions. + + Args: + addon_name (Optional[str]): Addon name which triggered process. + addon_version (Optional[str]): Addon version which triggered process. + project_name (Optional[str]): Project name. Can be filled in case + process is triggered for specific project. Some addons can have + different behavior based on project. + headless (Optional[bool]): Is process running in headless mode. + + """ + def __init__( + self, + addon_name: Optional[str] = None, + addon_version: Optional[str] = None, + project_name: Optional[str] = None, + headless: Optional[bool] = None, + **kwargs, + ): + if headless is None: + # TODO use lib function to get headless mode + headless = os.getenv("AYON_HEADLESS_MODE") == "1" + self.addon_name: Optional[str] = addon_name + self.addon_version: Optional[str] = addon_version + self.project_name: Optional[str] = project_name + self.headless: bool = headless + + if kwargs: + unknown_keys = ", ".join([f'"{key}"' for key in kwargs.keys()]) + print(f"Unknown keys in ProcessContext: {unknown_keys}") + + # Inherit from `object` for Python 2 hosts class _ModuleClass(object): """Fake module class for storing AYON addons. @@ -588,7 +638,29 @@ class AYONAddon(object): Args: enabled_addons (list[AYONAddon]): Addons that are enabled. """ + pass + def ensure_is_process_ready( + self, process_context: ProcessContext + ): + """Make sure addon is prepared for a process. + + This method is called when some action makes sure that addon has set + necessary data. For example if user should be logged in + and filled credentials in environment variables this method should + ask user for credentials. + + Implementation of this method is optional. + + Note: + The logic can be similar to logic in tray, but in tray not require + to be logged in. + + Args: + process_context (ProcessContext): Context of child + process. + + """ pass def get_global_environments(self): From 9de1f236f16166f51fdfed836284cdf1fd0ed3ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:12:52 +0200 Subject: [PATCH 02/22] prepare utils to run process preparation --- client/ayon_core/addon/__init__.py | 8 +++ client/ayon_core/addon/utils.py | 112 +++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 client/ayon_core/addon/utils.py diff --git a/client/ayon_core/addon/__init__.py b/client/ayon_core/addon/__init__.py index fe8865c730..27af2955ed 100644 --- a/client/ayon_core/addon/__init__.py +++ b/client/ayon_core/addon/__init__.py @@ -9,12 +9,17 @@ from .interfaces import ( ) from .base import ( + ProcessPreparationError, AYONAddon, AddonsManager, TrayAddonsManager, load_addons, ) +from .utils import ( + ensure_addons_are_process_ready, +) + __all__ = ( "click_wrap", @@ -25,8 +30,11 @@ __all__ = ( "ITrayService", "IHostAddon", + "ProcessPreparationError", "AYONAddon", "AddonsManager", "TrayAddonsManager", "load_addons", + + "ensure_addons_are_process_ready", ) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py new file mode 100644 index 0000000000..092ce502af --- /dev/null +++ b/client/ayon_core/addon/utils.py @@ -0,0 +1,112 @@ +import os +import sys +import contextlib +import tempfile +import json +from io import StringIO +from typing import Optional + +from ayon_core.lib import run_ayon_launcher_process + +from .base import AddonsManager, ProcessContext, ProcessPreparationError + + +def _handle_error( + process_context: ProcessContext, + message: str, + detail: Optional[str], +): + """Handle error in process ready preparation. + + Shows UI to inform user about the error, or prints the message + to stdout if running in headless mode. + + Args: + process_context (ProcessContext): The context in which the + error occurred. + message (str): The message to show. + detail (Optional[str]): The detail message to show (usually + traceback). + + """ + if process_context.headless: + print(detail) + print(f"{10*'*'}\n{message}\n{10*'*'}") + return + + current_dir = os.path.dirname(os.path.abspath(__file__)) + script_path = os.path.join(current_dir, "ui", "process_ready_error.py") + with tempfile.NamedTemporaryFile("w", delete=False) as tmp: + tmp_path = tmp.name + + try: + with open(tmp_path, "w") as stream: + json.dump( + {"message": message, "detail": detail}, + stream + ) + + run_ayon_launcher_process( + "run", script_path, tmp_path + ) + + finally: + os.remove(tmp_path) + + +def ensure_addons_are_process_ready( + process_context: ProcessContext, + addons_manager: Optional[AddonsManager] = None, + exit_on_failure: bool = True, +): + """Ensure all enabled addons are ready to be used in the given context. + + Call this method only in AYON launcher process and as first thing + to avoid possible clashes with preparation. For example 'QApplication' + should not be created. + + Args: + process_context (ProcessContext): The context in which the + addons should be prepared. + addons_manager (Optional[AddonsManager]): The addons + manager to use. If not provided, a new one will be created. + exit_on_failure (bool, optional): If True, the process will exit + if an error occurs. Defaults to True. + + Returns: + Optional[Exception]: The exception that occurred during the + preparation, if any. + + """ + if addons_manager is None: + addons_manager = AddonsManager() + + exc = None + message = None + failed = False + use_detail = False + with contextlib.redirect_stdout(StringIO()) as stdout: + for addon in addons_manager.get_enabled_addons(): + failed = True + try: + addon.ensure_is_process_ready(process_context) + failed = False + except ProcessPreparationError as exc: + message = str(exc) + + except BaseException as exc: + message = "An unexpected error occurred." + use_detail = True + + if failed: + break + + if failed: + detail = None + if use_detail: + detail = stdout.get_value() + + _handle_error(process_context, message, detail) + if not exit_on_failure: + return exc + sys.exit(1) From 01983de888db8118437c6feb104b576cbffc362f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:13:31 +0200 Subject: [PATCH 03/22] implemented dialog showing error --- .../ayon_core/addon/ui/process_ready_error.py | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 client/ayon_core/addon/ui/process_ready_error.py diff --git a/client/ayon_core/addon/ui/process_ready_error.py b/client/ayon_core/addon/ui/process_ready_error.py new file mode 100644 index 0000000000..78e2b9b9bb --- /dev/null +++ b/client/ayon_core/addon/ui/process_ready_error.py @@ -0,0 +1,132 @@ +import sys +import json +from typing import Optional + +from qtpy import QtWidgets, QtCore + +from ayon_core.style import load_stylesheet +from ayon_core.tools.utils import get_ayon_qt_app + + +class DetailDialog(QtWidgets.QDialog): + def __init__(self, detail, parent): + super().__init__(parent) + + self.setWindowTitle("Detail") + + detail_input = QtWidgets.QPlainTextEdit(self) + detail_input.setPlainText(detail) + detail_input.setReadOnly(True) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(detail_input, 1) + + def showEvent(self, event): + self.resize(600, 400) + super().showEvent(event) + + +class ErrorDialog(QtWidgets.QDialog): + def __init__( + self, + message: str, + detail: Optional[str], + parent: Optional[QtWidgets.QWidget] = None + ): + super().__init__(parent) + + self.setWindowTitle("Preparation failed") + self.setWindowFlags( + self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint + ) + + message_label = QtWidgets.QLabel(self) + + detail_wrapper = QtWidgets.QWidget(self) + + detail_label = QtWidgets.QLabel(detail_wrapper) + + detail_layout = QtWidgets.QVBoxLayout(detail_wrapper) + detail_layout.setContentsMargins(0, 0, 0, 0) + detail_layout.addWidget(detail_label) + + btns_wrapper = QtWidgets.QWidget(self) + + copy_detail_btn = QtWidgets.QPushButton("Copy detail", btns_wrapper) + show_detail_btn = QtWidgets.QPushButton("Show detail", btns_wrapper) + confirm_btn = QtWidgets.QPushButton("Close", btns_wrapper) + + btns_layout = QtWidgets.QHBoxLayout(btns_wrapper) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addWidget(copy_detail_btn, 0) + btns_layout.addWidget(show_detail_btn, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(confirm_btn, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(message_label, 0) + layout.addWidget(detail_wrapper, 1) + layout.addWidget(btns_wrapper, 0) + + copy_detail_btn.clicked.connect(self._on_copy_clicked) + show_detail_btn.clicked.connect(self._on_show_detail_clicked) + confirm_btn.clicked.connect(self._on_confirm_clicked) + + self._message_label = message_label + self._detail_wrapper = detail_wrapper + self._detail_label = detail_label + + self._copy_detail_btn = copy_detail_btn + self._show_detail_btn = show_detail_btn + self._confirm_btn = confirm_btn + + self._detail_dialog = None + + self._detail = detail + + self.set_message(message, detail) + + def showEvent(self, event): + self.setStyleSheet(load_stylesheet()) + self.resize(320, 140) + super().showEvent(event) + + def set_message(self, message, detail): + self._message_label.setText(message) + self._detail = detail + + for widget in ( + self._copy_detail_btn, + self._show_detail_btn, + ): + widget.setVisible(bool(detail)) + + def _on_copy_clicked(self): + if self._detail: + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(self._detail) + + def _on_show_detail_clicked(self): + if self._detail_dialog is None: + self._detail_dialog = DetailDialog(self._detail, self) + self._detail_dialog.show() + + def _on_confirm_clicked(self): + self.accept() + + +def main(): + json_path = sys.argv[-1] + with open(json_path, "r") as stream: + data = json.load(stream) + + message = data["message"] + detail = data["detail"] + app = get_ayon_qt_app() + dialog = ErrorDialog(message, detail) + dialog.show() + app.exec_() + + +if __name__ == "__main__": + main() From 03317229fa0073f8624357ac04ad1706bcff574d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:10:35 +0200 Subject: [PATCH 04/22] add missing import --- client/ayon_core/addon/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index c85f38b32a..4c27cca9d7 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -10,6 +10,7 @@ import threading import collections from uuid import uuid4 from abc import ABCMeta, abstractmethod +from typing import Optional import six import appdirs From c86613922f035162ed801c9dd95c8698022112a0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:15:05 +0200 Subject: [PATCH 05/22] better handling of the error --- client/ayon_core/addon/__init__.py | 2 + client/ayon_core/addon/utils.py | 63 ++++++++++++++++++------------ 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/client/ayon_core/addon/__init__.py b/client/ayon_core/addon/__init__.py index 27af2955ed..44f495b2a1 100644 --- a/client/ayon_core/addon/__init__.py +++ b/client/ayon_core/addon/__init__.py @@ -10,6 +10,7 @@ from .interfaces import ( from .base import ( ProcessPreparationError, + ProcessContext, AYONAddon, AddonsManager, TrayAddonsManager, @@ -31,6 +32,7 @@ __all__ = ( "IHostAddon", "ProcessPreparationError", + "ProcessContext", "AYONAddon", "AddonsManager", "TrayAddonsManager", diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index 092ce502af..77dc5a26f6 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -3,6 +3,7 @@ import sys import contextlib import tempfile import json +import traceback from io import StringIO from typing import Optional @@ -21,6 +22,10 @@ def _handle_error( Shows UI to inform user about the error, or prints the message to stdout if running in headless mode. + Todos: + Make this functionality with the dialog as unified function, so it can + be used elsewhere. + Args: process_context (ProcessContext): The context in which the error occurred. @@ -30,7 +35,8 @@ def _handle_error( """ if process_context.headless: - print(detail) + if detail: + print(detail) print(f"{10*'*'}\n{message}\n{10*'*'}") return @@ -38,16 +44,15 @@ def _handle_error( script_path = os.path.join(current_dir, "ui", "process_ready_error.py") with tempfile.NamedTemporaryFile("w", delete=False) as tmp: tmp_path = tmp.name + json.dump( + {"message": message, "detail": detail}, + tmp.file + ) try: - with open(tmp_path, "w") as stream: - json.dump( - {"message": message, "detail": detail}, - stream - ) - run_ayon_launcher_process( - "run", script_path, tmp_path + "run", script_path, tmp_path, + creationflags=0 ) finally: @@ -85,27 +90,35 @@ def ensure_addons_are_process_ready( message = None failed = False use_detail = False - with contextlib.redirect_stdout(StringIO()) as stdout: - for addon in addons_manager.get_enabled_addons(): - failed = True - try: - addon.ensure_is_process_ready(process_context) - failed = False - except ProcessPreparationError as exc: - message = str(exc) + output = StringIO() + with contextlib.redirect_stdout(output): + with contextlib.redirect_stderr(output): + for addon in addons_manager.get_enabled_addons(): + failed = True + try: + addon.ensure_is_process_ready(process_context) + failed = False + except ProcessPreparationError as exc: + message = str(exc) + print(f"Addon preparation failed: '{addon.name}'") + print(message) - except BaseException as exc: - message = "An unexpected error occurred." - use_detail = True + except BaseException as exc: + message = "An unexpected error occurred." + print(f"Addon preparation failed: '{addon.name}'") + print(message) + # Print the traceback so it is in the output + traceback.print_exception(*sys.exc_info()) + use_detail = True - if failed: - break + if failed: + break + output_str = output.getvalue() + # Print stdout/stderr to console as it was redirected + print(output_str) if failed: - detail = None - if use_detail: - detail = stdout.get_value() - + detail = output_str if use_detail else None _handle_error(process_context, message, detail) if not exit_on_failure: return exc From 54f22696a964154c9a1a113cc24bd486ed7ec58d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:27:39 +0200 Subject: [PATCH 06/22] 'run_ayon_launcher_process' can add sys path to python path --- client/ayon_core/lib/execute.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/execute.py b/client/ayon_core/lib/execute.py index e89c8f22ee..f61d892324 100644 --- a/client/ayon_core/lib/execute.py +++ b/client/ayon_core/lib/execute.py @@ -179,7 +179,7 @@ def clean_envs_for_ayon_process(env=None): return env -def run_ayon_launcher_process(*args, **kwargs): +def run_ayon_launcher_process(*args, add_sys_paths=False, **kwargs): """Execute AYON process with passed arguments and wait. Wrapper for 'run_process' which prepends AYON executable arguments @@ -209,6 +209,14 @@ def run_ayon_launcher_process(*args, **kwargs): # - fill more if you find more env = clean_envs_for_ayon_process(os.environ) + if add_sys_paths: + new_pythonpath = list(sys.path) + for path in env.get("PYTHONPATH", "").split(os.pathsep): + if not path or path in new_pythonpath: + continue + new_pythonpath.append(path) + env["PYTHONPATH"] = os.pathsep.join(new_pythonpath) + return run_subprocess(args, env=env, **kwargs) From 909e88baa799dc6f226c8bca746fffa697a0fa1a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:28:09 +0200 Subject: [PATCH 07/22] run the UI by adding sys path and skipping bootstrap --- client/ayon_core/addon/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index 77dc5a26f6..3ad112b562 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -51,8 +51,11 @@ def _handle_error( try: run_ayon_launcher_process( - "run", script_path, tmp_path, - creationflags=0 + "--skip-bootstrap", + script_path, + tmp_path, + add_sys_paths=True, + creationflags=0, ) finally: From 4fefd1e8e5270e77b9a66778373873e99513c526 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:53:50 +0200 Subject: [PATCH 08/22] Fix grammar Co-authored-by: Roy Nieterau --- client/ayon_core/addon/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 4c27cca9d7..0c173ee6f6 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -71,8 +71,8 @@ MOVED_ADDON_MILESTONE_VERSIONS = { class ProcessPreparationError(Exception): """Exception that can be used when process preparation failed. - The message is showed to user (either as UI dialog or printed). If - different error is raised a "generic" error message is showed to user + The message is shown to user (either as UI dialog or printed). If + different error is raised a "generic" error message is shown to user with option to copy error message to clipboard. """ @@ -654,7 +654,7 @@ class AYONAddon(object): Implementation of this method is optional. Note: - The logic can be similar to logic in tray, but in tray not require + The logic can be similar to logic in tray, but tray does not require to be logged in. Args: From e7f816795314a931e80f28a26c6a5eb4efa70d31 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 10:55:18 +0200 Subject: [PATCH 09/22] store tracebacks in case stdout is not available --- client/ayon_core/addon/utils.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index 3ad112b562..66c2d81a79 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -93,35 +93,50 @@ def ensure_addons_are_process_ready( message = None failed = False use_detail = False + # Wrap the output in StringIO to capture it for details on fail + # - but in case stdout was invalid on start of process also store + # the tracebacks + tracebacks = [] output = StringIO() with contextlib.redirect_stdout(output): with contextlib.redirect_stderr(output): for addon in addons_manager.get_enabled_addons(): - failed = True + addon_failed = True try: addon.ensure_is_process_ready(process_context) - failed = False + addon_failed = False except ProcessPreparationError as exc: message = str(exc) print(f"Addon preparation failed: '{addon.name}'") print(message) except BaseException as exc: + use_detail = True message = "An unexpected error occurred." + formatted_traceback = "".join(traceback.format_exception( + *sys.exc_info() + )) + tracebacks.append(formatted_traceback) print(f"Addon preparation failed: '{addon.name}'") print(message) - # Print the traceback so it is in the output - traceback.print_exception(*sys.exc_info()) - use_detail = True + # Print the traceback so it is in the stdout + print(formatted_traceback) - if failed: + if addon_failed: + failed = True break output_str = output.getvalue() # Print stdout/stderr to console as it was redirected print(output_str) if failed: - detail = output_str if use_detail else None + detail = None + if use_detail: + # In case stdout was not captured, use the tracebacks + if not output_str: + output_str = "\n".join(tracebacks) + detail = output_str + _handle_error(process_context, message, detail) if not exit_on_failure: return exc From e15a2b275f4373025a18c6d9b7a9fe72d2a81edd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 Jul 2024 11:00:30 +0200 Subject: [PATCH 10/22] fix ruff linting --- client/ayon_core/addon/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index 66c2d81a79..8b7740c89c 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -89,7 +89,7 @@ def ensure_addons_are_process_ready( if addons_manager is None: addons_manager = AddonsManager() - exc = None + exception = None message = None failed = False use_detail = False @@ -106,11 +106,13 @@ def ensure_addons_are_process_ready( addon.ensure_is_process_ready(process_context) addon_failed = False except ProcessPreparationError as exc: + exception = exc message = str(exc) print(f"Addon preparation failed: '{addon.name}'") print(message) except BaseException as exc: + exception = exc use_detail = True message = "An unexpected error occurred." formatted_traceback = "".join(traceback.format_exception( @@ -139,5 +141,5 @@ def ensure_addons_are_process_ready( _handle_error(process_context, message, detail) if not exit_on_failure: - return exc + return exception sys.exit(1) From ddd5313a415b67c4d1af4ece9d178932f73a79fc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 Jul 2024 13:42:23 +0200 Subject: [PATCH 11/22] add optional output of ensude function --- client/ayon_core/addon/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index 8b7740c89c..caa6b70a85 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -66,7 +66,7 @@ def ensure_addons_are_process_ready( process_context: ProcessContext, addons_manager: Optional[AddonsManager] = None, exit_on_failure: bool = True, -): +) -> Optional[Exception]: """Ensure all enabled addons are ready to be used in the given context. Call this method only in AYON launcher process and as first thing From f8e1b6eeb106666c1904eb27418a2c3cc789f605 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:59:28 +0200 Subject: [PATCH 12/22] added one more ensure function for easier approach. --- client/ayon_core/addon/__init__.py | 2 ++ client/ayon_core/addon/utils.py | 33 ++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/addon/__init__.py b/client/ayon_core/addon/__init__.py index 497987fa22..6a7ce8a3cb 100644 --- a/client/ayon_core/addon/__init__.py +++ b/client/ayon_core/addon/__init__.py @@ -17,6 +17,7 @@ from .base import ( ) from .utils import ( + ensure_addons_are_process_context_ready, ensure_addons_are_process_ready, ) @@ -36,5 +37,6 @@ __all__ = ( "AddonsManager", "load_addons", + "ensure_addons_are_process_context_ready", "ensure_addons_are_process_ready", ) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index caa6b70a85..d96809de49 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -62,7 +62,7 @@ def _handle_error( os.remove(tmp_path) -def ensure_addons_are_process_ready( +def ensure_addons_are_process_context_ready( process_context: ProcessContext, addons_manager: Optional[AddonsManager] = None, exit_on_failure: bool = True, @@ -134,7 +134,7 @@ def ensure_addons_are_process_ready( if failed: detail = None if use_detail: - # In case stdout was not captured, use the tracebacks + # In case stdout was not captured, use the tracebacks as detail if not output_str: output_str = "\n".join(tracebacks) detail = output_str @@ -143,3 +143,32 @@ def ensure_addons_are_process_ready( if not exit_on_failure: return exception sys.exit(1) + + +def ensure_addons_are_process_ready( + addons_manager: Optional[AddonsManager] = None, + exit_on_failure: bool = True, + **kwargs, +) -> Optional[Exception]: + """Ensure all enabled addons are ready to be used in the given context. + + Call this method only in AYON launcher process and as first thing + to avoid possible clashes with preparation. For example 'QApplication' + should not be created. + + Args: + addons_manager (Optional[AddonsManager]): The addons + manager to use. If not provided, a new one will be created. + exit_on_failure (bool, optional): If True, the process will exit + if an error occurs. Defaults to True. + kwargs: The keyword arguments to pass to the ProcessContext. + + Returns: + Optional[Exception]: The exception that occurred during the + preparation, if any. + + """ + context: ProcessContext = ProcessContext(**kwargs) + return ensure_addons_are_process_context_ready( + context, addons_manager, exit_on_failure + ) From 943245b698557f1bd8fa36aab30d6c2fc8547e3f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:05:20 +0200 Subject: [PATCH 13/22] start tray if is not running --- client/ayon_core/addon/utils.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index d96809de49..b6f665b677 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -62,6 +62,12 @@ def _handle_error( os.remove(tmp_path) +def _start_tray(process_context): + from ayon_core.tools.tray import make_sure_tray_is_running + + make_sure_tray_is_running() + + def ensure_addons_are_process_context_ready( process_context: ProcessContext, addons_manager: Optional[AddonsManager] = None, @@ -131,18 +137,22 @@ def ensure_addons_are_process_context_ready( output_str = output.getvalue() # Print stdout/stderr to console as it was redirected print(output_str) - if failed: - detail = None - if use_detail: - # In case stdout was not captured, use the tracebacks as detail - if not output_str: - output_str = "\n".join(tracebacks) - detail = output_str + if not failed: + if not process_context.headless: + _start_tray(process_context) + return None - _handle_error(process_context, message, detail) - if not exit_on_failure: - return exception - sys.exit(1) + detail = None + if use_detail: + # In case stdout was not captured, use the tracebacks as detail + if not output_str: + output_str = "\n".join(tracebacks) + detail = output_str + + _handle_error(process_context, message, detail) + if not exit_on_failure: + return exception + sys.exit(1) def ensure_addons_are_process_ready( From fa729cdcdd3b04c2e03fa15b157ec5869f4872d5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:29:25 +0200 Subject: [PATCH 14/22] don't require process context to start tray function --- client/ayon_core/addon/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/addon/utils.py b/client/ayon_core/addon/utils.py index b6f665b677..ac5ff25984 100644 --- a/client/ayon_core/addon/utils.py +++ b/client/ayon_core/addon/utils.py @@ -62,7 +62,7 @@ def _handle_error( os.remove(tmp_path) -def _start_tray(process_context): +def _start_tray(): from ayon_core.tools.tray import make_sure_tray_is_running make_sure_tray_is_running() @@ -139,7 +139,7 @@ def ensure_addons_are_process_context_ready( print(output_str) if not failed: if not process_context.headless: - _start_tray(process_context) + _start_tray() return None detail = None From 8cd6a2f342431d70afa9433b75d026ec7bb78e0d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:26:34 +0200 Subject: [PATCH 15/22] wrap tray info into object --- client/ayon_core/tools/tray/lib.py | 311 +++++++++++++++++++++-------- 1 file changed, 233 insertions(+), 78 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index ad190482a8..e6de884cb5 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -144,17 +144,6 @@ def get_tray_storage_dir() -> str: return get_ayon_appdirs("tray") -def _get_tray_information(tray_url: str) -> Optional[Dict[str, Any]]: - if not tray_url: - return None - try: - response = requests.get(f"{tray_url}/tray") - response.raise_for_status() - return response.json() - except (requests.HTTPError, requests.ConnectionError): - return None - - def _get_tray_info_filepath( server_url: Optional[str] = None, variant: Optional[str] = None @@ -165,6 +154,51 @@ def _get_tray_info_filepath( return os.path.join(hash_dir, filename) +def _get_tray_file_info( + server_url: Optional[str] = None, + variant: Optional[str] = None +) -> Tuple[Optional[Dict[str, Any]], Optional[float]]: + filepath = _get_tray_info_filepath(server_url, variant) + if not os.path.exists(filepath): + return None, None + file_modified = os.path.getmtime(filepath) + try: + with open(filepath, "r") as stream: + data = json.load(stream) + except Exception: + return None, file_modified + + return data, file_modified + + +def _remove_tray_server_url( + server_url: Optional[str], + variant: Optional[str], + file_modified: Optional[float], +): + """Remove tray information file. + + Called from tray logic, do not use on your own. + + Args: + server_url (Optional[str]): AYON server url. + variant (Optional[str]): Settings variant. + file_modified (Optional[float]): File modified timestamp. Is validated + against current state of file. + + """ + filepath = _get_tray_info_filepath(server_url, variant) + if not os.path.exists(filepath): + return + + if ( + file_modified is not None + and os.path.getmtime(filepath) != file_modified + ): + return + os.remove(filepath) + + def get_tray_file_info( server_url: Optional[str] = None, variant: Optional[str] = None @@ -182,15 +216,156 @@ def get_tray_file_info( Optional[Dict[str, Any]]: Tray information. """ - filepath = _get_tray_info_filepath(server_url, variant) - if not os.path.exists(filepath): + file_info, _ = _get_tray_file_info(server_url, variant) + return file_info + + +def _get_tray_rest_information(tray_url: str) -> Optional[Dict[str, Any]]: + if not tray_url: return None try: - with open(filepath, "r") as stream: - data = json.load(stream) - except Exception: + response = requests.get(f"{tray_url}/tray") + response.raise_for_status() + return response.json() + except (requests.HTTPError, requests.ConnectionError): return None - return data + + +class TrayInfo: + def __init__( + self, + server_url: str, + variant: str, + timeout: Optional[int] = None + ): + self.server_url = server_url + self.variant = variant + + if timeout is None: + timeout = 10 + + self._timeout = timeout + + self._file_modified = None + self._file_info = None + self._file_info_cached = False + self._tray_info = None + self._tray_info_cached = False + self._file_state = None + self._state = None + + @classmethod + def new( + cls, + server_url: Optional[str] = None, + variant: Optional[str] = None, + timeout: Optional[int] = None, + wait_to_start: Optional[bool] = True + ) -> "TrayInfo": + server_url, variant = _get_server_and_variant(server_url, variant) + obj = cls(server_url, variant, timeout=timeout) + if wait_to_start: + obj.wait_to_start() + return obj + + def get_pid(self) -> Optional[int]: + file_info = self.get_file_info() + if file_info: + return file_info.get("pid") + return None + + def reset(self): + self._file_modified = None + self._file_info = None + self._file_info_cached = False + self._tray_info = None + self._tray_info_cached = False + self._state = None + self._file_state = None + + def get_file_info(self) -> Optional[Dict[str, Any]]: + if not self._file_info_cached: + file_info, file_modified = _get_tray_file_info( + self.server_url, self.variant + ) + self._file_info = file_info + self._file_modified = file_modified + self._file_info_cached = True + return self._file_info + + def get_file_url(self) -> Optional[str]: + file_info = self.get_file_info() + if file_info: + return file_info.get("url") + return None + + def get_tray_url(self) -> Optional[str]: + info = self.get_tray_info() + if info: + return self.get_file_url() + return None + + def get_tray_info(self) -> Optional[Dict[str, Any]]: + if self._tray_info_cached: + return self._tray_info + + tray_url = self.get_file_url() + tray_info = None + if tray_url: + tray_info = _get_tray_rest_information(tray_url) + + self._tray_info = tray_info + self._tray_info_cached = True + return self._tray_info + + def get_file_state(self) -> int: + if self._file_state is not None: + return self._file_state + + state = TrayState.NOT_RUNNING + file_info = self.get_file_info() + if file_info: + state = TrayState.STARTING + if file_info.get("started") is True: + state = TrayState.RUNNING + self._file_state = state + return self._file_state + + def get_state(self) -> int: + if self._state is not None: + return self._state + + state = self.get_file_state() + if state == TrayState.RUNNING and not self.get_tray_info(): + state = TrayState.NOT_RUNNING + pid = self.pid + if pid: + _kill_tray_process(pid) + # Remove the file as tray is not running anymore and update + # the state of this object. + _remove_tray_server_url( + self.server_url, self.variant, self._file_modified + ) + self.reset() + + self._state = state + return self._state + + def get_ayon_username(self) -> Optional[str]: + tray_info = self.get_tray_info() + if tray_info: + return tray_info.get("username") + return None + + def wait_to_start(self) -> bool: + _wait_for_starting_tray( + self.server_url, self.variant, self._timeout + ) + self.reset() + return self.get_file_state() == TrayState.RUNNING + + pid = property(get_pid) + state = property(get_state) def get_tray_server_url( @@ -214,25 +389,12 @@ def get_tray_server_url( Optional[str]: Tray server url. """ - data = get_tray_file_info(server_url, variant) - if data is None: - return None - - if data.get("started") is False: - data = _wait_for_starting_tray(server_url, variant, timeout) - if data is None: - return None - - url = data.get("url") - if not url: - return None - - if not validate: - return url - - if _get_tray_information(url): - return url - return None + tray_info = TrayInfo.new( + server_url, variant, timeout, wait_to_start=True + ) + if validate: + return tray_info.get_tray_url() + return tray_info.get_file_url() def set_tray_server_url(tray_url: Optional[str], started: bool): @@ -246,10 +408,13 @@ def set_tray_server_url(tray_url: Optional[str], started: bool): that tray is starting up. """ - file_info = get_tray_file_info() - if file_info and file_info["pid"] != os.getpid(): - if not file_info["started"] or _get_tray_information(file_info["url"]): - raise TrayIsRunningError("Tray is already running.") + info = TrayInfo.new(wait_to_start=False) + if ( + info.pid + and info.pid != os.getpid() + and info.state in (TrayState.RUNNING, TrayState.STARTING) + ): + raise TrayIsRunningError("Tray is already running.") filepath = _get_tray_info_filepath() os.makedirs(os.path.dirname(filepath), exist_ok=True) @@ -292,20 +457,21 @@ def remove_tray_server_url(force: Optional[bool] = False): def get_tray_information( server_url: Optional[str] = None, - variant: Optional[str] = None -) -> Optional[Dict[str, Any]]: + variant: Optional[str] = None, + timeout: Optional[int] = None, +) -> TrayInfo: """Get information about tray. Args: server_url (Optional[str]): AYON server url. variant (Optional[str]): Settings variant. + timeout (Optional[int]): Timeout for tray start-up. Returns: - Optional[Dict[str, Any]]: Tray information. + TrayInfo: Tray information. """ - tray_url = get_tray_server_url(server_url, variant) - return _get_tray_information(tray_url) + return TrayInfo.new(server_url, variant, timeout) def get_tray_state( @@ -322,20 +488,8 @@ def get_tray_state( int: Tray state. """ - file_info = get_tray_file_info(server_url, variant) - if file_info is None: - return TrayState.NOT_RUNNING - - if file_info.get("started") is False: - return TrayState.STARTING - - tray_url = file_info.get("url") - info = _get_tray_information(tray_url) - if not info: - # Remove the information as the tray is not running - remove_tray_server_url(force=True) - return TrayState.NOT_RUNNING - return TrayState.RUNNING + tray_info = get_tray_information(server_url, variant) + return tray_info.state def is_tray_running( @@ -435,38 +589,39 @@ def main(force=False): Logger.set_process_name("Tray") - state = get_tray_state() - if force and state in (TrayState.RUNNING, TrayState.STARTING): - file_info = get_tray_file_info() or {} - pid = file_info.get("pid") + tray_info = TrayInfo.new(wait_to_start=False) + file_state = tray_info.get_file_state() + if force and file_state in (TrayState.RUNNING, TrayState.STARTING): + pid = tray_info.pid if pid is not None: _kill_tray_process(pid) remove_tray_server_url(force=True) - state = TrayState.NOT_RUNNING + file_state = TrayState.NOT_RUNNING - if state == TrayState.RUNNING: - show_message_in_tray( - "Tray is already running", - "Your AYON tray application is already running." - ) - print("Tray is already running.") - return + if file_state == TrayState.RUNNING: + if tray_info.get_state() == TrayState.RUNNING: + show_message_in_tray( + "Tray is already running", + "Your AYON tray application is already running." + ) + print("Tray is already running.") + return + file_state = tray_info.get_file_state() - if state == TrayState.STARTING: + if file_state == TrayState.STARTING: print("Tray is starting. Waiting for it to start.") - _wait_for_starting_tray() - state = get_tray_state() - if state == TrayState.RUNNING: + tray_info.wait_to_start() + file_state = tray_info.get_file_state() + if file_state == TrayState.RUNNING: print("Tray started. Exiting.") return - if state == TrayState.STARTING: + if file_state == TrayState.STARTING: print( "Tray did not start in expected time." " Killing the process and starting new." ) - file_info = get_tray_file_info() or {} - pid = file_info.get("pid") + pid = tray_info.pid if pid is not None: _kill_tray_process(pid) remove_tray_server_url(force=True) From 041b93d09a264800c6fc34f4a32c6b15036dd4df Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:26:16 +0200 Subject: [PATCH 16/22] use power of sets --- client/ayon_core/lib/execute.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/lib/execute.py b/client/ayon_core/lib/execute.py index f61d892324..1c73a97731 100644 --- a/client/ayon_core/lib/execute.py +++ b/client/ayon_core/lib/execute.py @@ -210,11 +210,12 @@ def run_ayon_launcher_process(*args, add_sys_paths=False, **kwargs): env = clean_envs_for_ayon_process(os.environ) if add_sys_paths: - new_pythonpath = list(sys.path) - for path in env.get("PYTHONPATH", "").split(os.pathsep): - if not path or path in new_pythonpath: - continue - new_pythonpath.append(path) + pp_set = frozenset(sys.path) + new_pythonpath = list(pp_set) + pythonpath = env.get("PYTHONPATH") or "" + for path in frozenset(pythonpath.split(os.pathsep)): + if path and path not in pp_set: + new_pythonpath.append(path) env["PYTHONPATH"] = os.pathsep.join(new_pythonpath) return run_subprocess(args, env=env, **kwargs) From 3e0d422ed5b7d9243fc85b33825e816d03d0e3e5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 1 Aug 2024 17:03:58 +0200 Subject: [PATCH 17/22] fix paths ordering --- client/ayon_core/lib/execute.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/lib/execute.py b/client/ayon_core/lib/execute.py index 1c73a97731..23da34b6fa 100644 --- a/client/ayon_core/lib/execute.py +++ b/client/ayon_core/lib/execute.py @@ -210,8 +210,8 @@ def run_ayon_launcher_process(*args, add_sys_paths=False, **kwargs): env = clean_envs_for_ayon_process(os.environ) if add_sys_paths: - pp_set = frozenset(sys.path) - new_pythonpath = list(pp_set) + new_pythonpath = list(sys.path) + pp_set = set(new_pythonpath) pythonpath = env.get("PYTHONPATH") or "" for path in frozenset(pythonpath.split(os.pathsep)): if path and path not in pp_set: From a90c7a0f9029a7587d9bc35182615ce31028bda5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 1 Aug 2024 17:05:48 +0200 Subject: [PATCH 18/22] update lookup set --- client/ayon_core/lib/execute.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/lib/execute.py b/client/ayon_core/lib/execute.py index 23da34b6fa..bc55c27bd8 100644 --- a/client/ayon_core/lib/execute.py +++ b/client/ayon_core/lib/execute.py @@ -211,11 +211,11 @@ def run_ayon_launcher_process(*args, add_sys_paths=False, **kwargs): if add_sys_paths: new_pythonpath = list(sys.path) - pp_set = set(new_pythonpath) - pythonpath = env.get("PYTHONPATH") or "" - for path in frozenset(pythonpath.split(os.pathsep)): - if path and path not in pp_set: + lookup_set = set(new_pythonpath) + for path in (env.get("PYTHONPATH") or "").split(os.pathsep): + if path and path not in lookup_set: new_pythonpath.append(path) + lookup_set.add(path) env["PYTHONPATH"] = os.pathsep.join(new_pythonpath) return run_subprocess(args, env=env, **kwargs) From e10a78d761210ae8f9484c653b049d02a16444bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:22:49 +0200 Subject: [PATCH 19/22] shutdown existing tray if is running under different user --- client/ayon_core/tools/tray/lib.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index e6de884cb5..f09f400dfa 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -13,7 +13,12 @@ from typing import Optional, Dict, Tuple, Any import ayon_api import requests -from ayon_core.lib import Logger, get_ayon_launcher_args, run_detached_process +from ayon_core.lib import ( + Logger, + get_ayon_launcher_args, + run_detached_process, + get_ayon_username, +) from ayon_core.lib.local_settings import get_ayon_appdirs @@ -590,6 +595,7 @@ def main(force=False): Logger.set_process_name("Tray") tray_info = TrayInfo.new(wait_to_start=False) + file_state = tray_info.get_file_state() if force and file_state in (TrayState.RUNNING, TrayState.STARTING): pid = tray_info.pid @@ -598,6 +604,17 @@ def main(force=False): remove_tray_server_url(force=True) file_state = TrayState.NOT_RUNNING + if file_state in (TrayState.RUNNING, TrayState.STARTING): + expected_username = get_ayon_username() + username = tray_info.get_ayon_username() + # TODO probably show some message to the user??? + if expected_username != username: + pid = tray_info.pid + if pid is not None: + _kill_tray_process(pid) + remove_tray_server_url(force=True) + file_state = TrayState.NOT_RUNNING + if file_state == TrayState.RUNNING: if tray_info.get_state() == TrayState.RUNNING: show_message_in_tray( From d7ab1328cfeab813e55ca0c0c5dd5f590e03e6cc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:42:22 +0200 Subject: [PATCH 20/22] 'make_sure_tray_is_running' is also handling username --- client/ayon_core/tools/tray/lib.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index f09f400dfa..0d0ead85d4 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -551,6 +551,7 @@ def show_message_in_tray( def make_sure_tray_is_running( ayon_url: Optional[str] = None, variant: Optional[str] = None, + username: Optional[str] = None, env: Optional[Dict[str, str]] = None ): """Make sure that tray for AYON url and variant is running. @@ -558,19 +559,26 @@ def make_sure_tray_is_running( Args: ayon_url (Optional[str]): AYON server url. variant (Optional[str]): Settings variant. + username (Optional[str]): Username under which should be tray running. env (Optional[Dict[str, str]]): Environment variables for the process. """ - state = get_tray_state(ayon_url, variant) - if state == TrayState.RUNNING: - return + tray_info = TrayInfo.new( + ayon_url, variant, wait_to_start=False + ) + if tray_info.state == TrayState.STARTING: + tray_info.wait_to_start() - if state == TrayState.STARTING: - _wait_for_starting_tray(ayon_url, variant) - state = get_tray_state(ayon_url, variant) - if state == TrayState.RUNNING: + if tray_info.state == TrayState.RUNNING: + if not username: + username = get_ayon_username() + if tray_info.get_ayon_username() == username: return + pid = tray_info.pid + if pid is not None: + _kill_tray_process(pid) + args = get_ayon_launcher_args("tray", "--force") if env is None: env = os.environ.copy() From 0a05731413b411a0fa554b727288e74b13f08aeb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:43:15 +0200 Subject: [PATCH 21/22] don't kill tray in make sure function --- client/ayon_core/tools/tray/lib.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 0d0ead85d4..7fec47ee56 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -575,10 +575,6 @@ def make_sure_tray_is_running( if tray_info.get_ayon_username() == username: return - pid = tray_info.pid - if pid is not None: - _kill_tray_process(pid) - args = get_ayon_launcher_args("tray", "--force") if env is None: env = os.environ.copy() From 60c919cd3386de4b7b79261806a29ca972e52745 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:24:18 +0200 Subject: [PATCH 22/22] use 'get_default_settings_variant' from utils --- client/ayon_core/tools/tray/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 7fec47ee56..fd84a9bd10 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -10,8 +10,8 @@ import signal import locale from typing import Optional, Dict, Tuple, Any -import ayon_api import requests +from ayon_api.utils import get_default_settings_variant from ayon_core.lib import ( Logger, @@ -39,7 +39,7 @@ def _get_default_server_url() -> str: def _get_default_variant() -> str: """Get default settings variant.""" - return ayon_api.get_default_settings_variant() + return get_default_settings_variant() def _get_server_and_variant(