diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 27ed2217dd..646a2dd1ee 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to AYON Tray options: + - 1.6.5 - 1.6.4 - 1.6.3 - 1.6.2 diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 1d1562f543..9207bb74c0 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -2,7 +2,6 @@ """Base class for AYON addons.""" from __future__ import annotations -import copy import os import sys import time @@ -13,6 +12,7 @@ import collections import warnings from uuid import uuid4 from abc import ABC, abstractmethod +from urllib.parse import urlencode from types import ModuleType import typing from typing import Optional, Any, Union @@ -136,39 +136,47 @@ def load_addons(force: bool = False) -> None: time.sleep(0.1) -def _get_ayon_bundle_data() -> Optional[dict[str, Any]]: +def _get_ayon_bundle_data() -> tuple[ + dict[str, Any], Optional[dict[str, Any]] +]: studio_bundle_name = os.environ.get("AYON_STUDIO_BUNDLE_NAME") project_bundle_name = os.getenv("AYON_BUNDLE_NAME") bundles = ayon_api.get_bundles()["bundles"] - project_bundle = next( + studio_bundle = next( ( bundle for bundle in bundles - if bundle["name"] == project_bundle_name + if bundle["name"] == studio_bundle_name ), None ) - studio_bundle = None - if studio_bundle_name and project_bundle_name != studio_bundle_name: - studio_bundle = next( + + if studio_bundle is None: + raise RuntimeError(f"Failed to find bundle '{studio_bundle_name}'.") + + project_bundle = None + if project_bundle_name and project_bundle_name != studio_bundle_name: + project_bundle = next( ( bundle for bundle in bundles - if bundle["name"] == studio_bundle_name + if bundle["name"] == project_bundle_name ), None ) - if project_bundle and studio_bundle: - addons = copy.deepcopy(studio_bundle["addons"]) - addons.update(project_bundle["addons"]) - project_bundle["addons"] = addons - return project_bundle + if project_bundle is None: + raise RuntimeError( + f"Failed to find project bundle '{project_bundle_name}'." + ) + + return studio_bundle, project_bundle def _get_ayon_addons_information( - bundle_info: dict[str, Any] -) -> list[dict[str, Any]]: + studio_bundle: dict[str, Any], + project_bundle: Optional[dict[str, Any]], +) -> dict[str, str]: """Receive information about addons to use from server. Todos: @@ -181,22 +189,20 @@ def _get_ayon_addons_information( list[dict[str, Any]]: List of addon information to use. """ - output = [] - bundle_addons = bundle_info["addons"] - addons = ayon_api.get_addons_info()["addons"] - for addon in addons: - name = addon["name"] - versions = addon.get("versions") - addon_version = bundle_addons.get(name) - if addon_version is None or not versions: - continue - version = versions.get(addon_version) - if version: - version = copy.deepcopy(version) - version["name"] = name - version["version"] = addon_version - output.append(version) - return output + key_values = { + "summary": "true", + "bundle_name": studio_bundle["name"], + } + if project_bundle: + key_values["project_bundle_name"] = project_bundle["name"] + + query = urlencode(key_values) + + response = ayon_api.get(f"settings?{query}") + return { + addon["name"]: addon["version"] + for addon in response.data["addons"] + } def _load_ayon_addons(log: logging.Logger) -> list[ModuleType]: @@ -214,8 +220,8 @@ def _load_ayon_addons(log: logging.Logger) -> list[ModuleType]: """ all_addon_modules = [] - bundle_info = _get_ayon_bundle_data() - addons_info = _get_ayon_addons_information(bundle_info) + studio_bundle, project_bundle = _get_ayon_bundle_data() + addons_info = _get_ayon_addons_information(studio_bundle, project_bundle) if not addons_info: return all_addon_modules @@ -227,17 +233,16 @@ def _load_ayon_addons(log: logging.Logger) -> list[ModuleType]: dev_addons_info = {} if dev_mode_enabled: # Get dev addons info only when dev mode is enabled - dev_addons_info = bundle_info.get("addonDevelopment", dev_addons_info) + dev_addons_info = studio_bundle.get( + "addonDevelopment", dev_addons_info + ) addons_dir_exists = os.path.exists(addons_dir) if not addons_dir_exists: log.warning( f"Addons directory does not exists. Path \"{addons_dir}\"") - for addon_info in addons_info: - addon_name = addon_info["name"] - addon_version = addon_info["version"] - + for addon_name, addon_version in addons_info.items(): # core addon does not have any addon object if addon_name == "core": continue diff --git a/client/ayon_core/lib/plugin_tools.py b/client/ayon_core/lib/plugin_tools.py index 654bc7ac4a..b19fe1e200 100644 --- a/client/ayon_core/lib/plugin_tools.py +++ b/client/ayon_core/lib/plugin_tools.py @@ -1,11 +1,9 @@ # -*- coding: utf-8 -*- """AYON plugin tools.""" import os -import logging import re import collections -log = logging.getLogger(__name__) CAPITALIZE_REGEX = re.compile(r"[a-zA-Z0-9]") diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 0d8e70f9d2..a5053844b9 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -249,7 +249,8 @@ def create_skeleton_instance( # map inputVersions `ObjectId` -> `str` so json supports it "inputVersions": list(map(str, data.get("inputVersions", []))), "colorspace": data.get("colorspace"), - "hasExplicitFrames": data.get("hasExplicitFrames") + "hasExplicitFrames": data.get("hasExplicitFrames", False), + "reuseLastVersion": data.get("reuseLastVersion", False), } if data.get("renderlayer"): diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 3b82d961f8..7152ec78fa 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -7,13 +7,20 @@ import copy import warnings import hashlib import xml.etree.ElementTree -from typing import TYPE_CHECKING, Optional, Union, List +from typing import TYPE_CHECKING, Optional, Union, List, Any +import clique +import speedcopy +import logging -import ayon_api import pyblish.util import pyblish.plugin import pyblish.api +from ayon_api import ( + get_server_api_connection, + get_representations, + get_last_version_by_product_name +) from ayon_core.lib import ( import_filepath, Logger, @@ -34,6 +41,8 @@ if TYPE_CHECKING: TRAIT_INSTANCE_KEY: str = "representations_with_traits" +log = logging.getLogger(__name__) + def get_template_name_profiles( project_name, project_settings=None, logger=None @@ -1030,7 +1039,7 @@ def main_cli_publish( # NOTE: ayon-python-api does not have public api function to find # out if is used service user. So we need to have try > except # block. - con = ayon_api.get_server_api_connection() + con = get_server_api_connection() try: con.set_default_service_username(username) except ValueError: @@ -1143,3 +1152,90 @@ def get_trait_representations( """ return instance.data.get(TRAIT_INSTANCE_KEY, []) + + +def fill_sequence_gaps_with_previous_version( + collection: str, + staging_dir: str, + instance: pyblish.plugin.Instance, + current_repre_name: str, + start_frame: int, + end_frame: int +) -> tuple[Optional[dict[str, Any]], Optional[dict[int, str]]]: + """Tries to replace missing frames from ones from last version""" + used_version_entity, repre_file_paths = _get_last_version_files( + instance, current_repre_name + ) + if repre_file_paths is None: + # issues in getting last version files + return (None, None) + + prev_collection = clique.assemble( + repre_file_paths, + patterns=[clique.PATTERNS["frames"]], + minimum_items=1 + )[0][0] + prev_col_format = prev_collection.format("{head}{padding}{tail}") + + added_files = {} + anatomy = instance.context.data["anatomy"] + col_format = collection.format("{head}{padding}{tail}") + for frame in range(start_frame, end_frame + 1): + if frame in collection.indexes: + continue + hole_fpath = os.path.join(staging_dir, col_format % frame) + + previous_version_path = prev_col_format % frame + previous_version_path = anatomy.fill_root(previous_version_path) + if not os.path.exists(previous_version_path): + log.warning( + "Missing frame should be replaced from " + f"'{previous_version_path}' but that doesn't exist. " + ) + return (None, None) + + log.warning( + f"Replacing missing '{hole_fpath}' with " + f"'{previous_version_path}'" + ) + speedcopy.copyfile(previous_version_path, hole_fpath) + added_files[frame] = hole_fpath + + return (used_version_entity, added_files) + + +def _get_last_version_files( + instance: pyblish.plugin.Instance, + current_repre_name: str, +) -> tuple[Optional[dict[str, Any]], Optional[list[str]]]: + product_name = instance.data["productName"] + project_name = instance.data["projectEntity"]["name"] + folder_entity = instance.data["folderEntity"] + + version_entity = get_last_version_by_product_name( + project_name, + product_name, + folder_entity["id"], + fields={"id", "attrib"} + ) + + if not version_entity: + return None, None + + matching_repres = get_representations( + project_name, + version_ids=[version_entity["id"]], + representation_names=[current_repre_name], + fields={"files"} + ) + + matching_repre = next(matching_repres, None) + if not matching_repre: + return None, None + + repre_file_paths = [ + file_info["path"] + for file_info in matching_repre["files"] + ] + + return (version_entity, repre_file_paths) diff --git a/client/ayon_core/plugins/publish/collect_resources_path.py b/client/ayon_core/plugins/publish/collect_resources_path.py index 2e5b296228..704c69a6ab 100644 --- a/client/ayon_core/plugins/publish/collect_resources_path.py +++ b/client/ayon_core/plugins/publish/collect_resources_path.py @@ -13,6 +13,8 @@ import copy import pyblish.api +from ayon_core.pipeline.publish import get_publish_template_name + class CollectResourcesPath(pyblish.api.InstancePlugin): """Generate directory path where the files and resources will be stored. @@ -77,16 +79,29 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): # This is for cases of Deprecated anatomy without `folder` # TODO remove when all clients have solved this issue - template_data.update({ - "frame": "FRAME_TEMP", - "representation": "TEMP" - }) + template_data.update({"frame": "FRAME_TEMP", "representation": "TEMP"}) - publish_templates = anatomy.get_template_item( - "publish", "default", "directory" + task_name = task_type = None + task_entity = instance.data.get("taskEntity") + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] + + template_name = get_publish_template_name( + project_name=instance.context.data["projectName"], + host_name=instance.context.data["hostName"], + product_type=instance.data["productType"], + task_name=task_name, + task_type=task_type, + project_settings=instance.context.data["project_settings"], + logger=self.log, ) + + publish_template = anatomy.get_template_item( + "publish", template_name, "directory") + publish_folder = os.path.normpath( - publish_templates.format_strict(template_data) + publish_template.format_strict(template_data) ) resources_folder = os.path.join(publish_folder, "resources") diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 04e534054e..580aa27eef 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -13,14 +13,15 @@ import clique import speedcopy import pyblish.api -from ayon_api import get_last_version_by_product_name, get_representations - from ayon_core.lib import ( get_ffmpeg_tool_args, filter_profiles, path_to_subprocess_arg, run_subprocess, ) +from ayon_core.pipeline.publish.lib import ( + fill_sequence_gaps_with_previous_version +) from ayon_core.lib.transcoding import ( IMAGE_EXTENSIONS, get_ffprobe_streams, @@ -130,7 +131,7 @@ def frame_to_timecode(frame: int, fps: float) -> str: class ExtractReview(pyblish.api.InstancePlugin): - """Extracting Review mov file for Ftrack + """Extracting Reviewable medias Compulsory attribute of representation is tags list with "review", otherwise the representation is ignored. @@ -508,10 +509,10 @@ class ExtractReview(pyblish.api.InstancePlugin): resolution_width=temp_data.resolution_width, resolution_height=temp_data.resolution_height, extension=temp_data.input_ext, - temp_data=temp_data + temp_data=temp_data, ) elif fill_missing_frames == "previous_version": - new_frame_files = self.fill_sequence_gaps_with_previous( + fill_output = fill_sequence_gaps_with_previous_version( collection=collection, staging_dir=new_repre["stagingDir"], instance=instance, @@ -519,8 +520,13 @@ class ExtractReview(pyblish.api.InstancePlugin): start_frame=temp_data.frame_start, end_frame=temp_data.frame_end, ) + _, new_frame_files = fill_output # fallback to original workflow if new_frame_files is None: + self.log.warning( + "Falling back to filling from currently " + "last rendered." + ) new_frame_files = ( self.fill_sequence_gaps_from_existing( collection=collection, @@ -612,8 +618,6 @@ class ExtractReview(pyblish.api.InstancePlugin): "name": "{}_{}".format(output_name, output_ext), "outputName": output_name, "outputDef": output_def, - "frameStartFtrack": temp_data.output_frame_start, - "frameEndFtrack": temp_data.output_frame_end, "ffmpeg_cmd": subprcs_cmd }) @@ -1050,92 +1054,6 @@ class ExtractReview(pyblish.api.InstancePlugin): return all_args - def fill_sequence_gaps_with_previous( - self, - collection: str, - staging_dir: str, - instance: pyblish.plugin.Instance, - current_repre_name: str, - start_frame: int, - end_frame: int - ) -> Optional[dict[int, str]]: - """Tries to replace missing frames from ones from last version""" - repre_file_paths = self._get_last_version_files( - instance, current_repre_name) - if repre_file_paths is None: - # issues in getting last version files, falling back - return None - - prev_collection = clique.assemble( - repre_file_paths, - patterns=[clique.PATTERNS["frames"]], - minimum_items=1 - )[0][0] - prev_col_format = prev_collection.format("{head}{padding}{tail}") - - added_files = {} - anatomy = instance.context.data["anatomy"] - col_format = collection.format("{head}{padding}{tail}") - for frame in range(start_frame, end_frame + 1): - if frame in collection.indexes: - continue - hole_fpath = os.path.join(staging_dir, col_format % frame) - - previous_version_path = prev_col_format % frame - previous_version_path = anatomy.fill_root(previous_version_path) - if not os.path.exists(previous_version_path): - self.log.warning( - "Missing frame should be replaced from " - f"'{previous_version_path}' but that doesn't exist. " - "Falling back to filling from currently last rendered." - ) - return None - - self.log.warning( - f"Replacing missing '{hole_fpath}' with " - f"'{previous_version_path}'" - ) - speedcopy.copyfile(previous_version_path, hole_fpath) - added_files[frame] = hole_fpath - - return added_files - - def _get_last_version_files( - self, - instance: pyblish.plugin.Instance, - current_repre_name: str, - ): - product_name = instance.data["productName"] - project_name = instance.data["projectEntity"]["name"] - folder_entity = instance.data["folderEntity"] - - version_entity = get_last_version_by_product_name( - project_name, - product_name, - folder_entity["id"], - fields={"id"} - ) - if not version_entity: - return None - - matching_repres = get_representations( - project_name, - version_ids=[version_entity["id"]], - representation_names=[current_repre_name], - fields={"files"} - ) - - if not matching_repres: - return None - matching_repre = list(matching_repres)[0] - - repre_file_paths = [ - file_info["path"] - for file_info in matching_repre["files"] - ] - - return repre_file_paths - def fill_sequence_gaps_with_blanks( self, collection: str, @@ -1384,15 +1302,7 @@ class ExtractReview(pyblish.api.InstancePlugin): return audio_in_args, audio_filters, audio_out_args for audio in audio_inputs: - # NOTE modified, always was expected "frameStartFtrack" which is - # STRANGE?!!! There should be different key, right? - # TODO use different frame start! offset_seconds = 0 - frame_start_ftrack = instance.data.get("frameStartFtrack") - if frame_start_ftrack is not None: - offset_frames = frame_start_ftrack - audio["offset"] - offset_seconds = offset_frames / temp_data.fps - if offset_seconds > 0: audio_in_args.append( "-ss {}".format(offset_seconds) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index bbced6b641..d3b3454fd1 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.6.4+dev" +__version__ = "1.6.5+dev" diff --git a/package.py b/package.py index 114f7d12ef..2889039502 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.4+dev" +version = "1.6.5+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 44c6a9d73c..f43846ec2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.4+dev" +version = "1.6.5+dev" description = "" authors = ["Ynput Team "] readme = "README.md" @@ -27,17 +27,6 @@ codespell = "^2.2.6" semver = "^3.0.2" mypy = "^1.14.0" mock = "^5.0.0" -tomlkit = "^0.13.2" -requests = "^2.32.3" -mkdocs-material = "^9.6.7" -mkdocs-autoapi = "^0.4.0" -mkdocstrings-python = "^1.16.2" -mkdocs-minify-plugin = "^0.8.0" -markdown-checklist = "^0.4.4" -mdx-gh-links = "^0.4" -pymdown-extensions = "^10.14.3" -mike = "^2.1.3" -mkdocstrings-shell = "^1.0.2" nxtools = "^1.6" [tool.poetry.group.test.dependencies] diff --git a/server/settings/tools.py b/server/settings/tools.py index 815ef40f8e..f40c7c3627 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -454,7 +454,7 @@ DEFAULT_TOOLS_VALUES = { "hosts": [], "task_types": [], "tasks": [], - "template": "{product[type]}{Task[name]}{Variant}" + "template": "{product[type]}{Task[name]}{Variant}<_{Aov}>" }, { "product_types": [