From a0f6a3f37971c30390f5a1b99d81f1a582ab5122 Mon Sep 17 00:00:00 2001 From: Aleks Berland Date: Mon, 25 Aug 2025 19:09:20 -0400 Subject: [PATCH 01/19] Implement upload retries for reviewable files and add user-friendly error handling in case of timeout. Update validation help documentation for upload failures. --- .../publish/help/validate_publish_dir.xml | 18 +++++ .../plugins/publish/integrate_review.py | 68 +++++++++++++++++-- 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/plugins/publish/help/validate_publish_dir.xml b/client/ayon_core/plugins/publish/help/validate_publish_dir.xml index 9f62b264bf..0449e61fa2 100644 --- a/client/ayon_core/plugins/publish/help/validate_publish_dir.xml +++ b/client/ayon_core/plugins/publish/help/validate_publish_dir.xml @@ -1,5 +1,23 @@ + +Review upload timed out + +## Review upload failed after retries + +The connection to the AYON server timed out while uploading a reviewable file. + +### How to repair? + +1. Try publishing again. Intermittent network hiccups often resolve on retry. +2. Ensure your network/VPN is stable and large uploads are allowed. +3. If it keeps failing, try again later or contact your admin. + +
File: {file}
+Error: {error}
+ +
+
Source directory not collected diff --git a/client/ayon_core/plugins/publish/integrate_review.py b/client/ayon_core/plugins/publish/integrate_review.py index 0a6b24adb4..c7ac5038d3 100644 --- a/client/ayon_core/plugins/publish/integrate_review.py +++ b/client/ayon_core/plugins/publish/integrate_review.py @@ -1,11 +1,14 @@ import os +import time -import pyblish.api import ayon_api +import pyblish.api from ayon_api.server_api import RequestTypes - from ayon_core.lib import get_media_mime_type -from ayon_core.pipeline.publish import get_publish_repre_path +from ayon_core.pipeline.publish import ( + PublishXmlValidationError, + get_publish_repre_path, +) class IntegrateAYONReview(pyblish.api.InstancePlugin): @@ -82,11 +85,12 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): headers = ayon_con.get_headers(content_type) headers["x-file-name"] = filename self.log.info(f"Uploading reviewable {repre_path}") - ayon_con.upload_file( + # Upload with retries and clear help if it keeps failing + self._upload_with_retries( + ayon_con, endpoint, repre_path, - headers=headers, - request_type=RequestTypes.post, + headers, ) def _get_review_label(self, repre, uploaded_labels): @@ -100,3 +104,55 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): idx += 1 label = f"{orig_label}_{idx}" return label + + def _upload_with_retries( + self, + ayon_con, + endpoint, + repre_path, + headers, + max_retries: int = 3, + backoff_seconds: int = 2, + ): + """Upload file with simple exponential backoff retries. + + If all retries fail we raise a PublishXmlValidationError with a help key + to guide the user to retry publish. + """ + last_error = None + for attempt in range(1, max_retries + 1): + try: + ayon_con.upload_file( + endpoint, + repre_path, + headers=headers, + request_type=RequestTypes.post, + ) + return + except Exception as exc: # noqa: BLE001 - bubble after retries + last_error = exc + # Log and retry with backoff if attempts remain + if attempt < max_retries: + wait = backoff_seconds * (2 ** (attempt - 1)) + self.log.warning( + f"Review upload failed (attempt {attempt}/{max_retries}): {exc}. " + f"Retrying in {wait}s..." + ) + try: + time.sleep(wait) + except Exception: # Sleep errors are highly unlikely; continue + pass + else: + # Exhausted retries - raise a user-friendly validation error with help + raise PublishXmlValidationError( + self, + ( + "Upload of reviewable timed out or failed after multiple attempts. " + "Please try publishing again." + ), + key="upload_timeout", + formatting_data={ + "file": repre_path, + "error": str(last_error), + }, + ) From 32c022cd4daeb4027f88021a0a5ea2163734f9de Mon Sep 17 00:00:00 2001 From: Aleks Berland Date: Tue, 26 Aug 2025 09:55:47 -0400 Subject: [PATCH 02/19] Refactor upload retry logic to handle only transient network issues and improve error handling --- .../plugins/publish/integrate_review.py | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_review.py b/client/ayon_core/plugins/publish/integrate_review.py index c7ac5038d3..f9fa862320 100644 --- a/client/ayon_core/plugins/publish/integrate_review.py +++ b/client/ayon_core/plugins/publish/integrate_review.py @@ -9,6 +9,10 @@ from ayon_core.pipeline.publish import ( PublishXmlValidationError, get_publish_repre_path, ) +from requests import exceptions as req_exc + +# Narrow retryable failures to transient network issues +RETRYABLE_EXCEPTIONS = (req_exc.Timeout, req_exc.ConnectionError) class IntegrateAYONReview(pyblish.api.InstancePlugin): @@ -47,7 +51,7 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): if "webreview" not in repre_tags: continue - # exclude representations with are going to be published on farm + # exclude representations going to be published on farm if "publish_on_farm" in repre_tags: continue @@ -120,7 +124,8 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): to guide the user to retry publish. """ last_error = None - for attempt in range(1, max_retries + 1): + for attempt in range(max_retries): + attempt_num = attempt + 1 try: ayon_con.upload_file( endpoint, @@ -129,30 +134,36 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): request_type=RequestTypes.post, ) return - except Exception as exc: # noqa: BLE001 - bubble after retries + except RETRYABLE_EXCEPTIONS as exc: last_error = exc # Log and retry with backoff if attempts remain - if attempt < max_retries: - wait = backoff_seconds * (2 ** (attempt - 1)) + if attempt_num < max_retries: + wait = backoff_seconds * (2 ** attempt) self.log.warning( - f"Review upload failed (attempt {attempt}/{max_retries}): {exc}. " - f"Retrying in {wait}s..." + "Review upload failed (attempt %s/%s). Retrying in %ss...", + attempt_num, max_retries, wait, + exc_info=True, ) try: time.sleep(wait) - except Exception: # Sleep errors are highly unlikely; continue + except Exception: pass else: - # Exhausted retries - raise a user-friendly validation error with help - raise PublishXmlValidationError( - self, - ( - "Upload of reviewable timed out or failed after multiple attempts. " - "Please try publishing again." - ), - key="upload_timeout", - formatting_data={ - "file": repre_path, - "error": str(last_error), - }, - ) + break + except Exception: + # Non retryable failures bubble immediately + raise + + # Exhausted retries - raise a user-friendly validation error with help + raise PublishXmlValidationError( + self, + ( + "Upload of reviewable timed out or failed after multiple attempts." + " Please try publishing again." + ), + key="upload_timeout", + formatting_data={ + "file": repre_path, + "error": str(last_error), + }, + ) From f665528ee7a64e416bab859537b24a8eaabaec00 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:36:01 +0100 Subject: [PATCH 03/19] fix 'product_name' to 'name' --- client/ayon_core/plugins/publish/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 8fce5574e9..9f24b35754 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -399,7 +399,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): product_id = existing_product_entity["id"] new_product_entity_kwargs = { - "product_name": product_name, + "name": product_name, "product_type": product_type, "folder_id": folder_entity["id"], "data": data, From fb2df3397063a5f06184cb2484d2c849d4d9d948 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:39:13 +0100 Subject: [PATCH 04/19] added option to define different help file --- client/ayon_core/pipeline/publish/lib.py | 17 +++++--- .../pipeline/publish/publish_plugins.py | 41 ++++++++++++++++--- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 7365ffee09..e512a0116f 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -192,7 +192,9 @@ class HelpContent: self.detail = detail -def load_help_content_from_filepath(filepath): +def load_help_content_from_filepath( + filepath: str +) -> dict[str, dict[str, HelpContent]]: """Load help content from xml file. Xml file may contain errors and warnings. """ @@ -227,15 +229,20 @@ def load_help_content_from_filepath(filepath): return output -def load_help_content_from_plugin(plugin): +def load_help_content_from_plugin( + plugin: pyblish.api.Plugin, + help_filename: Optional[str] = None, +) -> dict[str, dict[str, HelpContent]]: cls = plugin if not inspect.isclass(plugin): cls = plugin.__class__ + plugin_filepath = inspect.getfile(cls) plugin_dir = os.path.dirname(plugin_filepath) - basename = os.path.splitext(os.path.basename(plugin_filepath))[0] - filename = basename + ".xml" - filepath = os.path.join(plugin_dir, "help", filename) + if help_filename is None: + basename = os.path.splitext(os.path.basename(plugin_filepath))[0] + help_filename = basename + ".xml" + filepath = os.path.join(plugin_dir, "help", help_filename) return load_help_content_from_filepath(filepath) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index cc6887e762..90b8e90a3c 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -1,7 +1,7 @@ import inspect from abc import ABCMeta import typing -from typing import Optional +from typing import Optional, Any import pyblish.api import pyblish.logic @@ -82,22 +82,51 @@ class PublishValidationError(PublishError): class PublishXmlValidationError(PublishValidationError): + """Raise an error from a dedicated xml file. + + Can be useful to have one xml file with different possible messages that + helps to avoid flood code with dedicated artist messages. + + XML files should live relative to the plugin file location: + '{plugin dir}/help/some_plugin.xml'. + + Args: + plugin (pyblish.api.Plugin): Plugin that raised an error. Is used + to get path to xml file. + message (str): Exception message, can be technical, is used for + console output. + key (Optional[str]): XML file can contain multiple error messages, key + is used to get one of them. By default is used 'main'. + formatting_data (Optional[dict[str, Any]): Error message can have + variables to fill. + help_filename (Optional[str]): Name of xml file with messages. By + default, is used filename where plugin lives with .xml extension. + + """ def __init__( - self, plugin, message, key=None, formatting_data=None - ): + self, + plugin: pyblish.api.Plugin, + message: str, + key: Optional[str] = None, + formatting_data: Optional[dict[str, Any]] = None, + help_filename: Optional[str] = None, + ) -> None: if key is None: key = "main" if not formatting_data: formatting_data = {} - result = load_help_content_from_plugin(plugin) + result = load_help_content_from_plugin(plugin, help_filename) content_obj = result["errors"][key] description = content_obj.description.format(**formatting_data) detail = content_obj.detail if detail: detail = detail.format(**formatting_data) - super(PublishXmlValidationError, self).__init__( - message, content_obj.title, description, detail + super().__init__( + message, + content_obj.title, + description, + detail ) From f0bd2b7e98cb74cab1002874ac8b8d88d611957b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:39:44 +0100 Subject: [PATCH 05/19] use different help file for integrate review --- .../plugins/publish/help/upload_file.xml | 21 +++++++++++++++++++ .../publish/help/validate_publish_dir.xml | 18 ---------------- .../plugins/publish/integrate_review.py | 7 ++++--- 3 files changed, 25 insertions(+), 21 deletions(-) create mode 100644 client/ayon_core/plugins/publish/help/upload_file.xml diff --git a/client/ayon_core/plugins/publish/help/upload_file.xml b/client/ayon_core/plugins/publish/help/upload_file.xml new file mode 100644 index 0000000000..8c270c7b19 --- /dev/null +++ b/client/ayon_core/plugins/publish/help/upload_file.xml @@ -0,0 +1,21 @@ + + + +{upload_type} upload timed out + +## {upload_type} upload failed after retries + +The connection to the AYON server timed out while uploading a file. + +### How to resolve? + +1. Try publishing again. Intermittent network hiccups often resolve on retry. +2. Ensure your network/VPN is stable and large uploads are allowed. +3. If it keeps failing, try again later or contact your admin. + +
File: {file}
+Error: {error}
+ +
+
+
diff --git a/client/ayon_core/plugins/publish/help/validate_publish_dir.xml b/client/ayon_core/plugins/publish/help/validate_publish_dir.xml index 0449e61fa2..9f62b264bf 100644 --- a/client/ayon_core/plugins/publish/help/validate_publish_dir.xml +++ b/client/ayon_core/plugins/publish/help/validate_publish_dir.xml @@ -1,23 +1,5 @@ - -Review upload timed out - -## Review upload failed after retries - -The connection to the AYON server timed out while uploading a reviewable file. - -### How to repair? - -1. Try publishing again. Intermittent network hiccups often resolve on retry. -2. Ensure your network/VPN is stable and large uploads are allowed. -3. If it keeps failing, try again later or contact your admin. - -
File: {file}
-Error: {error}
- -
-
Source directory not collected diff --git a/client/ayon_core/plugins/publish/integrate_review.py b/client/ayon_core/plugins/publish/integrate_review.py index f9fa862320..4d091aa17a 100644 --- a/client/ayon_core/plugins/publish/integrate_review.py +++ b/client/ayon_core/plugins/publish/integrate_review.py @@ -158,12 +158,13 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): raise PublishXmlValidationError( self, ( - "Upload of reviewable timed out or failed after multiple attempts." - " Please try publishing again." + "Upload of reviewable timed out or failed after multiple" + " attempts. Please try publishing again." ), - key="upload_timeout", formatting_data={ + "upload_type": "Review", "file": repre_path, "error": str(last_error), }, + help_filename="upload_file.xml", ) From 699673bbf2e55a0f35d9ca966baaec1a0e7705dd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:40:13 +0100 Subject: [PATCH 06/19] slightly modified upload --- .../plugins/publish/integrate_review.py | 67 ++++++++----------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_review.py b/client/ayon_core/plugins/publish/integrate_review.py index 4d091aa17a..06cc2f55b4 100644 --- a/client/ayon_core/plugins/publish/integrate_review.py +++ b/client/ayon_core/plugins/publish/integrate_review.py @@ -9,10 +9,7 @@ from ayon_core.pipeline.publish import ( PublishXmlValidationError, get_publish_repre_path, ) -from requests import exceptions as req_exc - -# Narrow retryable failures to transient network issues -RETRYABLE_EXCEPTIONS = (req_exc.Timeout, req_exc.ConnectionError) +import requests.exceptions class IntegrateAYONReview(pyblish.api.InstancePlugin): @@ -82,19 +79,13 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): f"/projects/{project_name}" f"/versions/{version_id}/reviewables{query}" ) - filename = os.path.basename(repre_path) - # Upload the reviewable - self.log.info(f"Uploading reviewable '{label or filename}' ...") - - headers = ayon_con.get_headers(content_type) - headers["x-file-name"] = filename self.log.info(f"Uploading reviewable {repre_path}") # Upload with retries and clear help if it keeps failing self._upload_with_retries( ayon_con, endpoint, repre_path, - headers, + content_type, ) def _get_review_label(self, repre, uploaded_labels): @@ -111,48 +102,48 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): def _upload_with_retries( self, - ayon_con, - endpoint, - repre_path, - headers, - max_retries: int = 3, - backoff_seconds: int = 2, + ayon_con: ayon_api.ServerAPI, + endpoint: str, + repre_path: str, + content_type: str, ): """Upload file with simple exponential backoff retries. If all retries fail we raise a PublishXmlValidationError with a help key to guide the user to retry publish. """ + # How long to sleep before next attempt + wait_time = 1 + filename = os.path.basename(repre_path) + + headers = ayon_con.get_headers(content_type) + headers["x-file-name"] = filename + max_retries = ayon_con.get_default_max_retries() last_error = None for attempt in range(max_retries): - attempt_num = attempt + 1 + attempt += 1 try: - ayon_con.upload_file( + return ayon_con.upload_file( endpoint, repre_path, headers=headers, request_type=RequestTypes.post, ) - return - except RETRYABLE_EXCEPTIONS as exc: - last_error = exc + + except ( + requests.exceptions.Timeout, + requests.exceptions.ConnectionError + ): # Log and retry with backoff if attempts remain - if attempt_num < max_retries: - wait = backoff_seconds * (2 ** attempt) - self.log.warning( - "Review upload failed (attempt %s/%s). Retrying in %ss...", - attempt_num, max_retries, wait, - exc_info=True, - ) - try: - time.sleep(wait) - except Exception: - pass - else: - break - except Exception: - # Non retryable failures bubble immediately - raise + if attempt >= max_retries: + raise + + self.log.warning( + f"Review upload failed ({attempt}/{max_retries})." + f" Retrying in {wait_time}s...", + exc_info=True, + ) + time.sleep(wait_time) # Exhausted retries - raise a user-friendly validation error with help raise PublishXmlValidationError( From 989c54001c73bf56caa10780227e463faf7b1a45 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:53:30 +0100 Subject: [PATCH 07/19] added retries in thumbnail integration --- .../plugins/publish/integrate_thumbnail.py | 82 +++++++++++++++++-- 1 file changed, 73 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_thumbnail.py b/client/ayon_core/plugins/publish/integrate_thumbnail.py index 067c3470e8..7d36a1c7eb 100644 --- a/client/ayon_core/plugins/publish/integrate_thumbnail.py +++ b/client/ayon_core/plugins/publish/integrate_thumbnail.py @@ -24,11 +24,19 @@ import os import collections +import time import pyblish.api import ayon_api from ayon_api import RequestTypes from ayon_api.operations import OperationsSession +try: + from ayon_api.utils import get_media_mime_type +except ImportError: + from ayon_core.lib import get_media_mime_type +import requests + +from ayon_core.pipeline.publish import PublishXmlValidationError InstanceFilterResult = collections.namedtuple( @@ -170,19 +178,16 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): fix jpeg mime type. """ - mime_type = None - with open(src_filepath, "rb") as stream: - if b"\xff\xd8\xff" == stream.read(3): - mime_type = "image/jpeg" - + mime_type = get_media_mime_type(src_filepath) if mime_type is None: - return ayon_api.create_thumbnail(project_name, src_filepath) + return ayon_api.create_thumbnail( + project_name, src_filepath + ) - response = ayon_api.upload_file( + response = self._upload_with_retries( f"projects/{project_name}/thumbnails", src_filepath, - request_type=RequestTypes.post, - headers={"Content-Type": mime_type}, + mime_type, ) response.raise_for_status() return response.json()["id"] @@ -248,3 +253,62 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): or instance.data.get("name") or "N/A" ) + + def _upload_with_retries( + self, + endpoint: str, + repre_path: str, + content_type: str, + ): + """Upload file with simple exponential backoff retries. + + If all retries fail we raise a PublishXmlValidationError with a help key + to guide the user to retry publish. + """ + # How long to sleep before next attempt + wait_time = 1 + filename = os.path.basename(repre_path) + + ayon_con = ayon_api.get_server_api_connection() + headers = ayon_con.get_headers(content_type) + max_retries = ayon_con.get_default_max_retries() + last_error = None + for attempt in range(max_retries): + attempt += 1 + try: + return ayon_con.upload_file( + endpoint, + repre_path, + headers=headers, + request_type=RequestTypes.post, + ) + + except ( + requests.exceptions.Timeout, + requests.exceptions.ConnectionError + ): + # Log and retry with backoff if attempts remain + if attempt >= max_retries: + raise + + self.log.warning( + f"Review upload failed ({attempt}/{max_retries})." + f" Retrying in {wait_time}s...", + exc_info=True, + ) + time.sleep(wait_time) + + # Exhausted retries - raise a user-friendly validation error with help + raise PublishXmlValidationError( + self, + ( + "Upload of thumbnail timed out or failed after multiple" + " attempts. Please try publishing again." + ), + formatting_data={ + "upload_type": "Thumbnail", + "file": repre_path, + "error": str(last_error), + }, + help_filename="upload_file.xml", + ) From 9b35dd6cfc91ae99d03474888bd80d97a45a47d1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:16:16 +0100 Subject: [PATCH 08/19] remove unused variable --- client/ayon_core/plugins/publish/integrate_thumbnail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate_thumbnail.py b/client/ayon_core/plugins/publish/integrate_thumbnail.py index 7d36a1c7eb..233ab751f6 100644 --- a/client/ayon_core/plugins/publish/integrate_thumbnail.py +++ b/client/ayon_core/plugins/publish/integrate_thumbnail.py @@ -267,7 +267,6 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): """ # How long to sleep before next attempt wait_time = 1 - filename = os.path.basename(repre_path) ayon_con = ayon_api.get_server_api_connection() headers = ayon_con.get_headers(content_type) From e0597ac6de2d5ddf6161fbb2687518d59ee7edb1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:20:02 +0100 Subject: [PATCH 09/19] remove unnecessary imports --- client/ayon_core/plugins/publish/integrate_thumbnail.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_thumbnail.py b/client/ayon_core/plugins/publish/integrate_thumbnail.py index 233ab751f6..e68a4179ae 100644 --- a/client/ayon_core/plugins/publish/integrate_thumbnail.py +++ b/client/ayon_core/plugins/publish/integrate_thumbnail.py @@ -26,16 +26,13 @@ import os import collections import time -import pyblish.api import ayon_api from ayon_api import RequestTypes from ayon_api.operations import OperationsSession -try: - from ayon_api.utils import get_media_mime_type -except ImportError: - from ayon_core.lib import get_media_mime_type +import pyblish.api import requests +from ayon_core.lib import get_media_mime_type from ayon_core.pipeline.publish import PublishXmlValidationError From 647d91e288aaa4e2ad1facab2e907b0936c97632 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:26:02 +0100 Subject: [PATCH 10/19] update docstring --- client/ayon_core/plugins/publish/integrate_review.py | 6 +----- client/ayon_core/plugins/publish/integrate_thumbnail.py | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_review.py b/client/ayon_core/plugins/publish/integrate_review.py index 06cc2f55b4..1b236d9070 100644 --- a/client/ayon_core/plugins/publish/integrate_review.py +++ b/client/ayon_core/plugins/publish/integrate_review.py @@ -107,11 +107,7 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): repre_path: str, content_type: str, ): - """Upload file with simple exponential backoff retries. - - If all retries fail we raise a PublishXmlValidationError with a help key - to guide the user to retry publish. - """ + """Upload file with simple retries.""" # How long to sleep before next attempt wait_time = 1 filename = os.path.basename(repre_path) diff --git a/client/ayon_core/plugins/publish/integrate_thumbnail.py b/client/ayon_core/plugins/publish/integrate_thumbnail.py index e68a4179ae..aef95525cb 100644 --- a/client/ayon_core/plugins/publish/integrate_thumbnail.py +++ b/client/ayon_core/plugins/publish/integrate_thumbnail.py @@ -257,11 +257,7 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): repre_path: str, content_type: str, ): - """Upload file with simple exponential backoff retries. - - If all retries fail we raise a PublishXmlValidationError with a help key - to guide the user to retry publish. - """ + """Upload file with simple retries.""" # How long to sleep before next attempt wait_time = 1 From faff50ce333eeff2e1d69be4f50b8b5a1f72d3a2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:03:19 +0100 Subject: [PATCH 11/19] don't use custom retries if are already handled by ayon api --- .../plugins/publish/integrate_review.py | 18 +++++++++++++----- .../plugins/publish/integrate_thumbnail.py | 17 +++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_review.py b/client/ayon_core/plugins/publish/integrate_review.py index 1b236d9070..cdb16b5ac3 100644 --- a/client/ayon_core/plugins/publish/integrate_review.py +++ b/client/ayon_core/plugins/publish/integrate_review.py @@ -2,8 +2,10 @@ import os import time import ayon_api -import pyblish.api +from ayon_api import TransferProgress from ayon_api.server_api import RequestTypes +import pyblish.api + from ayon_core.lib import get_media_mime_type from ayon_core.pipeline.publish import ( PublishXmlValidationError, @@ -108,13 +110,18 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): content_type: str, ): """Upload file with simple retries.""" - # How long to sleep before next attempt - wait_time = 1 filename = os.path.basename(repre_path) headers = ayon_con.get_headers(content_type) headers["x-file-name"] = filename max_retries = ayon_con.get_default_max_retries() + # Retries are already implemented in 'ayon_api.upload_file' + # - added in ayon api 1.2.7 + if hasattr(TransferProgress, "get_attempt"): + max_retries = 1 + + # How long to sleep before next attempt + wait_time = 1 last_error = None for attempt in range(max_retries): attempt += 1 @@ -129,10 +136,11 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): except ( requests.exceptions.Timeout, requests.exceptions.ConnectionError - ): + ) as exc: # Log and retry with backoff if attempts remain if attempt >= max_retries: - raise + last_error = exc + break self.log.warning( f"Review upload failed ({attempt}/{max_retries})." diff --git a/client/ayon_core/plugins/publish/integrate_thumbnail.py b/client/ayon_core/plugins/publish/integrate_thumbnail.py index aef95525cb..12c5e483e3 100644 --- a/client/ayon_core/plugins/publish/integrate_thumbnail.py +++ b/client/ayon_core/plugins/publish/integrate_thumbnail.py @@ -27,7 +27,7 @@ import collections import time import ayon_api -from ayon_api import RequestTypes +from ayon_api import RequestTypes, TransferProgress from ayon_api.operations import OperationsSession import pyblish.api import requests @@ -258,12 +258,16 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): content_type: str, ): """Upload file with simple retries.""" - # How long to sleep before next attempt - wait_time = 1 - ayon_con = ayon_api.get_server_api_connection() headers = ayon_con.get_headers(content_type) max_retries = ayon_con.get_default_max_retries() + # Retries are already implemented in 'ayon_api.upload_file' + # - added in ayon api 1.2.7 + if hasattr(TransferProgress, "get_attempt"): + max_retries = 1 + + # How long to sleep before next attempt + wait_time = 1 last_error = None for attempt in range(max_retries): attempt += 1 @@ -278,10 +282,11 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): except ( requests.exceptions.Timeout, requests.exceptions.ConnectionError - ): + ) as exc: # Log and retry with backoff if attempts remain if attempt >= max_retries: - raise + last_error = exc + break self.log.warning( f"Review upload failed ({attempt}/{max_retries})." From dde471332fc2b01c88e23277e49924e4a46b6f7d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:33:19 +0100 Subject: [PATCH 12/19] added more logs --- .../ayon_core/plugins/publish/integrate_review.py | 15 ++++++++++++--- .../plugins/publish/integrate_thumbnail.py | 15 ++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_review.py b/client/ayon_core/plugins/publish/integrate_review.py index cdb16b5ac3..6d7e98ae66 100644 --- a/client/ayon_core/plugins/publish/integrate_review.py +++ b/client/ayon_core/plugins/publish/integrate_review.py @@ -6,7 +6,7 @@ from ayon_api import TransferProgress from ayon_api.server_api import RequestTypes import pyblish.api -from ayon_core.lib import get_media_mime_type +from ayon_core.lib import get_media_mime_type, format_file_size from ayon_core.pipeline.publish import ( PublishXmlValidationError, get_publish_repre_path, @@ -120,18 +120,26 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): if hasattr(TransferProgress, "get_attempt"): max_retries = 1 + size = os.path.getsize(repre_path) + self.log.info( + f"Uploading '{repre_path}' (size: {format_file_size(size)})" + ) + # How long to sleep before next attempt wait_time = 1 last_error = None for attempt in range(max_retries): attempt += 1 + start = time.time() try: - return ayon_con.upload_file( + output = ayon_con.upload_file( endpoint, repre_path, headers=headers, request_type=RequestTypes.post, ) + self.log.info(f"Uploade in {time.time() - start}s.") + return output except ( requests.exceptions.Timeout, @@ -143,7 +151,8 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): break self.log.warning( - f"Review upload failed ({attempt}/{max_retries})." + f"Review upload failed ({attempt}/{max_retries})" + f" after {time.time() - start}s." f" Retrying in {wait_time}s...", exc_info=True, ) diff --git a/client/ayon_core/plugins/publish/integrate_thumbnail.py b/client/ayon_core/plugins/publish/integrate_thumbnail.py index 12c5e483e3..36b79570f0 100644 --- a/client/ayon_core/plugins/publish/integrate_thumbnail.py +++ b/client/ayon_core/plugins/publish/integrate_thumbnail.py @@ -32,7 +32,7 @@ from ayon_api.operations import OperationsSession import pyblish.api import requests -from ayon_core.lib import get_media_mime_type +from ayon_core.lib import get_media_mime_type, format_file_size from ayon_core.pipeline.publish import PublishXmlValidationError @@ -266,18 +266,26 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): if hasattr(TransferProgress, "get_attempt"): max_retries = 1 + size = os.path.getsize(repre_path) + self.log.info( + f"Uploading '{repre_path}' (size: {format_file_size(size)})" + ) + # How long to sleep before next attempt wait_time = 1 last_error = None for attempt in range(max_retries): attempt += 1 + start = time.time() try: - return ayon_con.upload_file( + output = ayon_con.upload_file( endpoint, repre_path, headers=headers, request_type=RequestTypes.post, ) + self.log.info(f"Uploade in {time.time() - start}s.") + return output except ( requests.exceptions.Timeout, @@ -289,7 +297,8 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): break self.log.warning( - f"Review upload failed ({attempt}/{max_retries})." + f"Review upload failed ({attempt}/{max_retries})" + f" after {time.time() - start}s." f" Retrying in {wait_time}s...", exc_info=True, ) From 5c17102d16e2af49448798f94dee1bc26ca11f7a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:38:51 +0100 Subject: [PATCH 13/19] remove outdated docstring info --- client/ayon_core/plugins/publish/integrate_thumbnail.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_thumbnail.py b/client/ayon_core/plugins/publish/integrate_thumbnail.py index 36b79570f0..a55a9ac6d8 100644 --- a/client/ayon_core/plugins/publish/integrate_thumbnail.py +++ b/client/ayon_core/plugins/publish/integrate_thumbnail.py @@ -169,12 +169,7 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): return os.path.normpath(filled_path) def _create_thumbnail(self, project_name: str, src_filepath: str) -> str: - """Upload thumbnail to AYON and return its id. - - This is temporary fix of 'create_thumbnail' function in ayon_api to - fix jpeg mime type. - - """ + """Upload thumbnail to AYON and return its id.""" mime_type = get_media_mime_type(src_filepath) if mime_type is None: return ayon_api.create_thumbnail( From ab78158d6ef48b3a28c5ff3ddf0bc01445398d27 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Dec 2025 14:17:00 +0100 Subject: [PATCH 14/19] Fixed typo and use debug level Co-authored-by: Roy Nieterau --- client/ayon_core/plugins/publish/integrate_review.py | 2 +- client/ayon_core/plugins/publish/integrate_thumbnail.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_review.py b/client/ayon_core/plugins/publish/integrate_review.py index 6d7e98ae66..b0cc41acc9 100644 --- a/client/ayon_core/plugins/publish/integrate_review.py +++ b/client/ayon_core/plugins/publish/integrate_review.py @@ -138,7 +138,7 @@ class IntegrateAYONReview(pyblish.api.InstancePlugin): headers=headers, request_type=RequestTypes.post, ) - self.log.info(f"Uploade in {time.time() - start}s.") + self.log.debug(f"Uploaded in {time.time() - start}s.") return output except ( diff --git a/client/ayon_core/plugins/publish/integrate_thumbnail.py b/client/ayon_core/plugins/publish/integrate_thumbnail.py index a55a9ac6d8..60b3a97639 100644 --- a/client/ayon_core/plugins/publish/integrate_thumbnail.py +++ b/client/ayon_core/plugins/publish/integrate_thumbnail.py @@ -279,7 +279,7 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): headers=headers, request_type=RequestTypes.post, ) - self.log.info(f"Uploade in {time.time() - start}s.") + self.log.debug(f"Uploaded in {time.time() - start}s.") return output except ( From f3a2cad425da75d51d38d77dffaa94aab28fb984 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Dec 2025 14:20:28 +0100 Subject: [PATCH 15/19] refresh my tasks filters on refresh --- client/ayon_core/tools/launcher/ui/hierarchy_page.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/ayon_core/tools/launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py index 3c8be4679e..57524e8155 100644 --- a/client/ayon_core/tools/launcher/ui/hierarchy_page.py +++ b/client/ayon_core/tools/launcher/ui/hierarchy_page.py @@ -120,6 +120,8 @@ class HierarchyPage(QtWidgets.QWidget): self._project_name = None + self._my_tasks_filter_enabled = False + # Post init projects_combobox.set_listen_to_selection_change(self._is_visible) @@ -136,6 +138,9 @@ class HierarchyPage(QtWidgets.QWidget): self._folders_widget.refresh() self._tasks_widget.refresh() self._workfiles_page.refresh() + self._on_my_tasks_checkbox_state_changed( + self._my_tasks_filter_enabled + ) def _on_back_clicked(self): self._controller.set_selected_project(None) @@ -155,6 +160,7 @@ class HierarchyPage(QtWidgets.QWidget): ) folder_ids = entity_ids["folder_ids"] task_ids = entity_ids["task_ids"] + self._my_tasks_filter_enabled = enabled self._folders_widget.set_folder_ids_filter(folder_ids) self._tasks_widget.set_task_ids_filter(task_ids) From 9ade73fb27a5f11fe063cbe7fd5bcf007a47df10 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Dec 2025 14:28:35 +0100 Subject: [PATCH 16/19] refresh my tasks in all tools --- client/ayon_core/tools/launcher/ui/hierarchy_page.py | 1 + client/ayon_core/tools/loader/ui/window.py | 4 ++++ .../tools/publisher/widgets/create_context_widgets.py | 5 +++++ client/ayon_core/tools/publisher/widgets/folders_dialog.py | 1 + client/ayon_core/tools/utils/folders_widget.py | 6 ++++++ client/ayon_core/tools/workfiles/widgets/window.py | 6 ++++++ 6 files changed, 23 insertions(+) diff --git a/client/ayon_core/tools/launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py index 57524e8155..575666b64d 100644 --- a/client/ayon_core/tools/launcher/ui/hierarchy_page.py +++ b/client/ayon_core/tools/launcher/ui/hierarchy_page.py @@ -138,6 +138,7 @@ class HierarchyPage(QtWidgets.QWidget): self._folders_widget.refresh() self._tasks_widget.refresh() self._workfiles_page.refresh() + # Update my tasks self._on_my_tasks_checkbox_state_changed( self._my_tasks_filter_enabled ) diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index a6807a1ebb..e4677a62d9 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -527,6 +527,10 @@ class LoaderWindow(QtWidgets.QWidget): if not self._refresh_handler.project_refreshed: self._projects_combobox.refresh() self._update_filters() + # Update my tasks + self._on_my_tasks_checkbox_state_changed( + self._filters_widget.is_my_tasks_checked() + ) def _on_load_finished(self, event): error_info = event["error_info"] diff --git a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py index 49d236353f..405445c8eb 100644 --- a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py @@ -221,6 +221,7 @@ class CreateContextWidget(QtWidgets.QWidget): filters_widget.text_changed.connect(self._on_folder_filter_change) filters_widget.my_tasks_changed.connect(self._on_my_tasks_change) + self._filters_widget = filters_widget self._current_context_btn = current_context_btn self._folders_widget = folders_widget self._tasks_widget = tasks_widget @@ -290,6 +291,10 @@ class CreateContextWidget(QtWidgets.QWidget): self._hierarchy_controller.set_expected_selection( self._last_project_name, folder_id, task_name ) + # Update my tasks + self._on_my_tasks_change( + self._filters_widget.is_my_tasks_checked() + ) def _clear_selection(self): self._folders_widget.set_selected_folder(None) diff --git a/client/ayon_core/tools/publisher/widgets/folders_dialog.py b/client/ayon_core/tools/publisher/widgets/folders_dialog.py index e0d9c098d8..824ed728c9 100644 --- a/client/ayon_core/tools/publisher/widgets/folders_dialog.py +++ b/client/ayon_core/tools/publisher/widgets/folders_dialog.py @@ -113,6 +113,7 @@ class FoldersDialog(QtWidgets.QDialog): self._soft_reset_enabled = False self._folders_widget.set_project_name(self._project_name) + self._on_my_tasks_change(self._filters_widget.is_my_tasks_checked()) def get_selected_folder_path(self): """Get selected folder path.""" diff --git a/client/ayon_core/tools/utils/folders_widget.py b/client/ayon_core/tools/utils/folders_widget.py index f506af5352..ea278da6cb 100644 --- a/client/ayon_core/tools/utils/folders_widget.py +++ b/client/ayon_core/tools/utils/folders_widget.py @@ -834,6 +834,12 @@ class FoldersFiltersWidget(QtWidgets.QWidget): self._folders_filter_input = folders_filter_input self._my_tasks_checkbox = my_tasks_checkbox + def is_my_tasks_checked(self) -> bool: + return self._my_tasks_checkbox.isChecked() + + def text(self) -> str: + return self._folders_filter_input.text() + def set_text(self, text: str) -> None: self._folders_filter_input.setText(text) diff --git a/client/ayon_core/tools/workfiles/widgets/window.py b/client/ayon_core/tools/workfiles/widgets/window.py index 811fe602d1..bb3fd19ae1 100644 --- a/client/ayon_core/tools/workfiles/widgets/window.py +++ b/client/ayon_core/tools/workfiles/widgets/window.py @@ -205,6 +205,8 @@ class WorkfilesToolWindow(QtWidgets.QWidget): self._folders_widget = folder_widget + self._filters_widget = filters_widget + return col_widget def _create_col_3_widget(self, controller, parent): @@ -343,6 +345,10 @@ class WorkfilesToolWindow(QtWidgets.QWidget): self._project_name = self._controller.get_current_project_name() self._folders_widget.set_project_name(self._project_name) + # Update my tasks + self._on_my_tasks_checkbox_state_changed( + self._filters_widget.is_my_tasks_checked() + ) def _on_save_as_finished(self, event): if event["failed"]: From 8076615a5f87b131bbcac282b8148a8acc569e3b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:01:41 +0100 Subject: [PATCH 17/19] use same approach in launcher as in other tools --- client/ayon_core/tools/launcher/ui/hierarchy_page.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py index 575666b64d..9d5cb8e8d0 100644 --- a/client/ayon_core/tools/launcher/ui/hierarchy_page.py +++ b/client/ayon_core/tools/launcher/ui/hierarchy_page.py @@ -112,6 +112,7 @@ class HierarchyPage(QtWidgets.QWidget): self._is_visible = False self._controller = controller + self._filters_widget = filters_widget self._btn_back = btn_back self._projects_combobox = projects_combobox self._folders_widget = folders_widget @@ -120,8 +121,6 @@ class HierarchyPage(QtWidgets.QWidget): self._project_name = None - self._my_tasks_filter_enabled = False - # Post init projects_combobox.set_listen_to_selection_change(self._is_visible) @@ -140,7 +139,7 @@ class HierarchyPage(QtWidgets.QWidget): self._workfiles_page.refresh() # Update my tasks self._on_my_tasks_checkbox_state_changed( - self._my_tasks_filter_enabled + self._filters_widget.is_my_tasks_checked() ) def _on_back_clicked(self): @@ -161,7 +160,7 @@ class HierarchyPage(QtWidgets.QWidget): ) folder_ids = entity_ids["folder_ids"] task_ids = entity_ids["task_ids"] - self._my_tasks_filter_enabled = enabled + self._folders_widget.set_folder_ids_filter(folder_ids) self._tasks_widget.set_task_ids_filter(task_ids) From f0e603fe7c73d18910e3a0fb2e708a60e9b284ac Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 10 Dec 2025 00:30:16 +0100 Subject: [PATCH 18/19] Do not invert source display/view if it already matches target display/view --- client/ayon_core/lib/transcoding.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index b4a3e77f5a..1f9005d92b 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1234,17 +1234,21 @@ def oiio_color_convert( if source_view and source_display: color_convert_args = None ocio_display_args = None - oiio_cmd.extend([ - "--ociodisplay:inverse=1:subimages=0", - source_display, - source_view, - ]) + + if source_display != target_display or source_view != target_view: + # Undo source display/view if we have a source display/view + # that does not match the target display/view + oiio_cmd.extend([ + "--ociodisplay:inverse=1:subimages=0", + source_display, + source_view, + ]) if target_colorspace: # This is a two-step conversion process since there's no direct # display/view to colorspace command # This could be a config parameter or determined from OCIO config - # Use temporarty role space 'scene_linear' + # Use temporary role space 'scene_linear' color_convert_args = ("scene_linear", target_colorspace) elif source_display != target_display or source_view != target_view: # Complete display/view pair conversion From b1be956994acd10e1c0c52103e79a33187ac06e7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 10 Dec 2025 00:38:49 +0100 Subject: [PATCH 19/19] Also invert if target_colorspace, which means - always invert source display/view if we have any target colorspace or a display/view that differs from the source display/view --- client/ayon_core/lib/transcoding.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 1f9005d92b..f1c1cd7aa6 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1234,16 +1234,6 @@ def oiio_color_convert( if source_view and source_display: color_convert_args = None ocio_display_args = None - - if source_display != target_display or source_view != target_view: - # Undo source display/view if we have a source display/view - # that does not match the target display/view - oiio_cmd.extend([ - "--ociodisplay:inverse=1:subimages=0", - source_display, - source_view, - ]) - if target_colorspace: # This is a two-step conversion process since there's no direct # display/view to colorspace command @@ -1260,6 +1250,15 @@ def oiio_color_convert( " No color conversion needed." ) + if color_convert_args or ocio_display_args: + # Invert source display/view so that we can go from there to the + # target colorspace or display/view + oiio_cmd.extend([ + "--ociodisplay:inverse=1:subimages=0", + source_display, + source_view, + ]) + if color_convert_args: # Use colorconvert for colorspace target oiio_cmd.extend([