From 49927bded70ddc6e8216edb87b93eb35c462d2fc Mon Sep 17 00:00:00 2001 From: jm22dogs Date: Thu, 30 Oct 2025 15:37:44 +0000 Subject: [PATCH 1/8] validate approved containers --- client/ayon_core/pipeline/load/utils.py | 64 ++++++++++++++++--- .../plugins/publish/validate_containers.py | 9 ++- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index d1731d4cf9..2572984883 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -5,7 +5,7 @@ import logging import inspect import collections import numbers -from typing import Optional, Union, Any +from typing import Optional, Union, Any, Iterable import ayon_api @@ -882,6 +882,45 @@ def get_representation_by_names( project_name, representation_name, version_id=version_entity["id"]) +def get_last_versions_with_status( + project_name: str, + product_ids: Iterable[str], + statuses: Iterable[str], + active: Union[bool, None] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=ayon_api.server_api._PLACEHOLDER, +): + # project_name, versions_by_product_id.keys(), fields={"id"} + if fields: + fields = set(fields) + fields.add("productId") + product_ids = set(product_ids) + _versions = ayon_api.get_versions( + project_name, + product_ids=product_ids, + # latest=True, + hero=False, + active=active, + fields=fields, + statuses=statuses, + own_attributes=own_attributes, + ) + # remove all not latest versions + version_by_product_ids = { + pid: [v for v in _versions if v["productId"] == pid] for pid in product_ids + } + versions = list() + for pid in product_ids: + sorted_versions = sorted( + version_by_product_ids[pid], key=lambda x: x["version"] + ) + versions.append(sorted_versions[-1]) + output = {version["productId"]: version for version in versions} + for product_id in product_ids: + output.setdefault(product_id, None) + return output + + def is_compatible_loader(Loader, context): """Return whether a loader is compatible with a context. @@ -934,10 +973,10 @@ def loaders_from_representation(loaders, representation): return loaders_from_repre_context(loaders, context) -def any_outdated_containers(host=None, project_name=None): +def any_outdated_containers(host=None, project_name=None, statuses=None): """Check if there are any outdated containers in scene.""" - if get_outdated_containers(host, project_name): + if get_outdated_containers(host, project_name, statuses): return True return False @@ -946,6 +985,7 @@ def get_outdated_containers( host: Optional[AbstractHost] = None, project_name: Optional[str] = None, ignore_locked_versions: bool = False, + statuses: Optional[Iterable[str]] = None, ): """Collect outdated containers from host scene. @@ -972,7 +1012,7 @@ def get_outdated_containers( containers = host.ls() outdated_containers = [] - for container in filter_containers(containers, project_name).outdated: + for container in filter_containers(containers, project_name, statuses).outdated: if ( not ignore_locked_versions and container.get("version_locked") is True @@ -992,7 +1032,7 @@ def _is_valid_representation_id(repre_id: Any) -> bool: return True -def filter_containers(containers, project_name): +def filter_containers(containers, project_name, statuses: Optional[Iterable[str]]=None): """Filter containers and split them into 4 categories. Categories are 'latest', 'outdated', 'invalid' and 'not_found'. @@ -1007,6 +1047,7 @@ def filter_containers(containers, project_name): containers (Iterable[dict]): List of containers referenced into scene. project_name (str): Name of project in which context shoud look for versions. + statuses (Optional[Iterable[str]]): statuses to use as a filter. Returns: ContainersFilterResult: Named tuple with 'latest', 'outdated', @@ -1075,11 +1116,14 @@ def filter_containers(containers, project_name): product_id = version_entity["productId"] versions_by_product_id[product_id].append(version_entity) - last_versions = ayon_api.get_last_versions( - project_name, - versions_by_product_id.keys(), - fields={"id"} - ) + if statuses is None: + last_versions = ayon_api.get_last_versions( + project_name, versions_by_product_id.keys(), fields={"id"} + ) + else: + last_versions = get_last_versions_with_status( + project_name, versions_by_product_id.keys(), statuses, fields={"id"} + ) # Figure out which versions are outdated outdated_version_ids = set() for product_id, last_version_entity in last_versions.items(): diff --git a/client/ayon_core/plugins/publish/validate_containers.py b/client/ayon_core/plugins/publish/validate_containers.py index fda3d93627..dc7281b583 100644 --- a/client/ayon_core/plugins/publish/validate_containers.py +++ b/client/ayon_core/plugins/publish/validate_containers.py @@ -35,6 +35,7 @@ class ValidateOutdatedContainers( optional = True actions = [ShowInventory] + validate_if_approved: bool = False @classmethod def apply_settings(cls, settings): @@ -69,9 +70,15 @@ class ValidateOutdatedContainers( setattr(cls, attr_name, profile[attr_name]) def process(self, context): + if self.validate_if_approved: + statuses_data = context.data["projectEntity"]["statuses"] + approved_statuses = [s["name"] for s in statuses_data if s["state"] == "done"] + else: + approved_statuses = None + if not self.is_active(context.data): return - if any_outdated_containers(): + if any_outdated_containers(statuses=approved_statuses): msg = "There are outdated containers in the scene." raise PublishXmlValidationError(self, msg) From d4fd71b7091dae6253ab9a86cccef030ace96035 Mon Sep 17 00:00:00 2001 From: Juan M <166030421+jm22dogs@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:55:33 +0000 Subject: [PATCH 2/8] add docstring to `get_last_versions_with_status` Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client/ayon_core/pipeline/load/utils.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 2572984883..1436d2bc1f 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -890,6 +890,28 @@ def get_last_versions_with_status( fields: Optional[Iterable[str]] = None, own_attributes=ayon_api.server_api._PLACEHOLDER, ): + """ + Retrieve the latest version for each product ID that matches the given status filter. + + Args: + project_name (str): Name of the project. + product_ids (Iterable[str]): Iterable of product IDs to query. + statuses (Iterable[str]): Iterable of status names to filter versions. + active (bool or None, optional): If True, only active versions are returned. + If False, only inactive versions are returned. If None, both are returned. + fields (Optional[Iterable[str]], optional): Additional fields to include in the result. + own_attributes: Custom attributes to include in the query (default is ayon_api.server_api._PLACEHOLDER). + + Returns: + Dict[str, Optional[dict]]: A dictionary mapping each product ID to its latest version + entity (as a dict) that matches the status filter. If no version matches for a + product ID, its value will be None. + + Behavior: + - Only the latest version (by version number) for each product ID is returned. + - If no versions match the status filter for a product ID, the value for that product ID + in the output dictionary will be None. + """ # project_name, versions_by_product_id.keys(), fields={"id"} if fields: fields = set(fields) From a44374a502edbfb38b37e1f0fba6f3360a8b9624 Mon Sep 17 00:00:00 2001 From: Juan M <166030421+jm22dogs@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:56:38 +0000 Subject: [PATCH 3/8] remove commented out line in `get_last_versions_with_status` Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client/ayon_core/pipeline/load/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 1436d2bc1f..4e40246c4d 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -920,7 +920,6 @@ def get_last_versions_with_status( _versions = ayon_api.get_versions( project_name, product_ids=product_ids, - # latest=True, hero=False, active=active, fields=fields, From 2b757849faebfb563e9fa081b667880f2000222d Mon Sep 17 00:00:00 2001 From: Juan M <166030421+jm22dogs@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:57:14 +0000 Subject: [PATCH 4/8] update docstring in `any_outdated_containers` Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client/ayon_core/pipeline/load/utils.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 4e40246c4d..e96cf31835 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -995,8 +995,17 @@ def loaders_from_representation(loaders, representation): def any_outdated_containers(host=None, project_name=None, statuses=None): - """Check if there are any outdated containers in scene.""" + """Check if there are any outdated containers in the scene. + Args: + host (Optional[AbstractHost]): Host implementation. + project_name (Optional[str]): Name of project in which context we are. + statuses (Optional[Iterable[str]]): Iterable of status strings to filter containers. + If None, all statuses are included. + + Returns: + bool: True if there are any outdated containers matching the criteria, False otherwise. + """ if get_outdated_containers(host, project_name, statuses): return True return False From e8df477e1031d445da34a8045cf7207ae1e53f50 Mon Sep 17 00:00:00 2001 From: Juan M <166030421+jm22dogs@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:58:34 +0000 Subject: [PATCH 5/8] Remove commented out line in `get_last_versions_with_status` Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client/ayon_core/pipeline/load/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index e96cf31835..e2fa1379e2 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -912,7 +912,6 @@ def get_last_versions_with_status( - If no versions match the status filter for a product ID, the value for that product ID in the output dictionary will be None. """ - # project_name, versions_by_product_id.keys(), fields={"id"} if fields: fields = set(fields) fields.add("productId") From cc0c08710274d6eac76ae38168641e4fc778b709 Mon Sep 17 00:00:00 2001 From: Juan M <166030421+jm22dogs@users.noreply.github.com> Date: Fri, 31 Oct 2025 09:00:06 +0000 Subject: [PATCH 6/8] Prevent `IndexError` in `get_last_versions_with_status` Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client/ayon_core/pipeline/load/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index e2fa1379e2..cea897a954 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -934,7 +934,8 @@ def get_last_versions_with_status( sorted_versions = sorted( version_by_product_ids[pid], key=lambda x: x["version"] ) - versions.append(sorted_versions[-1]) + if sorted_versions: + versions.append(sorted_versions[-1]) output = {version["productId"]: version for version in versions} for product_id in product_ids: output.setdefault(product_id, None) From f789ec0843f19bd43a39aa49c88d789e9b8d489d Mon Sep 17 00:00:00 2001 From: Juan M <166030421+jm22dogs@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:34:37 +0000 Subject: [PATCH 7/8] Improve readability in the `version_by_product_id` dictionary creation Accepted suggested change by @iLLiCiTiT Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/load/utils.py | 32 ++++++++++++++----------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index cea897a954..4d610091c9 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -916,7 +916,7 @@ def get_last_versions_with_status( fields = set(fields) fields.add("productId") product_ids = set(product_ids) - _versions = ayon_api.get_versions( + versions = ayon_api.get_versions( project_name, product_ids=product_ids, hero=False, @@ -925,20 +925,24 @@ def get_last_versions_with_status( statuses=statuses, own_attributes=own_attributes, ) - # remove all not latest versions - version_by_product_ids = { - pid: [v for v in _versions if v["productId"] == pid] for pid in product_ids + versions_by_product_id = { + product_id: [] + for product_id in product_ids } - versions = list() - for pid in product_ids: - sorted_versions = sorted( - version_by_product_ids[pid], key=lambda x: x["version"] - ) - if sorted_versions: - versions.append(sorted_versions[-1]) - output = {version["productId"]: version for version in versions} - for product_id in product_ids: - output.setdefault(product_id, None) + for version in versions: + product_id = version["productId"] + versions_by_product_id[product_id].append(version) + + output = { + product_id: None + for product_id in product_ids + } + for product_id, product_versions in versions_by_product_id.items(): + if not product_versions: + continue + product_versions.sort(key=lambda v: v["version"]) + output[product_id] = product_versions[-1] + return output From be0f9d0a80680ffab7ed12e8fa7d56d3437c570a Mon Sep 17 00:00:00 2001 From: jm22dogs Date: Fri, 14 Nov 2025 11:25:23 +0000 Subject: [PATCH 8/8] fallback to latest version if missing approved version with status --- client/ayon_core/pipeline/load/utils.py | 73 +++++++++++-------------- 1 file changed, 32 insertions(+), 41 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 1393cd14da..f932196ba5 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -9,7 +9,7 @@ import collections import numbers import copy from functools import wraps -from typing import Optional, Union, Any, overload +from typing import Optional, Union, Any, overload, Iterable import ayon_api @@ -28,6 +28,7 @@ ContainersFilterResult = collections.namedtuple( ["latest", "outdated", "not_found", "invalid"] ) +_PLACEHOLDER = object() class HeroVersionType(object): def __init__(self, version): @@ -999,35 +1000,13 @@ def get_last_versions_with_status( statuses: Iterable[str], active: Union[bool, None] = True, fields: Optional[Iterable[str]] = None, - own_attributes=ayon_api.server_api._PLACEHOLDER, + own_attributes=_PLACEHOLDER, ): - """ - Retrieve the latest version for each product ID that matches the given status filter. - - Args: - project_name (str): Name of the project. - product_ids (Iterable[str]): Iterable of product IDs to query. - statuses (Iterable[str]): Iterable of status names to filter versions. - active (bool or None, optional): If True, only active versions are returned. - If False, only inactive versions are returned. If None, both are returned. - fields (Optional[Iterable[str]], optional): Additional fields to include in the result. - own_attributes: Custom attributes to include in the query (default is ayon_api.server_api._PLACEHOLDER). - - Returns: - Dict[str, Optional[dict]]: A dictionary mapping each product ID to its latest version - entity (as a dict) that matches the status filter. If no version matches for a - product ID, its value will be None. - - Behavior: - - Only the latest version (by version number) for each product ID is returned. - - If no versions match the status filter for a product ID, the value for that product ID - in the output dictionary will be None. - """ if fields: fields = set(fields) fields.add("productId") product_ids = set(product_ids) - versions = ayon_api.get_versions( + versions_with_status = ayon_api.get_versions( project_name, product_ids=product_ids, hero=False, @@ -1036,27 +1015,39 @@ def get_last_versions_with_status( statuses=statuses, own_attributes=own_attributes, ) - versions_by_product_id = { - product_id: [] - for product_id in product_ids + latest_versions = ayon_api.get_versions( + project_name, + product_ids=product_ids, + latest=True, + hero=False, + active=active, + fields=fields, + own_attributes=own_attributes, + ) + latest_versions_by_product_ids = { + pid: [v for v in latest_versions if v["productId"] == pid] + for pid in product_ids } - for version in versions: - product_id = version["productId"] - versions_by_product_id[product_id].append(version) - - output = { - product_id: None - for product_id in product_ids + # remove all not latest versions + version_by_product_ids = { + pid: [v for v in versions_with_status if v["productId"] == pid] + for pid in product_ids } - for product_id, product_versions in versions_by_product_id.items(): - if not product_versions: - continue - product_versions.sort(key=lambda v: v["version"]) - output[product_id] = product_versions[-1] - + versions = list() + for pid in product_ids: + if not version_by_product_ids[pid]: + version_by_product_ids[pid] = latest_versions_by_product_ids[pid] + sorted_versions = sorted( + version_by_product_ids[pid], key=lambda x: x["version"] + ) + versions.append(sorted_versions[-1]) + output = {version["productId"]: version for version in versions} + for product_id in product_ids: + output.setdefault(product_id, None) return output + def is_compatible_loader(Loader, context): """Return whether a loader is compatible with a context.