From 7accb372cd65df831233d379b861a3da84e7f0d9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 14:00:11 +0200 Subject: [PATCH 01/75] remove tray publisher from experimental tools --- openpype/modules/traypublish_action.py | 15 ++------------- openpype/tools/experimental_tools/tools_def.py | 5 ----- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/openpype/modules/traypublish_action.py b/openpype/modules/traypublish_action.py index 39163b8eb8..1038bac47b 100644 --- a/openpype/modules/traypublish_action.py +++ b/openpype/modules/traypublish_action.py @@ -2,7 +2,7 @@ import os from openpype.lib import get_openpype_execute_args from openpype.lib.execute import run_detached_process from openpype.modules import OpenPypeModule -from openpype_interfaces import ITrayAction +from openpype.modules.interfaces import ITrayAction class TrayPublishAction(OpenPypeModule, ITrayAction): @@ -21,20 +21,9 @@ class TrayPublishAction(OpenPypeModule, ITrayAction): "publish" ) ] - self._experimental_tools = None def tray_init(self): - from openpype.tools.experimental_tools import ExperimentalTools - - self._experimental_tools = ExperimentalTools() - - def tray_menu(self, *args, **kwargs): - super(TrayPublishAction, self).tray_menu(*args, **kwargs) - traypublisher = self._experimental_tools.get("traypublisher") - visible = False - if traypublisher and traypublisher.enabled: - visible = True - self._action_item.setVisible(visible) + return def on_action_trigger(self): self.run_traypublisher() diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index fa2971dc1d..38a49e8ab5 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -89,11 +89,6 @@ class ExperimentalTools: "New publisher", "Combined creation and publishing into one tool.", self._show_publisher - ), - ExperimentalTool( - "traypublisher", - "New Standalone Publisher", - "Standalone publisher using new publisher. Requires restart" ) ] From 8e6dcbc048be40e038e3406fd3b3ae22e1631c9a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 14:00:20 +0200 Subject: [PATCH 02/75] change label of tray publisher action --- openpype/modules/traypublish_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/traypublish_action.py b/openpype/modules/traypublish_action.py index 1038bac47b..29aea07210 100644 --- a/openpype/modules/traypublish_action.py +++ b/openpype/modules/traypublish_action.py @@ -6,7 +6,7 @@ from openpype.modules.interfaces import ITrayAction class TrayPublishAction(OpenPypeModule, ITrayAction): - label = "New Publish (beta)" + label = "Tray Publish" name = "traypublish_tool" def initialize(self, modules_settings): From ed6cadb22b331f985348e5ed22168cea5ebb24d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 12 Oct 2022 18:25:14 +0200 Subject: [PATCH 03/75] :construction: remove staging from version logic --- igniter/bootstrap_repos.py | 219 +++++-------------------------------- igniter/tools.py | 11 +- 2 files changed, 30 insertions(+), 200 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index ccc9d4ac52..bbb3dd506c 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -61,7 +61,6 @@ class OpenPypeVersion(semver.VersionInfo): path (str): path to OpenPype """ - staging = False path = None _VERSION_REGEX = re.compile(r"(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?") # noqa: E501 _installed_version = None @@ -82,12 +81,10 @@ class OpenPypeVersion(semver.VersionInfo): build (str): an optional build string version (str): if set, it will be parsed and will override parameters like `major`, `minor` and so on. - staging (bool): set to True if version is staging. path (Path): path to version location. """ self.path = None - self.staging = False if "version" in kwargs.keys(): if not kwargs.get("version"): @@ -112,29 +109,8 @@ class OpenPypeVersion(semver.VersionInfo): if "path" in kwargs.keys(): kwargs.pop("path") - if kwargs.get("staging"): - self.staging = kwargs.get("staging", False) - kwargs.pop("staging") - - if "staging" in kwargs.keys(): - kwargs.pop("staging") - - if self.staging: - if kwargs.get("build"): - if "staging" not in kwargs.get("build"): - kwargs["build"] = f"{kwargs.get('build')}-staging" - else: - kwargs["build"] = "staging" - - if kwargs.get("build") and "staging" in kwargs.get("build", ""): - self.staging = True - super().__init__(*args, **kwargs) - def __eq__(self, other): - result = super().__eq__(other) - return bool(result and self.staging == other.staging) - def __repr__(self): return f"<{self.__class__.__name__}: {str(self)} - path={self.path}>" @@ -149,43 +125,11 @@ class OpenPypeVersion(semver.VersionInfo): return True if self.finalize_version() == other.finalize_version() and \ - self.prerelease == other.prerelease and \ - self.is_staging() and not other.is_staging(): + self.prerelease == other.prerelease: return True return result - def set_staging(self) -> OpenPypeVersion: - """Set version as staging and return it. - - This will preserve current one. - - Returns: - OpenPypeVersion: Set as staging. - - """ - if self.staging: - return self - return self.replace(parts={"build": f"{self.build}-staging"}) - - def set_production(self) -> OpenPypeVersion: - """Set version as production and return it. - - This will preserve current one. - - Returns: - OpenPypeVersion: Set as production. - - """ - if not self.staging: - return self - return self.replace( - parts={"build": self.build.replace("-staging", "")}) - - def is_staging(self) -> bool: - """Test if current version is staging one.""" - return self.staging - def get_main_version(self) -> str: """Return main version component. @@ -215,8 +159,6 @@ class OpenPypeVersion(semver.VersionInfo): if not m: return None version = OpenPypeVersion.parse(string[m.start():m.end()]) - if "staging" in string[m.start():m.end()]: - version.staging = True return version @classmethod @@ -226,8 +168,6 @@ class OpenPypeVersion(semver.VersionInfo): openpype_version = cls(major=v.major, minor=v.minor, patch=v.patch, prerelease=v.prerelease, build=v.build) - if v.build and "staging" in v.build: - openpype_version.staging = True return openpype_version def __hash__(self): @@ -379,80 +319,28 @@ class OpenPypeVersion(semver.VersionInfo): return False @classmethod - def get_local_versions( - cls, production: bool = None, - staging: bool = None - ) -> List: + def get_local_versions(cls) -> List: """Get all versions available on this machine. - Arguments give ability to specify if filtering is needed. If both - arguments are set to None all found versions are returned. - - Args: - production (bool): Return production versions. - staging (bool): Return staging versions. - Returns: list: of compatible versions available on the machine. """ - # Return all local versions if arguments are set to None - if production is None and staging is None: - production = True - staging = True - - elif production is None and not staging: - production = True - - elif staging is None and not production: - staging = True - - # Just return empty output if both are disabled - if not production and not staging: - return [] - # DEPRECATED: backwards compatible way to look for versions in root dir_to_search = Path(user_data_dir("openpype", "pypeclub")) versions = OpenPypeVersion.get_versions_from_directory(dir_to_search) - filtered_versions = [] - for version in versions: - if version.is_staging(): - if staging: - filtered_versions.append(version) - elif production: - filtered_versions.append(version) - return list(sorted(set(filtered_versions))) + return list(sorted(set(versions))) @classmethod - def get_remote_versions( - cls, production: bool = None, - staging: bool = None - ) -> List: + def get_remote_versions(cls) -> List: """Get all versions available in OpenPype Path. - Arguments give ability to specify if filtering is needed. If both - arguments are set to None all found versions are returned. - - Args: - production (bool): Return production versions. - staging (bool): Return staging versions. + Returns: + list of OpenPypeVersions: Versions found in OpenPype path. """ # Return all local versions if arguments are set to None - if production is None and staging is None: - production = True - staging = True - - elif production is None and not staging: - production = True - - elif staging is None and not production: - staging = True - - # Just return empty output if both are disabled - if not production and not staging: - return [] dir_to_search = None if cls.openpype_path_is_accessible(): @@ -473,14 +361,7 @@ class OpenPypeVersion(semver.VersionInfo): versions = cls.get_versions_from_directory(dir_to_search) - filtered_versions = [] - for version in versions: - if version.is_staging(): - if staging: - filtered_versions.append(version) - elif production: - filtered_versions.append(version) - return list(sorted(set(filtered_versions))) + return list(sorted(set(versions))) @staticmethod def get_versions_from_directory( @@ -559,7 +440,6 @@ class OpenPypeVersion(semver.VersionInfo): @staticmethod def get_latest_version( - staging: bool = False, local: bool = None, remote: bool = None ) -> Union[OpenPypeVersion, None]: @@ -577,7 +457,6 @@ class OpenPypeVersion(semver.VersionInfo): 'False' in that case only build version can be used. Args: - staging (bool, optional): List staging versions if True. local (bool, optional): List local versions if True. remote (bool, optional): List remote versions if True. @@ -596,28 +475,15 @@ class OpenPypeVersion(semver.VersionInfo): remote = True installed_version = OpenPypeVersion.get_installed_version() - local_versions = [] - remote_versions = [] - if local: - local_versions = OpenPypeVersion.get_local_versions( - staging=staging - ) - if remote: - remote_versions = OpenPypeVersion.get_remote_versions( - staging=staging - ) - all_versions = local_versions + remote_versions - if not staging: - all_versions.append(installed_version) - - if not all_versions: - return None + local_versions = OpenPypeVersion.get_local_versions() if local else [] + remote_versions = OpenPypeVersion.get_remote_versions() if remote else [] # noqa: E501 + all_versions = local_versions + remote_versions + installed_version all_versions.sort() return all_versions[-1] @classmethod - def get_expected_studio_version(cls, staging=False, global_settings=None): + def get_expected_studio_version(cls, global_settings=None): """Expected OpenPype version that should be used at the moment. If version is not defined in settings the latest found version is @@ -626,13 +492,12 @@ class OpenPypeVersion(semver.VersionInfo): Using precached global settings is needed for usage inside OpenPype. Args: - staging (bool): Staging version or production version. global_settings (dict): Optional precached global settings. Returns: OpenPypeVersion: Version that should be used. """ - result = get_expected_studio_version_str(staging, global_settings) + result = get_expected_studio_version_str(global_settings) if not result: return None return OpenPypeVersion(version=result) @@ -1121,14 +986,12 @@ class BootstrapRepos: @staticmethod def find_openpype_version( - version: Union[str, OpenPypeVersion], - staging: bool + version: Union[str, OpenPypeVersion] ) -> Union[OpenPypeVersion, None]: """Find location of specified OpenPype version. Args: version (Union[str, OpenPypeVersion): Version to find. - staging (bool): Filter staging versions. Returns: requested OpenPypeVersion. @@ -1141,9 +1004,7 @@ class BootstrapRepos: if installed_version == version: return installed_version - local_versions = OpenPypeVersion.get_local_versions( - staging=staging, production=not staging - ) + local_versions = OpenPypeVersion.get_local_versions() zip_version = None for local_version in local_versions: if local_version == version: @@ -1155,37 +1016,25 @@ class BootstrapRepos: if zip_version is not None: return zip_version - remote_versions = OpenPypeVersion.get_remote_versions( - staging=staging, production=not staging - ) - for remote_version in remote_versions: - if remote_version == version: - return remote_version - return None + remote_versions = OpenPypeVersion.get_remote_versions() + return next( + ( + remote_version for remote_version in remote_versions + if remote_version == version + ), None) @staticmethod - def find_latest_openpype_version( - staging: bool - ) -> Union[OpenPypeVersion, None]: + def find_latest_openpype_version() -> Union[OpenPypeVersion, None]: """Find the latest available OpenPype version in all location. - Args: - staging (bool): True to look for staging versions. - Returns: Latest OpenPype version on None if nothing was found. """ installed_version = OpenPypeVersion.get_installed_version() - local_versions = OpenPypeVersion.get_local_versions( - staging=staging - ) - remote_versions = OpenPypeVersion.get_remote_versions( - staging=staging - ) - all_versions = local_versions + remote_versions - if not staging: - all_versions.append(installed_version) + local_versions = OpenPypeVersion.get_local_versions() + remote_versions = OpenPypeVersion.get_remote_versions() + all_versions = local_versions + remote_versions + installed_version if not all_versions: return None @@ -1205,7 +1054,6 @@ class BootstrapRepos: def find_openpype( self, openpype_path: Union[Path, str] = None, - staging: bool = False, include_zips: bool = False ) -> Union[List[OpenPypeVersion], None]: """Get ordered dict of detected OpenPype version. @@ -1219,8 +1067,6 @@ class BootstrapRepos: Args: openpype_path (Path or str, optional): Try to find OpenPype on the given path or url. - staging (bool, optional): Filter only staging version, skip them - otherwise. include_zips (bool, optional): If set True it will try to find OpenPype in zip files in given directory. @@ -1268,7 +1114,7 @@ class BootstrapRepos: for dir_to_search in dirs_to_search: try: openpype_versions += self.get_openpype_versions( - dir_to_search, staging) + dir_to_search) except ValueError: # location is invalid, skip it pass @@ -1633,15 +1479,11 @@ class BootstrapRepos: return False return True - def get_openpype_versions( - self, - openpype_dir: Path, - staging: bool = False) -> list: + def get_openpype_versions(self, openpype_dir: Path) -> list: """Get all detected OpenPype versions in directory. Args: openpype_dir (Path): Directory to scan. - staging (bool, optional): Find staging versions if True. Returns: list of OpenPypeVersion @@ -1659,8 +1501,7 @@ class BootstrapRepos: for item in openpype_dir.iterdir(): # if the item is directory with major.minor version, dive deeper if item.is_dir() and re.match(r"^\d+\.\d+$", item.name): - _versions = self.get_openpype_versions( - item, staging=staging) + _versions = self.get_openpype_versions(item) if _versions: openpype_versions += _versions @@ -1683,11 +1524,7 @@ class BootstrapRepos: continue detected_version.path = item - if staging and detected_version.is_staging(): - openpype_versions.append(detected_version) - - if not staging and not detected_version.is_staging(): - openpype_versions.append(detected_version) + openpype_versions.append(detected_version) return sorted(openpype_versions) diff --git a/igniter/tools.py b/igniter/tools.py index a9d592acf0..5c2c64a14b 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -184,11 +184,7 @@ def get_openpype_path_from_settings(settings: dict) -> Union[str, None]: if paths and isinstance(paths, str): paths = [paths] - # Loop over paths and return only existing - for path in paths: - if os.path.exists(path): - return path - return None + return next((path for path in paths if os.path.exists(path)), None) def get_expected_studio_version_str( @@ -206,10 +202,7 @@ def get_expected_studio_version_str( mongo_url = os.environ.get("OPENPYPE_MONGO") if global_settings is None: global_settings = get_openpype_global_settings(mongo_url) - if staging: - key = "staging_version" - else: - key = "production_version" + key = "staging_version" if staging else "production_version" return global_settings.get(key) or "" From 84b3bc3db2bd2a0f31af991b98e3c3a022e8e4e4 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 13 Oct 2022 11:34:57 +0200 Subject: [PATCH 04/75] :recycle: remove staging logic --- igniter/bootstrap_repos.py | 18 ++---- openpype/cli.py | 17 +++-- openpype/lib/openpype_version.py | 8 +++ start.py | 104 +++++++++++-------------------- 4 files changed, 54 insertions(+), 93 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index bbb3dd506c..6d583469ef 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -57,7 +57,6 @@ class OpenPypeVersion(semver.VersionInfo): """Class for storing information about OpenPype version. Attributes: - staging (bool): True if it is staging version path (str): path to OpenPype """ @@ -161,15 +160,6 @@ class OpenPypeVersion(semver.VersionInfo): version = OpenPypeVersion.parse(string[m.start():m.end()]) return version - @classmethod - def parse(cls, version): - """Extends parse to handle ta handle staging variant.""" - v = super().parse(version) - openpype_version = cls(major=v.major, minor=v.minor, - patch=v.patch, prerelease=v.prerelease, - build=v.build) - return openpype_version - def __hash__(self): return hash(self.path) if self.path else hash(str(self)) @@ -448,7 +438,6 @@ class OpenPypeVersion(semver.VersionInfo): The version does not contain information about path and source. This is utility version to get the latest version from all found. - Build version is not listed if staging is enabled. Arguments 'local' and 'remote' define if local and remote repository versions are used. All versions are used if both are not set (or set @@ -483,7 +472,7 @@ class OpenPypeVersion(semver.VersionInfo): return all_versions[-1] @classmethod - def get_expected_studio_version(cls, global_settings=None): + def get_expected_studio_version(cls, staging=False, global_settings=None): """Expected OpenPype version that should be used at the moment. If version is not defined in settings the latest found version is @@ -492,12 +481,13 @@ class OpenPypeVersion(semver.VersionInfo): Using precached global settings is needed for usage inside OpenPype. Args: + staging (bool): Staging version or production version. global_settings (dict): Optional precached global settings. Returns: OpenPypeVersion: Version that should be used. """ - result = get_expected_studio_version_str(global_settings) + result = get_expected_studio_version_str(staging, global_settings) if not result: return None return OpenPypeVersion(version=result) @@ -567,7 +557,7 @@ class BootstrapRepos: """Get path for specific version in list of OpenPype versions. Args: - version (str): Version string to look for (1.2.4+staging) + version (str): Version string to look for (1.2.4-nightly.1+test) version_list (list of OpenPypeVersion): list of version to search. Returns: diff --git a/openpype/cli.py b/openpype/cli.py index 398d1a94c0..e3eacad8aa 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -16,14 +16,13 @@ from .pype_commands import PypeCommands @click.option("--use-staging", is_flag=True, expose_value=False, help="use staging variants") @click.option("--list-versions", is_flag=True, expose_value=False, - help=("list all detected versions. Use With `--use-staging " - "to list staging versions.")) + help="list all detected versions.") @click.option("--validate-version", expose_value=False, help="validate given version integrity") @click.option("--debug", is_flag=True, expose_value=False, - help=("Enable debug")) + help="Enable debug") @click.option("--verbose", expose_value=False, - help=("Change OpenPype log level (debug - critical or 0-50)")) + help="Change OpenPype log level (debug - critical or 0-50)") def main(ctx): """Pype is main command serving as entry point to pipeline system. @@ -416,20 +415,18 @@ def unpack_project(zipfile, root): @main.command() def interactive(): - """Interative (Python like) console. + """Interactive (Python like) console. - Helpfull command not only for development to directly work with python + Helpful command not only for development to directly work with python interpreter. Warning: - Executable 'openpype_gui' on windows won't work. + Executable 'openpype_gui' on Windows won't work. """ from openpype.version import __version__ - banner = "OpenPype {}\nPython {} on {}".format( - __version__, sys.version, sys.platform - ) + banner = f"OpenPype {__version__}\nPython {sys.version} on {sys.platform}" code.interact(banner) diff --git a/openpype/lib/openpype_version.py b/openpype/lib/openpype_version.py index d547d34755..8c74f96da6 100644 --- a/openpype/lib/openpype_version.py +++ b/openpype/lib/openpype_version.py @@ -11,6 +11,7 @@ repository or locally available. import os import sys +import warnings import openpype.version @@ -60,9 +61,16 @@ def is_running_from_build(): def is_running_staging(): """Currently used OpenPype is staging version. + Deprecated: + Since 3.15 + Returns: bool: True if openpype version containt 'staging'. """ + warnings.warn( + "Staging version logic set by version string is deprecated.", + DeprecationWarning + ) if "staging" in get_openpype_version(): return True return False diff --git a/start.py b/start.py index d1198a85e4..e59de93236 100644 --- a/start.py +++ b/start.py @@ -516,8 +516,6 @@ def _process_arguments() -> tuple: if m and m.group('version'): use_version = m.group('version') _print(f">>> Requested version [ {use_version} ]") - if "+staging" in use_version: - use_staging = True break if use_version is None: @@ -682,8 +680,7 @@ def _find_frozen_openpype(use_version: str = None, Path: Path to version to be used. Raises: - RuntimeError: If no OpenPype version are found or no staging version - (if requested). + RuntimeError: If no OpenPype version are found. """ # Collect OpenPype versions @@ -698,13 +695,10 @@ def _find_frozen_openpype(use_version: str = None, if use_version.lower() == "latest": # Version says to use latest version _print(">>> Finding latest version defined by use version") - openpype_version = bootstrap.find_latest_openpype_version( - use_staging) + openpype_version = bootstrap.find_latest_openpype_version() else: _print(f">>> Finding specified version \"{use_version}\"") - openpype_version = bootstrap.find_openpype_version( - use_version, use_staging - ) + openpype_version = bootstrap.find_openpype_version(use_version) if openpype_version is None: raise OpenPypeVersionNotFound( @@ -714,8 +708,7 @@ def _find_frozen_openpype(use_version: str = None, elif studio_version is not None: # Studio has defined a version to use _print(f">>> Finding studio version \"{studio_version}\"") - openpype_version = bootstrap.find_openpype_version( - studio_version, use_staging) + openpype_version = bootstrap.find_openpype_version(studio_version) if openpype_version is None: raise OpenPypeVersionNotFound(( "Requested OpenPype version " @@ -728,20 +721,15 @@ def _find_frozen_openpype(use_version: str = None, _print(( ">>> Finding latest version " f"with [ {installed_version} ]")) - openpype_version = bootstrap.find_latest_openpype_version( - use_staging) + openpype_version = bootstrap.find_latest_openpype_version() if openpype_version is None: - if use_staging: - reason = "Didn't find any staging versions." - else: - reason = "Didn't find any versions." - raise OpenPypeVersionNotFound(reason) + raise OpenPypeVersionNotFound("Didn't find any versions.") # get local frozen version and add it to detected version so if it is # newer it will be used instead. if installed_version == openpype_version: - version_path = _bootstrap_from_code(use_version, use_staging) + version_path = _bootstrap_from_code(use_version) openpype_version = OpenPypeVersion( version=BootstrapRepos.get_version(version_path), path=version_path) @@ -805,8 +793,8 @@ def _find_frozen_openpype(use_version: str = None, return openpype_version.path -def _bootstrap_from_code(use_version, use_staging): - """Bootstrap live code (or the one coming with frozen OpenPype. +def _bootstrap_from_code(use_version): + """Bootstrap live code (or the one coming with frozen OpenPype). Args: use_version: (str): specific version to use. @@ -829,33 +817,25 @@ def _bootstrap_from_code(use_version, use_staging): local_version = bootstrap.get_version(Path(_openpype_root)) switch_str = f" - will switch to {use_version}" if use_version and use_version != local_version else "" # noqa _print(f" - booting version: {local_version}{switch_str}") - assert local_version + if not local_version: + raise OpenPypeVersionNotFound( + f"Cannot find version at {_openpype_root}") else: # get current version of OpenPype local_version = OpenPypeVersion.get_installed_version_str() # All cases when should be used different version than build - if (use_version and use_version != local_version) or use_staging: + if use_version and use_version != local_version: if use_version: # Explicit version should be used - version_to_use = bootstrap.find_openpype_version( - use_version, use_staging - ) + version_to_use = bootstrap.find_openpype_version(use_version) if version_to_use is None: raise OpenPypeVersionIncompatible( f"Requested version \"{use_version}\" was not found.") else: - # Staging version should be used - version_to_use = bootstrap.find_latest_openpype_version( - use_staging - ) + version_to_use = bootstrap.find_latest_openpype_version() if version_to_use is None: - if use_staging: - reason = "Didn't find any staging versions." - else: - # This reason is backup for possible bug in code - reason = "Didn't find any versions." - raise OpenPypeVersionNotFound(reason) + raise OpenPypeVersionNotFound("Didn't find any versions.") # Start extraction of version if needed if version_to_use.path.is_file(): @@ -913,10 +893,7 @@ def _bootstrap_from_code(use_version, use_staging): def _boot_validate_versions(use_version, local_version): _print(f">>> Validating version [ {use_version} ]") - openpype_versions = bootstrap.find_openpype(include_zips=True, - staging=True) - openpype_versions += bootstrap.find_openpype(include_zips=True, - staging=False) + openpype_versions = bootstrap.find_openpype(include_zips=True) v: OpenPypeVersion found = [v for v in openpype_versions if str(v) == use_version] if not found: @@ -932,14 +909,7 @@ def _boot_validate_versions(use_version, local_version): _print(f'{">>> " if valid else "!!! "}{message}') -def _boot_print_versions(use_staging, local_version, openpype_root): - if not use_staging: - _print("--- This will list only non-staging versions detected.") - _print(" To see staging versions, use --use-staging argument.") - else: - _print("--- This will list only staging versions detected.") - _print(" To see other version, omit --use-staging argument.") - +def _boot_print_versions(openpype_root): if getattr(sys, 'frozen', False): local_version = bootstrap.get_version(Path(openpype_root)) else: @@ -947,16 +917,12 @@ def _boot_print_versions(use_staging, local_version, openpype_root): compatible_with = OpenPypeVersion(version=local_version) if "--all" in sys.argv: - compatible_with = None _print("--- Showing all version (even those not compatible).") else: _print(("--- Showing only compatible versions " f"with [ {compatible_with.major}.{compatible_with.minor} ]")) - openpype_versions = bootstrap.find_openpype( - include_zips=True, - staging=use_staging, - ) + openpype_versions = bootstrap.find_openpype(include_zips=True) openpype_versions = [ version for version in openpype_versions if version.is_compatible( @@ -966,12 +932,11 @@ def _boot_print_versions(use_staging, local_version, openpype_root): list_versions(openpype_versions, local_version) -def _boot_handle_missing_version(local_version, use_staging, message): +def _boot_handle_missing_version(local_version, message): _print(message) if os.environ.get("OPENPYPE_HEADLESS_MODE") == "1": openpype_versions = bootstrap.find_openpype( - include_zips=True, staging=use_staging - ) + include_zips=True) list_versions(openpype_versions, local_version) else: igniter.show_message_dialog("Version not found", message) @@ -1005,7 +970,6 @@ def boot(): "is overridden by command line argument.")) else: _print(">>> version set by environment variable") - use_staging = "staging" in os.getenv("OPENPYPE_VERSION") use_version = os.getenv("OPENPYPE_VERSION") # ------------------------------------------------------------------------ @@ -1059,7 +1023,7 @@ def boot(): os.environ["OPENPYPE_PATH"] = openpype_path if "print_versions" in commands: - _boot_print_versions(use_staging, local_version, OPENPYPE_ROOT) + _boot_print_versions(OPENPYPE_ROOT) sys.exit(1) # ------------------------------------------------------------------------ @@ -1072,7 +1036,7 @@ def boot(): try: version_path = _find_frozen_openpype(use_version, use_staging) except OpenPypeVersionNotFound as exc: - _boot_handle_missing_version(local_version, use_staging, str(exc)) + _boot_handle_missing_version(local_version, str(exc)) sys.exit(1) except RuntimeError as e: @@ -1088,10 +1052,10 @@ def boot(): _print("--- version is valid") else: try: - version_path = _bootstrap_from_code(use_version, use_staging) + version_path = _bootstrap_from_code(use_version) except OpenPypeVersionNotFound as exc: - _boot_handle_missing_version(local_version, use_staging, str(exc)) + _boot_handle_missing_version(local_version, str(exc)) sys.exit(1) # set this to point either to `python` from venv in case of live code @@ -1172,10 +1136,10 @@ def get_info(use_staging=None) -> list: inf.append(("OpenPype variant", "staging")) else: inf.append(("OpenPype variant", "production")) - inf.append( - ("Running OpenPype from", os.environ.get('OPENPYPE_REPOS_ROOT')) + inf.extend([ + ("Running OpenPype from", os.environ.get('OPENPYPE_REPOS_ROOT')), + ("Using mongodb", components["host"])] ) - inf.append(("Using mongodb", components["host"])) if os.environ.get("FTRACK_SERVER"): inf.append(("Using FTrack at", @@ -1194,11 +1158,13 @@ def get_info(use_staging=None) -> list: mongo_components = get_default_components() if mongo_components["host"]: - inf.append(("Logging to MongoDB", mongo_components["host"])) - inf.append((" - port", mongo_components["port"] or "")) - inf.append((" - database", Logger.log_database_name)) - inf.append((" - collection", Logger.log_collection_name)) - inf.append((" - user", mongo_components["username"] or "")) + inf.extend([ + ("Logging to MongoDB", mongo_components["host"]), + (" - port", mongo_components["port"] or ""), + (" - database", Logger.log_database_name), + (" - collection", Logger.log_collection_name), + (" - user", mongo_components["username"] or "") + ]) if mongo_components["auth_db"]: inf.append((" - auth source", mongo_components["auth_db"])) From 59e23f21d02613d97217bf188f613796759c22b5 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 13 Oct 2022 11:35:38 +0200 Subject: [PATCH 05/75] :memo: change staging logic in documentation --- website/docs/admin_distribute.md | 10 +++------- website/docs/admin_openpype_commands.md | 2 +- website/docs/admin_use.md | 5 +---- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/website/docs/admin_distribute.md b/website/docs/admin_distribute.md index 2cccce0fbd..169b97e63d 100644 --- a/website/docs/admin_distribute.md +++ b/website/docs/admin_distribute.md @@ -52,14 +52,10 @@ The default locations are: ### Staging vs. Production -You can have version of OpenPype with experimental features you want to try somewhere but you -don't want to disrupt your production. You can tag version as **staging** simply by appending `+staging` -to its name. +You can have version of OpenPype with experimental features you want to try somewhere, but you +don't want to disrupt your production. You can set such version in th Settings. -So if you have OpenPype version like `OpenPype-v3.0.0.zip` just name it `OpenPype-v3.0.0+staging.zip`. -When both these versions are present, production one will always take precedence over staging. - -You can run OpenPype with `--use-staging` argument to add use staging versions. +You can run OpenPype with `--use-staging` argument to use staging version specified in the Settings. :::note Running staging version is identified by orange **P** icon in system tray. diff --git a/website/docs/admin_openpype_commands.md b/website/docs/admin_openpype_commands.md index 85f661d51e..131b6c0a51 100644 --- a/website/docs/admin_openpype_commands.md +++ b/website/docs/admin_openpype_commands.md @@ -22,7 +22,7 @@ openpype_console --use-version=3.0.0-foo+bar `--use-staging` - to use staging versions of OpenPype. -`--list-versions [--use-staging]` - to list available versions. +`--list-versions` - to list available versions. `--validate-version` - to validate integrity of given version diff --git a/website/docs/admin_use.md b/website/docs/admin_use.md index 1c4ae9e01c..c92ac3a77a 100644 --- a/website/docs/admin_use.md +++ b/website/docs/admin_use.md @@ -43,8 +43,7 @@ You can use following command line arguments: openpype_console --use-version=3.0.1 ``` -`--use-staging` - to specify you prefer staging version. In that case it will be used -(if found) instead of production one. +`--use-staging` - to specify you prefer staging version. In that case it will be used instead of production one. :::tip List available versions To list all available versions, use: @@ -52,8 +51,6 @@ To list all available versions, use: ```shell openpype_console --list-versions ``` - -You can add `--use-staging` to list staging versions. ::: If you want to validate integrity of some available version, you can use: From 7bd7c0391c12a55236122d216f1f123e8f571021 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 13 Oct 2022 11:59:10 +0200 Subject: [PATCH 06/75] :white_check_mark: fix tests --- tests/unit/igniter/test_bootstrap_repos.py | 30 ++++++++++++---------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/tests/unit/igniter/test_bootstrap_repos.py b/tests/unit/igniter/test_bootstrap_repos.py index 10278c4928..8b32bbe03c 100644 --- a/tests/unit/igniter/test_bootstrap_repos.py +++ b/tests/unit/igniter/test_bootstrap_repos.py @@ -33,11 +33,11 @@ def test_openpype_version(printer): assert str(v2) == "1.2.3-x" assert v1 > v2 - v3 = OpenPypeVersion(1, 2, 3, staging=True) - assert str(v3) == "1.2.3+staging" + v3 = OpenPypeVersion(1, 2, 3) + assert str(v3) == "1.2.3" - v4 = OpenPypeVersion(1, 2, 3, staging="True", prerelease="rc.1") - assert str(v4) == "1.2.3-rc.1+staging" + v4 = OpenPypeVersion(1, 2, 3, prerelease="rc.1") + assert str(v4) == "1.2.3-rc.1" assert v3 > v4 assert v1 > v4 assert v4 < OpenPypeVersion(1, 2, 3, prerelease="rc.1") @@ -73,7 +73,7 @@ def test_openpype_version(printer): OpenPypeVersion(4, 8, 10), OpenPypeVersion(4, 8, 20), OpenPypeVersion(4, 8, 9), - OpenPypeVersion(1, 2, 3, staging=True), + OpenPypeVersion(1, 2, 3), OpenPypeVersion(1, 2, 3, build="foo") ] res = sorted(sort_versions) @@ -104,27 +104,26 @@ def test_openpype_version(printer): with pytest.raises(ValueError): _ = OpenPypeVersion(version="booobaa") - v11 = OpenPypeVersion(version="4.6.7-foo+staging") + v11 = OpenPypeVersion(version="4.6.7-foo") assert v11.major == 4 assert v11.minor == 6 assert v11.patch == 7 - assert v11.staging is True assert v11.prerelease == "foo" def test_get_main_version(): - ver = OpenPypeVersion(1, 2, 3, staging=True, prerelease="foo") + ver = OpenPypeVersion(1, 2, 3, prerelease="foo") assert ver.get_main_version() == "1.2.3" def test_get_version_path_from_list(): versions = [ OpenPypeVersion(1, 2, 3, path=Path('/foo/bar')), - OpenPypeVersion(3, 4, 5, staging=True, path=Path("/bar/baz")), + OpenPypeVersion(3, 4, 5, path=Path("/bar/baz")), OpenPypeVersion(6, 7, 8, prerelease="x", path=Path("boo/goo")) ] path = BootstrapRepos.get_version_path_from_list( - "3.4.5+staging", versions) + "3.4.5", versions) assert path == Path("/bar/baz") @@ -362,12 +361,15 @@ def test_find_openpype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): result = fix_bootstrap.find_openpype(include_zips=True) # we should have results as file were created assert result is not None, "no OpenPype version found" - # latest item in `result` should be latest version found. + # latest item in `result` should be the latest version found. + # this will be `7.2.10-foo+staging` even with *staging* in since we've + # dropped the logic to handle staging separately and in alphabetical + # sorting it is after `strange`. expected_path = Path( d_path / "{}{}{}".format( - test_versions_2[3].prefix, - test_versions_2[3].version, - test_versions_2[3].suffix + test_versions_2[4].prefix, + test_versions_2[4].version, + test_versions_2[4].suffix ) ) assert result, "nothing found" From 9330d92fdc8520f7f032fcec803a6c0848b37460 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 13 Oct 2022 15:26:30 +0200 Subject: [PATCH 07/75] fix 'get_remote_versions' call in version entity --- openpype/settings/entities/__init__.py | 8 ++------ openpype/settings/entities/op_version_entity.py | 17 +++-------------- .../schemas/system_schema/schema_general.json | 4 ++-- 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index b2cb2204f4..5e3a76094e 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -123,10 +123,7 @@ from .dict_conditional import ( ) from .anatomy_entities import AnatomyEntity -from .op_version_entity import ( - ProductionVersionsInputEntity, - StagingVersionsInputEntity -) +from .op_version_entity import VersionsInputEntity __all__ = ( "DefaultsNotDefined", @@ -188,6 +185,5 @@ __all__ = ( "AnatomyEntity", - "ProductionVersionsInputEntity", - "StagingVersionsInputEntity" + "VersionsInputEntity", ) diff --git a/openpype/settings/entities/op_version_entity.py b/openpype/settings/entities/op_version_entity.py index 782d65a446..f79048222e 100644 --- a/openpype/settings/entities/op_version_entity.py +++ b/openpype/settings/entities/op_version_entity.py @@ -66,24 +66,13 @@ class OpenPypeVersionInput(TextEntity): return super(OpenPypeVersionInput, self).convert_to_valid_type(value) -class ProductionVersionsInputEntity(OpenPypeVersionInput): +class VersionsInputEntity(OpenPypeVersionInput): """Entity meant only for global settings to define production version.""" - schema_types = ["production-versions-text"] + schema_types = ["versions-text"] def _get_openpype_versions(self): - versions = get_remote_versions(staging=False, production=True) + versions = get_remote_versions() if versions is None: return [] versions.append(get_installed_version()) return sorted(versions) - - -class StagingVersionsInputEntity(OpenPypeVersionInput): - """Entity meant only for global settings to define staging version.""" - schema_types = ["staging-versions-text"] - - def _get_openpype_versions(self): - versions = get_remote_versions(staging=True, production=False) - if versions is None: - return [] - return sorted(versions) diff --git a/openpype/settings/entities/schemas/system_schema/schema_general.json b/openpype/settings/entities/schemas/system_schema/schema_general.json index 5b6d8d5d62..d6c22fe54c 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_general.json +++ b/openpype/settings/entities/schemas/system_schema/schema_general.json @@ -146,12 +146,12 @@ "label": "Define explicit OpenPype version that should be used. Keep empty to use latest available version." }, { - "type": "production-versions-text", + "type": "versions-text", "key": "production_version", "label": "Production version" }, { - "type": "staging-versions-text", + "type": "versions-text", "key": "staging_version", "label": "Staging version" }, From 1bf4fca218b1cd7969f41a2d7dc994d9f8f08467 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 11:34:56 +0200 Subject: [PATCH 08/75] don't store settings to ordered list of staging and production (unused anyway) --- openpype/settings/handlers.py | 35 ++--------------------------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index def8c16ea7..24544e24e2 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -577,8 +577,6 @@ class MongoSettingsHandler(SettingsHandler): key_suffix = "_versioned" _version_order_key = "versions_order" _all_versions_keys = "all_versions" - _production_versions_key = "production_versions" - _staging_versions_key = "staging_versions" def __init__(self): # Get mongo connection @@ -997,10 +995,7 @@ class MongoSettingsHandler(SettingsHandler): return self._version_order_checked = True - from openpype.lib.openpype_version import ( - get_OpenPypeVersion, - is_running_staging - ) + from openpype.lib.openpype_version import get_OpenPypeVersion OpenPypeVersion = get_OpenPypeVersion() # Skip if 'OpenPypeVersion' is not available @@ -1012,25 +1007,11 @@ class MongoSettingsHandler(SettingsHandler): if not doc: doc = {"type": self._version_order_key} - if self._production_versions_key not in doc: - doc[self._production_versions_key] = [] - - if self._staging_versions_key not in doc: - doc[self._staging_versions_key] = [] - if self._all_versions_keys not in doc: doc[self._all_versions_keys] = [] - if is_running_staging(): - versions_key = self._staging_versions_key - else: - versions_key = self._production_versions_key - # Skip if current version is already available - if ( - self._current_version in doc[self._all_versions_keys] - and self._current_version in doc[versions_key] - ): + if self._current_version in doc[self._all_versions_keys]: return if self._current_version not in doc[self._all_versions_keys]: @@ -1047,18 +1028,6 @@ class MongoSettingsHandler(SettingsHandler): str(version) for version in sorted(all_objected_versions) ] - if self._current_version not in doc[versions_key]: - objected_versions = [ - OpenPypeVersion(version=self._current_version) - ] - for version_str in doc[versions_key]: - objected_versions.append(OpenPypeVersion(version=version_str)) - - # Update versions list and push changes to Mongo - doc[versions_key] = [ - str(version) for version in sorted(objected_versions) - ] - self.collection.replace_one( {"type": self._version_order_key}, doc, From 2341f08492f09c12c0011a63e1a68df895c3ba4a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 16:45:40 +0200 Subject: [PATCH 09/75] store '--use-staging' to 'OPENPYPE_USE_STAGING' env variable --- start.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/start.py b/start.py index e59de93236..2fca2a763d 100644 --- a/start.py +++ b/start.py @@ -242,6 +242,9 @@ if "--debug" in sys.argv: sys.argv.remove("--debug") os.environ["OPENPYPE_DEBUG"] = "1" +if "--use-staging" in sys.argv: + sys.argv.remove("--use-staging") + os.environ["OPENPYPE_USE_STAGING"] = "1" import igniter # noqa: E402 from igniter import BootstrapRepos # noqa: E402 @@ -484,7 +487,6 @@ def _process_arguments() -> tuple: """ # check for `--use-version=3.0.0` argument and `--use-staging` use_version = None - use_staging = False commands = [] # OpenPype version specification through arguments @@ -542,10 +544,6 @@ def _process_arguments() -> tuple: " proper version string.")) sys.exit(1) - if "--use-staging" in sys.argv: - use_staging = True - sys.argv.remove("--use-staging") - if "--list-versions" in sys.argv: commands.append("print_versions") sys.argv.remove("--list-versions") @@ -568,7 +566,7 @@ def _process_arguments() -> tuple: sys.argv.pop(idx) sys.argv.insert(idx, "tray") - return use_version, use_staging, commands + return use_version, commands def _determine_mongodb() -> str: @@ -962,7 +960,8 @@ def boot(): # Process arguments # ------------------------------------------------------------------------ - use_version, use_staging, commands = _process_arguments() + use_version, commands = _process_arguments() + use_staging = os.environ.get("OPENPYPE_USE_STAGING") == "1" if os.getenv("OPENPYPE_VERSION"): if use_version: From 0e9164f7ff458606f4adb56845507819a42d7c57 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 16:46:03 +0200 Subject: [PATCH 10/75] settings have function to receive global settings --- openpype/settings/__init__.py | 4 ++- openpype/settings/handlers.py | 64 +++++++++++++++++++++++++++-------- openpype/settings/lib.py | 11 ++++++ 3 files changed, 63 insertions(+), 16 deletions(-) diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index ca7157812d..22d734ae58 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -18,11 +18,12 @@ from .exceptions import ( ) from .lib import ( get_general_environments, + get_global_settings, get_system_settings, get_project_settings, get_current_project_settings, get_anatomy_settings, - get_local_settings + get_local_settings, ) from .entities import ( SystemSettings, @@ -49,6 +50,7 @@ __all__ = ( "SaveWarningExc", "get_general_environments", + "get_global_settings", "get_system_settings", "get_project_settings", "get_current_project_settings", diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 24544e24e2..373029d9df 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -181,7 +181,16 @@ class SettingsStateInfo: @six.add_metaclass(ABCMeta) -class SettingsHandler: +class SettingsHandler(object): + global_keys = { + "openpype_path", + "admin_password", + "log_to_server", + "disk_mapping", + "production_version", + "staging_version" + } + @abstractmethod def save_studio_settings(self, data): """Save studio overrides of system settings. @@ -328,6 +337,19 @@ class SettingsHandler: """ pass + @abstractmethod + def get_global_settings(self): + """Studio global settings available across versions. + + Output must contain all keys from 'global_keys'. If value is not set + the output value should be 'None'. + + Returns: + Dict[str, Any]: Global settings same across versions. + """ + + pass + # Clear methods - per version # - clearing may be helpfull when a version settings were created for # testing purposes @@ -566,14 +588,6 @@ class CacheValues: class MongoSettingsHandler(SettingsHandler): """Settings handler that use mongo for storing and loading of settings.""" - global_general_keys = ( - "openpype_path", - "admin_password", - "log_to_server", - "disk_mapping", - "production_version", - "staging_version" - ) key_suffix = "_versioned" _version_order_key = "versions_order" _all_versions_keys = "all_versions" @@ -603,6 +617,7 @@ class MongoSettingsHandler(SettingsHandler): self.collection = settings_collection[database_name][collection_name] + self.global_settings_cache = CacheValues() self.system_settings_cache = CacheValues() self.project_settings_cache = collections.defaultdict(CacheValues) self.project_anatomy_cache = collections.defaultdict(CacheValues) @@ -636,6 +651,23 @@ class MongoSettingsHandler(SettingsHandler): self._prepare_project_settings_keys() return self._attribute_keys + def get_global_settings_doc(self): + if self.global_settings_cache.is_outdated: + global_settings_doc = self.collection.find_one({ + "type": GLOBAL_SETTINGS_KEY + }) or {} + self.global_settings_cache.update_data(global_settings_doc, None) + return self.global_settings_cache.data_copy() + + def get_global_settings(self): + global_settings_doc = self.get_global_settings_doc() + global_settings = global_settings_doc.get("data", {}) + return { + key: global_settings[key] + for key in self.global_keys + if key in global_settings + } + def _extract_global_settings(self, data): """Extract global settings data from system settings overrides. @@ -652,7 +684,7 @@ class MongoSettingsHandler(SettingsHandler): general_data = data["general"] # Add predefined keys to global settings if are set - for key in self.global_general_keys: + for key in self.global_keys: if key not in general_data: continue # Pop key from values @@ -696,7 +728,7 @@ class MongoSettingsHandler(SettingsHandler): # Check if data contain any key from predefined keys any_key_found = False if globals_data: - for key in self.global_general_keys: + for key in self.global_keys: if key in globals_data: any_key_found = True break @@ -723,7 +755,7 @@ class MongoSettingsHandler(SettingsHandler): system_settings_data["general"] = system_general overridden_keys = system_general.get(M_OVERRIDDEN_KEY) or [] - for key in self.global_general_keys: + for key in self.global_keys: if key not in globals_data: continue @@ -765,6 +797,10 @@ class MongoSettingsHandler(SettingsHandler): global_settings = self._extract_global_settings( system_settings_data ) + self.global_settings_cache.update_data( + global_settings, + None + ) system_settings_doc = self.collection.find_one( { @@ -1267,9 +1303,7 @@ class MongoSettingsHandler(SettingsHandler): def get_studio_system_settings_overrides(self, return_version): """Studio overrides of system settings.""" if self.system_settings_cache.is_outdated: - globals_document = self.collection.find_one({ - "type": GLOBAL_SETTINGS_KEY - }) + globals_document = self.get_global_settings_doc() document, version = self._get_system_settings_overrides_doc() last_saved_info = SettingsStateInfo.from_document( diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 5eaddf6e6e..d6f31b2593 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -1043,6 +1043,17 @@ def get_current_project_settings(): return get_project_settings(project_name) +@require_handler +def get_global_settings(): + default_settings = load_openpype_default_settings() + default_values = default_settings["system_settings"]["general"] + studio_values = _SETTINGS_HANDLER.get_global_settings() + return { + key: studio_values.get(key, default_values.get(key)) + for key in _SETTINGS_HANDLER.global_keys + } + + def get_general_environments(): """Get general environments. From 5958229ac87d7a86c1379a355fa4d12a04f1e6f4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 16:46:32 +0200 Subject: [PATCH 11/75] added function to check if staging is enabled --- openpype/lib/openpype_version.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/lib/openpype_version.py b/openpype/lib/openpype_version.py index 8c74f96da6..e287d20e2d 100644 --- a/openpype/lib/openpype_version.py +++ b/openpype/lib/openpype_version.py @@ -58,6 +58,10 @@ def is_running_from_build(): return True +def is_staging_enabled(): + return os.environ.get("OPENPYPE_USE_STAGING") == "1" + + def is_running_staging(): """Currently used OpenPype is staging version. From 929db9fd13c283cecb5ed21e12669206c093d2d4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 16:46:53 +0200 Subject: [PATCH 12/75] fixed 'get_latest_version' --- igniter/bootstrap_repos.py | 2 +- openpype/lib/openpype_version.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index fd411272c4..2854a047ae 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -466,7 +466,7 @@ class OpenPypeVersion(semver.VersionInfo): installed_version = OpenPypeVersion.get_installed_version() local_versions = OpenPypeVersion.get_local_versions() if local else [] remote_versions = OpenPypeVersion.get_remote_versions() if remote else [] # noqa: E501 - all_versions = local_versions + remote_versions + installed_version + all_versions = local_versions + remote_versions + [installed_version] all_versions.sort() return all_versions[-1] diff --git a/openpype/lib/openpype_version.py b/openpype/lib/openpype_version.py index e287d20e2d..4c927ed191 100644 --- a/openpype/lib/openpype_version.py +++ b/openpype/lib/openpype_version.py @@ -143,13 +143,11 @@ def get_remote_versions(*args, **kwargs): return None -def get_latest_version(staging=None, local=None, remote=None): +def get_latest_version(local=None, remote=None): """Get latest version from repository path.""" - if staging is None: - staging = is_running_staging() + if op_version_control_available(): return get_OpenPypeVersion().get_latest_version( - staging=staging, local=local, remote=remote ) From 1cd4cbcc09079c07c50fbe802e9b9947401a4a77 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 16:47:03 +0200 Subject: [PATCH 13/75] reimplemented 'is_running_staging' --- openpype/lib/openpype_version.py | 59 ++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/openpype/lib/openpype_version.py b/openpype/lib/openpype_version.py index 4c927ed191..586fc56eb7 100644 --- a/openpype/lib/openpype_version.py +++ b/openpype/lib/openpype_version.py @@ -11,7 +11,6 @@ repository or locally available. import os import sys -import warnings import openpype.version @@ -65,19 +64,59 @@ def is_staging_enabled(): def is_running_staging(): """Currently used OpenPype is staging version. - Deprecated: - Since 3.15 + This function is not 100% proper check of staging version. It is possible + to have enabled to use staging version but be in different one. + + The function is based on 4 factors: + - env 'OPENPYPE_IS_STAGING' is set + - current production version + - current staging version + - use staging is enabled + + First checks for 'OPENPYPE_IS_STAGING' environment which can be set to '1'. + The value should be set only when a process without access to + OpenPypeVersion is launched (e.g. in DCCs). If current version is same + as production version it is expected that it is not staging, and it + doesn't matter what would 'is_staging_enabled' return. If current version + is same as staging version it is expected we're in staging. In all other + cases 'is_staging_enabled' is used as source of outpu value. + + The function is used to decide which icon is used. To check e.g. updates + the output should be combined with other functions from this file. Returns: - bool: True if openpype version containt 'staging'. + bool: Using staging version or not. """ - warnings.warn( - "Staging version logic set by version string is deprecated.", - DeprecationWarning - ) - if "staging" in get_openpype_version(): + + if os.environ.get("OPENPYPE_IS_STAGING") == "1": return True - return False + + if not op_version_control_available(): + return False + + from openpype.settings import get_global_settings + + global_settings = get_global_settings() + production_version = global_settings["production_version"] + latest_version = None + if not production_version or production_version == "latest": + latest_version = get_latest_version(local=False, remote=True) + production_version = latest_version + + current_version = get_openpype_version() + if current_version == production_version: + return False + + staging_version = global_settings["staging_version"] + if not staging_version or staging_version == "latest": + if latest_version is None: + latest_version = get_latest_version(local=False, remote=True) + staging_version = latest_version + + if current_version == production_version: + return True + + return is_staging_enabled() # ---------------------------------------- From 8f617d5d6e6410639fb147acbfd2c71bc564b1f3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 16:47:56 +0200 Subject: [PATCH 14/75] use 'run_detached_process' from lib to start new tray --- openpype/tools/tray/pype_tray.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 3842a4e216..8817990f07 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -2,7 +2,6 @@ import collections import os import sys import atexit -import subprocess import platform @@ -11,8 +10,9 @@ from Qt import QtCore, QtGui, QtWidgets import openpype.version from openpype import resources, style from openpype.lib import ( - get_openpype_execute_args, Logger, + get_openpype_execute_args, + run_detached_process, ) from openpype.lib.openpype_version import ( op_version_control_available, @@ -590,14 +590,8 @@ class TrayManager: kwargs["env"].pop("OPENPYPE_VERSION", None) args.extend(additional_args) - if platform.system().lower() == "windows": - flags = ( - subprocess.CREATE_NEW_PROCESS_GROUP - | subprocess.DETACHED_PROCESS - ) - kwargs["creationflags"] = flags - subprocess.Popen(args, **kwargs) + run_detached_process(args, **kwargs) self.exit() def exit(self): From ebbcbdde1221fedbbb315e6a5c3e1e17a4cf10de Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 16:48:58 +0200 Subject: [PATCH 15/75] simplified args for subprocess --- openpype/tools/tray/pype_tray.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 8817990f07..a49471a944 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -562,9 +562,7 @@ class TrayManager: logic will decide which version will be used. """ args = get_openpype_execute_args() - kwargs = { - "env": dict(os.environ.items()) - } + envs = dict(os.environ.items()) # Create a copy of sys.argv additional_args = list(sys.argv) @@ -577,7 +575,7 @@ class TrayManager: expected_version = get_expected_version() if expected_version is not None: reset_version = False - kwargs["env"]["OPENPYPE_VERSION"] = str(expected_version) + envs["OPENPYPE_VERSION"] = str(expected_version) else: # Trigger reset of version if expected version was not found reset_version = True @@ -587,11 +585,11 @@ class TrayManager: # Add staging flag if was running from staging if is_running_staging(): args.append("--use-staging") - kwargs["env"].pop("OPENPYPE_VERSION", None) + envs.pop("OPENPYPE_VERSION", None) args.extend(additional_args) - run_detached_process(args, **kwargs) + run_detached_process(args, env=envs) self.exit() def exit(self): From 0b68cbae5c064353119c74e32eee5db08e6c3d28 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 16:49:13 +0200 Subject: [PATCH 16/75] added cleanup of additional arguments --- openpype/tools/tray/pype_tray.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index a49471a944..3a457d2951 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -571,7 +571,9 @@ class TrayManager: if args[-1] == additional_args[0]: additional_args.pop(0) + cleanup_additional_args = False if use_expected_version: + cleanup_additional_args = True expected_version = get_expected_version() if expected_version is not None: reset_version = False @@ -585,8 +587,17 @@ class TrayManager: # Add staging flag if was running from staging if is_running_staging(): args.append("--use-staging") + cleanup_additional_args = True envs.pop("OPENPYPE_VERSION", None) + if cleanup_additional_args: + _additional_args = [] + for arg in additional_args: + if arg == "--use-staging" or arg.startswith("--use-version"): + continue + _additional_args.append(arg) + additional_args = _additional_args + args.extend(additional_args) run_detached_process(args, env=envs) From d2127b510c83866a78b8c42f889ad5ebeb7ea042 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 16:49:30 +0200 Subject: [PATCH 17/75] skip '--use-staging' argument as env is already set --- openpype/tools/tray/pype_tray.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 3a457d2951..4a7d3e84b0 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -584,9 +584,6 @@ class TrayManager: # Pop OPENPYPE_VERSION if reset_version: - # Add staging flag if was running from staging - if is_running_staging(): - args.append("--use-staging") cleanup_additional_args = True envs.pop("OPENPYPE_VERSION", None) From ec0855ce7466bbcb3f66605d848f512a2b847fe4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 18:54:13 +0200 Subject: [PATCH 18/75] one more fix of list comprehention --- igniter/bootstrap_repos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 2854a047ae..49de83cebd 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -1026,7 +1026,7 @@ class BootstrapRepos: installed_version = OpenPypeVersion.get_installed_version() local_versions = OpenPypeVersion.get_local_versions() remote_versions = OpenPypeVersion.get_remote_versions() - all_versions = local_versions + remote_versions + installed_version + all_versions = local_versions + remote_versions + [installed_version] if not all_versions: return None From 0f8df529d0b7e51a28fccdf2afb1335105a0b46c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 18:54:19 +0200 Subject: [PATCH 19/75] removed unused import --- openpype/tools/tray/pype_tray.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 4a7d3e84b0..ada2e42121 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -21,7 +21,6 @@ from openpype.lib.openpype_version import ( is_current_version_studio_latest, is_current_version_higher_than_expected, is_running_from_build, - is_running_staging, get_openpype_version, ) from openpype.modules import TrayModulesManager From 46d06b01b0e9f7c4781381a880f42271000c7063 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 26 Oct 2022 10:45:32 +0200 Subject: [PATCH 20/75] fix args in get expected version --- openpype/lib/openpype_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/openpype_version.py b/openpype/lib/openpype_version.py index 586fc56eb7..23a4cd3443 100644 --- a/openpype/lib/openpype_version.py +++ b/openpype/lib/openpype_version.py @@ -207,7 +207,7 @@ def get_expected_version(staging=None): if expected_version is None: # Look for latest if expected version is not set in settings expected_version = get_latest_version( - staging=staging, + local=False, remote=True ) return expected_version From 12522aa5902e0dfeb459ba4fea0a872a868e72bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 26 Oct 2022 11:00:57 +0200 Subject: [PATCH 21/75] fix how the expected studio version is checked --- openpype/lib/openpype_version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/lib/openpype_version.py b/openpype/lib/openpype_version.py index 23a4cd3443..e052002468 100644 --- a/openpype/lib/openpype_version.py +++ b/openpype/lib/openpype_version.py @@ -195,9 +195,9 @@ def get_latest_version(local=None, remote=None): def get_expected_studio_version(staging=None): """Expected production or staging version in studio.""" - if staging is None: - staging = is_running_staging() if op_version_control_available(): + if staging is None: + staging = is_staging_enabled() return get_OpenPypeVersion().get_expected_studio_version(staging) return None From ff3325ddc67cfb981ccf7f9e9383415733ede6cd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 26 Oct 2022 12:16:10 +0200 Subject: [PATCH 22/75] added option to receive staging and production icon explicitly --- openpype/resources/__init__.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/openpype/resources/__init__.py b/openpype/resources/__init__.py index 49eee21002..0d7778e546 100644 --- a/openpype/resources/__init__.py +++ b/openpype/resources/__init__.py @@ -39,15 +39,21 @@ def get_liberation_font_path(bold=False, italic=False): return font_path +def get_openpype_production_icon_filepath(): + return get_resource("icons", "openpype_icon.png") + + +def get_openpype_staging_icon_filepath(): + return get_resource("icons", "openpype_icon_staging.png") + + def get_openpype_icon_filepath(staging=None): if staging is None: staging = is_running_staging() if staging: - icon_file_name = "openpype_icon_staging.png" - else: - icon_file_name = "openpype_icon.png" - return get_resource("icons", icon_file_name) + return get_openpype_staging_icon_filepath() + return get_openpype_production_icon_filepath() def get_openpype_splash_filepath(staging=None): From c59d5f814c390d44a9553971e412102077a5805a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 26 Oct 2022 12:16:30 +0200 Subject: [PATCH 23/75] show a dialog in tray when staging and production versions are same --- openpype/tools/tray/pype_tray.py | 68 ++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index ada2e42121..01749aba61 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -22,6 +22,8 @@ from openpype.lib.openpype_version import ( is_current_version_higher_than_expected, is_running_from_build, get_openpype_version, + is_running_staging, + is_staging_enabled, ) from openpype.modules import TrayModulesManager from openpype.settings import ( @@ -201,6 +203,68 @@ class VersionUpdateDialog(QtWidgets.QDialog): self.accept() +class ProductionStagingDialog(QtWidgets.QDialog): + """Tell user that he has enabled staging but is in production version. + + This is showed only when staging is enabled with '--use-staging' and it's + version is the same as production's version. + """ + + def __init__(self, parent=None): + super(ProductionStagingDialog, self).__init__(parent) + + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) + self.setWindowIcon(icon) + self.setWindowTitle("Production and Staging versions are the same") + self.setWindowFlags( + self.windowFlags() + | QtCore.Qt.WindowStaysOnTopHint + ) + + top_widget = QtWidgets.QWidget(self) + + staging_pixmap = QtGui.QPixmap( + resources.get_openpype_staging_icon_filepath() + ) + staging_icon_label = PixmapLabel(staging_pixmap, top_widget) + message = ( + "Because production and staging versions are the same" + " your changes and work will affect both." + ) + content_label = QtWidgets.QLabel(message, self) + content_label.setWordWrap(True) + + top_layout = QtWidgets.QHBoxLayout(top_widget) + top_layout.setContentsMargins(0, 0, 0, 0) + top_layout.setSpacing(10) + top_layout.addWidget( + staging_icon_label, 0, + QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter + ) + top_layout.addWidget(content_label, 1) + + footer_widget = QtWidgets.QWidget(self) + ok_btn = QtWidgets.QPushButton("I understand", footer_widget) + + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + footer_layout.setContentsMargins(0, 0, 0, 0) + footer_layout.addStretch(1) + footer_layout.addWidget(ok_btn) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(top_widget, 0) + main_layout.addStretch(1) + main_layout.addWidget(footer_widget, 0) + + self.setStyleSheet(style.load_stylesheet()) + self.resize(400, 140) + + ok_btn.clicked.connect(self._on_ok_clicked) + + def _on_ok_clicked(self): + self.close() + + class BuildVersionDialog(QtWidgets.QDialog): """Build/Installation version is too low for current OpenPype version. @@ -461,6 +525,10 @@ class TrayManager: dialog = BuildVersionDialog() dialog.exec_() + elif is_staging_enabled() and not is_running_staging(): + dialog = ProductionStagingDialog() + dialog.exec_() + def _validate_settings_defaults(self): valid = True try: From 96719a39b4719dd6d23b638547825bca88fadc7a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 26 Oct 2022 12:21:37 +0200 Subject: [PATCH 24/75] set 'OPENPYPE_IS_STAGING' on app launch --- openpype/lib/applications.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 990dc7495a..317a17796e 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1368,6 +1368,7 @@ def get_app_environments_for_context( from openpype.modules import ModulesManager from openpype.pipeline import AvalonMongoDB, Anatomy + from openpype.lib.openpype_version import is_running_staging # Avalon database connection dbcon = AvalonMongoDB() @@ -1404,6 +1405,8 @@ def get_app_environments_for_context( "env": env }) data["env"].update(anatomy.root_environments()) + if is_running_staging(): + data["env"]["OPENPYPE_IS_STAGING"] = "1" prepare_app_environments(data, env_group, modules_manager) prepare_context_environments(data, env_group, modules_manager) From cc7a3e8581293e7fa2c3a678a43ca9c579c40e0e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 9 Nov 2022 18:41:48 +0800 Subject: [PATCH 25/75] adding the switching on off for multipart and force muiltilayer options --- openpype/hosts/maya/api/lib_rendersettings.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 2b996702c3..2fc7547c8c 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -154,6 +154,16 @@ class RenderSettings(object): self._set_global_output_settings() cmds.setAttr("redshiftOptions.imageFormat", img_ext) + if redshift_render_presets["multilayer_exr"]: + cmds.setAttr("redshiftOptions.exrMultipart", 1) + else: + cmds.setAttr("redshiftOptions.exrMultipart", 0) + + if redshift_render_presets["force_combine"]: + cmds.setAttr("redshiftOptions.exrForceMultilayer", 1) + else: + cmds.setAttr("redshiftOptions.exrForceMultilayer", 0) + cmds.setAttr("defaultResolution.width", width) cmds.setAttr("defaultResolution.height", height) self._additional_attribs_setter(additional_options) From 9996c3f1afbe2e2b3adb110382586ffefd82a3ae Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 13:44:20 +0800 Subject: [PATCH 26/75] AOV Filtering --- openpype/hosts/maya/api/lib_renderproducts.py | 3 ++- openpype/hosts/maya/api/lib_rendersettings.py | 10 ---------- .../deadline/plugins/publish/submit_publish_job.py | 10 ++++++---- vendor/configs/OpenColorIO-Configs | 1 + 4 files changed, 9 insertions(+), 15 deletions(-) create mode 160000 vendor/configs/OpenColorIO-Configs diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index cd204445b7..ef75391638 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -1016,7 +1016,8 @@ class RenderProductsRedshift(ARenderProducts): # due to some AOVs still being written into separate files, # like Cryptomatte. # AOVs are merged in multi-channel file - multipart = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) + multipart = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) or \ + bool(self._get_attr("redshiftOptions.exrMultipart")) # Get Redshift Extension from image format image_format = self._get_attr("redshiftOptions.imageFormat") # integer diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 2fc7547c8c..2b996702c3 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -154,16 +154,6 @@ class RenderSettings(object): self._set_global_output_settings() cmds.setAttr("redshiftOptions.imageFormat", img_ext) - if redshift_render_presets["multilayer_exr"]: - cmds.setAttr("redshiftOptions.exrMultipart", 1) - else: - cmds.setAttr("redshiftOptions.exrMultipart", 0) - - if redshift_render_presets["force_combine"]: - cmds.setAttr("redshiftOptions.exrForceMultilayer", 1) - else: - cmds.setAttr("redshiftOptions.exrForceMultilayer", 0) - cmds.setAttr("defaultResolution.width", width) cmds.setAttr("defaultResolution.height", height) self._additional_attribs_setter(additional_options) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 35f2532c16..615be78794 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -494,12 +494,14 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): else: render_file_name = os.path.basename(col) aov_patterns = self.aov_filter - preview = match_aov_pattern(app, aov_patterns, render_file_name) - + self.log.info("aov_pattern:{}".format(aov_patterns)) # toggle preview on if multipart is on - if instance_data.get("multipartExr"): + preview = match_aov_pattern(app, aov_patterns, render_file_name) + #if instance_data.get("multipartExr"): + if "Cryptomatte" in render_file_name: # for redshift preview = True + self.log.info("preview:{}".format(preview)) new_instance = deepcopy(instance_data) new_instance["subset"] = subset_name new_instance["subsetGroup"] = group_name @@ -542,7 +544,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if new_instance.get("extendFrames", False): self._copy_extend_frames(new_instance, rep) instances.append(new_instance) - + self.log.info("instances:{}".format(instances)) return instances def _get_representations(self, instance, exp_files): diff --git a/vendor/configs/OpenColorIO-Configs b/vendor/configs/OpenColorIO-Configs new file mode 160000 index 0000000000..0bb079c08b --- /dev/null +++ b/vendor/configs/OpenColorIO-Configs @@ -0,0 +1 @@ +Subproject commit 0bb079c08be410030669cbf5f19ff869b88af953 From 7e2ba84911dec742654ab07f28062c0ccbf0a731 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 13:57:15 +0800 Subject: [PATCH 27/75] AOV Filtering --- openpype/hosts/maya/api/lib_renderproducts.py | 3 ++- .../modules/deadline/plugins/publish/submit_publish_job.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index ef75391638..f89441cfc7 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -1016,7 +1016,8 @@ class RenderProductsRedshift(ARenderProducts): # due to some AOVs still being written into separate files, # like Cryptomatte. # AOVs are merged in multi-channel file - multipart = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) or \ + multipart = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) \ + or \ bool(self._get_attr("redshiftOptions.exrMultipart")) # Get Redshift Extension from image format diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 615be78794..18fc769d49 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -497,7 +497,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("aov_pattern:{}".format(aov_patterns)) # toggle preview on if multipart is on preview = match_aov_pattern(app, aov_patterns, render_file_name) - #if instance_data.get("multipartExr"): if "Cryptomatte" in render_file_name: # for redshift preview = True From 252859ce0206a011828a1314e1530dbc12db5ea7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 18:03:46 +0800 Subject: [PATCH 28/75] AOV Filtering --- openpype/hosts/maya/api/lib_renderproducts.py | 6 ++++-- .../modules/deadline/plugins/publish/submit_publish_job.py | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index f89441cfc7..a95c1c4932 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -536,6 +536,7 @@ class RenderProductsArnold(ARenderProducts): products = [] aov_name = self._get_attr(aov, "name") + multipart = bool(self._get_attr("defaultArnoldDriver.multipart")) ai_drivers = cmds.listConnections("{}.outputs".format(aov), source=True, destination=False, @@ -589,6 +590,7 @@ class RenderProductsArnold(ARenderProducts): ext=ext, aov=aov_name, driver=ai_driver, + multipart=multipart, camera=camera) products.append(product) @@ -1016,9 +1018,9 @@ class RenderProductsRedshift(ARenderProducts): # due to some AOVs still being written into separate files, # like Cryptomatte. # AOVs are merged in multi-channel file + multipart = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) \ - or \ - bool(self._get_attr("redshiftOptions.exrMultipart")) + or bool(self._get_attr("redshiftOptions.exrMultipart")) # Get Redshift Extension from image format image_format = self._get_attr("redshiftOptions.imageFormat") # integer diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 18fc769d49..27400bb269 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -494,13 +494,13 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): else: render_file_name = os.path.basename(col) aov_patterns = self.aov_filter - self.log.info("aov_pattern:{}".format(aov_patterns)) + # toggle preview on if multipart is on preview = match_aov_pattern(app, aov_patterns, render_file_name) - if "Cryptomatte" in render_file_name: # for redshift + + if instance_data.get("multipartExr"): preview = True - self.log.info("preview:{}".format(preview)) new_instance = deepcopy(instance_data) new_instance["subset"] = subset_name new_instance["subsetGroup"] = group_name From 4d3961e04446bb3515c2f56b0cdd11731825524a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Nov 2022 11:43:17 +0100 Subject: [PATCH 29/75] changed label of standalone publisher --- openpype/hosts/standalonepublisher/addon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/addon.py b/openpype/hosts/standalonepublisher/addon.py index 65a4226664..67204b581b 100644 --- a/openpype/hosts/standalonepublisher/addon.py +++ b/openpype/hosts/standalonepublisher/addon.py @@ -10,7 +10,7 @@ STANDALONEPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) class StandAlonePublishAddon(OpenPypeModule, ITrayAction, IHostAddon): - label = "Publish" + label = "Publisher (legacy)" name = "standalonepublisher" host_name = "standalonepublisher" From e07d26c9392104f3b236435443b6564bfe5adffb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Nov 2022 11:43:27 +0100 Subject: [PATCH 30/75] disable standalone publisher by default --- openpype/settings/defaults/system_settings/modules.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index c84d23d3fc..703e72cb5d 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -195,7 +195,7 @@ "enabled": true }, "standalonepublish_tool": { - "enabled": true + "enabled": false }, "project_manager": { "enabled": true From 7ca8b4ddf476644847f84d4690a69a81dd2bba41 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 10 Nov 2022 17:36:58 +0100 Subject: [PATCH 31/75] OP-4361 - addd support for multiple composition in AE There cannot be multiple Render items for same composition in Render Queue. There cannot be multiple output modules for same Render item. --- openpype/hosts/aftereffects/api/extension.zxp | Bin 100943 -> 101272 bytes .../aftereffects/api/extension/js/main.js | 4 +- .../api/extension/jsx/hostscript.jsx | 96 +++++++++++++----- openpype/hosts/aftereffects/api/ws_stub.py | 10 +- .../plugins/publish/collect_render.py | 15 ++- .../plugins/publish/extract_local_render.py | 12 ++- 6 files changed, 93 insertions(+), 44 deletions(-) diff --git a/openpype/hosts/aftereffects/api/extension.zxp b/openpype/hosts/aftereffects/api/extension.zxp index 217d06098f856d8daa2e50f4dcdb56149fe71b52..e7b3bcac8ae26be2d55a122a77701e43e265fa06 100644 GIT binary patch delta 10790 zcmZX4bwJ+Cv+n!i?ozb47k4S{6e;fRPH_uX++B)Wf#R+$?(XjHu7%rv-#Nc??!8Gi zd3I+qv-?l>$;^x=!lb6a#D+koQrlLCkAor8sBIH;AkzN!Yabv~Q2qf5=ok?H02546 zWJqCK%Y;%AH$1!?YvVua#*)a;MvAhKP*@-k2oCgD%1i1G`&atcNEhiGV{*jb3KH(WJtv@2 z;wNZ)b^OQ2$k%WG7_ttop#BFCRx~;N1&#A0U_YFHkQNu%8u}kJzz^m}`Uk;Eg9#8& z5`(N%rLDH!AH#t_Fl;Hz%3ymS6yNqgi#*k~wqI_*e?8L|w&Q-`^aLIM`e>7dEDv zB$DqXjklD5p~+2C1nxdkxCJc{^qB`OW+ciD`oyo_8sT720jQIxknG*}6rWHhuOaKW zPLS`567`M4JBV_4X11=udHlav_iX&+`|VRHEQ25?xb@(q&2Tl=kYd|N*{)F7_$g^E z4sU-+V}<5%@=8RGlwIk)eGl06Y7fq_vx<=@{PvppQ4{5E^A8=@~P z98^Lg>9P4d3?xh|X)X|;OeRXPQ_ly$HeP&+y@@WMk);j){82P$wU^c$4INuJJ&qU$ z5hsVn*x%|K%QNYvP~Kay>j!YNhF=`JF?lSW^&3AdQq)4`KCgxT@w#O6@;nMX?d(+| z4GVvN^+-J->F+8V1{EGL8A$KyJbhMoUl!ihhw)yEJV4aQMk|4|d8}wyBE{y2x3Bay zJ)$4|D)PZ?Jq@;F$2hIe=ID0vlEo>z*+XhZRZ{%&(Bd!+y4d?J=M7S7f_2s?j5{AO zh}w*YBbxTIj)d94o%fZUSZ!I=G$g8z zWhiG~l7&3Cca06lTMEMOmWB7bP(>fPh;$dFLm&?!@m^@3OtM;~@1LLmSS3ZNZ#`a+MN(HGbZ>iM?YvjBz5( zV4w|ZgXVE{q{J8;oq~{fB{w^-PAM(%PDd#TEak?C z5+cgcGD*O=vpEjSq=&R`bg*{->G1t529OV))d@`|R{x;F_THqo&HeyMbDr0)`hFt^ zC|IJ6PgTWVtex`J+gn^wG`oF)htKWK{|pM*q?~Wc`p&=?PWGL`I|~iQpDy66#7@mr zId_35gA}Vvl#~aSUCp>Sn%*f9e;*CY@-m*3Zb%G@n{@N*U>v!Rx#y*I)+^_00r2U` zBd<1qM>kob1Mc$*PbzDmt?Fe5@fU0U8$+Bf{^c6}s2lZXX*}N9a3d_WB=r)z-&5nq zG4HRAP;n9@yDDH3@!~W%+5<1tXUhW(eb|MMbQyPS-?xP~toEV>dbyckT#+a%H^XV zS7v>-xtlKM)ks@Etd?Tq^E@M1y9TmCGr}#ic}#p zjq8-Xgu^OrC(MIli*pyOZo>_2rLW6UJUCD)Hyb=_$0_VI+8WmG*NoLWA0!VU@391i zk;Tur#)n#`YgVRg_;~6)O2M9u8Yn;EwEhWokygyAg>V1x6sowOtQ{y7(J$-mlZ@pB z=TZ;xVI#JZ11WLX`nZ~x?k21L%jtJoUws2xq5_y@ZzJ|>oyAJhef)^8+$iBY$f;Y7AAd+2c*`tt9q!Ba?`a3@g*|20Ym86XS*&>kZaTS3dyVY$J>Ny9pkkJ?Xj45Y zUI>iVT{~)>41nu6lzH#DT2CbB!tvc#d+v-Sr>SHLg=J3OAosLma{)b1|1Fz&pYdN@pP2W+(;!U zvNNPym|{k)N4Nyx{B@7Tq_eCnuV++tC9h{tmQb6Os>-1wj#X)28a%p@44Fh$8f|^O zh=!Wo4m?~ILjCBgpBHq>GE>Cr3y1%)3IY*LP2hI+O-WsHD^zXp3q$(j0;X>FmI$Oj<~1(%jLWa`zKM?30WC#C~q> z7b?Xt6@q*VH$#-m`IlEfNFm_6%5m*->Ci!2a zO6S~ZnLGZ?yCmJs&qbS@ZgOI9WmPY;J?%rxwn;k|rWg76h;HF$y6_+hDy%_Dn&tF? z^)sTg-)GVlt>9P?+;7KkgDFI+Jm}&%lLvOrZxa?*7nfdfg&w07`7(snEm~Kk^tFY* z1bs3utg{Xy+B7Zz9eow=K6_c0+u3|}1$Vvo)c{eAxbtw?QRwew<-3QAGRcq_b4`{o z*1Hf=FHIBp%8PkG{+e5x>f3GC#YH>!$W)l$TLiy1lV`uaK$q_c#jr;6HxtVddB*WY z=d^wotsS|M2-9Q`;%&{)qP`? z4)$bQJ?5P^=Zg8bb_HbeoOfqB;{$k+%?bfI8%4A4T2mn8{FWa(w~g0MTOk7BJ7F0y z@OgChHh~)8JiJSg&&B=hEsMNfy+58!ujf6wD+!&d7zMOc{zeTcYRT8Kcr^4FHBEZZ z&C6TkbqgyaDC2Ua&(8l1VIp13#{4sRO4+YhkkYhMyfAHL)o*#*R)n zv0ZXP@MRzZ{cFGf*;~IeVxhKtDPDK%&#E?eCo+{?8yc-?V&P6@Eq$OFHOG6y(}uBM zM-H`KzL*l%^bbK!)P1zESHZ;ws&@UK)57wBxtz^>T9`Z-VzWT<;I*V)ag?&cxlcgj z!nk#{iM>&6J951qyYMcikGIt|HX)&Q^6L;}cOMdcMiJgm8V?jRwQ(QLU@mCH84%>r zuN>p|d^beOsmU_IOu0!E9&OVvac>ZssmX!<{lBw*HCn&s{rL`)NVWM&uDBZ+v4pq+ zO(3$PQH{&Np|1oVIxh$4nQNBIr~RvHi1^LH$T2=H=s~~v^(ACrQxD) zJdCOhU6j8#^Oa_27JWvrTTZ!h!^)(`&gdlxl;RgEiLF9Y;v^?*7>i)fKWE5Ba*GCfiUi+-h5~Z1svCr`YHDy!^u3 z&Nc89{ltQ8^09f0Ma{7kDuH4ZpT_z#4%f$W5`b z?T)`osu&yVeb$8ixaUq8!An>(FF&I+8Irq2}>)Pi7WG|8I!^Viz zGkx9R7=a$QcJdagA6#{`+r=qVE#m19XXM?b z&@6y?ps6!+?uvimH1=R_gt9?~hKfsIV@N!{W5HQeM7+&HBgR7LY)7I^s>k zLHX5|nV&2_$TkxJb^NfqzlsYdPBCN!!6dA zB#-wwycjBhECL#+0)+rlQ~-;b+ylcXFqE*Rs>(2&L!BfK*S-j3*sV3APcVr_r(s%y z{RtN&%`#kCgygYkdB(o(Ai25q+Zne3WEDFt_2yS<)wK5aYns~NQT273X-VKDou7{% zYEnjhMc02v4+(?!8vjOt$+ztUb;0eV|j|f z>}9S6exJe@w)i*4$c`&)Vna=(K`}n$&Rhtt8&OQL^QF}8>w+lm^03Fas3lx( zbHTQvmjl2u&Y!^{hb4#R9XjLYayb|27^Qc}Gr~Ji@&T9bQ_X!c^7k{s0uO=M0VL*t zT1nLz=ltSz{^xvN)%;IPj_u%EFAUW6$}#KE%K8_q8TFT3X9v_)E! zi?BdYOnD&C72f~WhO*)UO_M?OCXxj@ag9Ly#V3-E7qzDT`)McNWqnV~a=6Pa(>VYO zDuzz+lNHs)J8SnmRXzutea2RG$kUslA(I;o{TXU%zYr@~^tJ9B+!>^mlTvJN^_dfn zhtTl+!2C$-xIm@3c$S;X3utTeom5+<0xN)y({UVHFeQh+gQjA6_rjEdfBTk6{C2Zo zOKBUvai@FQTT2DeV-NFFxap`}4p$XswLOiO#yI)Ihf!8eOH;UG_$o#Pky7pOd>Q)1G zo!>~nm`&#ai{y$A=sDXDo?J2;`Mjl8nPRe^!FE34Kihx6eYHEdGt!K^NY6T2lJ2@K zlG;V~Ls_kQ+&mjzNIrWY{_qoW-P+{3stn_F`d0O_-lrMBqImy|)0V;?cIfFjW!Av?R50XyCvx@Sz^YK# zC`<|*5oBCeN|1o>$=RRdJLtJx09ECJC2fM%>f{qxpl;iWj0f*t=h$3JihstXU+o5rMf1TX zdtxoRpP-jXQ&EDZ2hpbEjHU$vl|vZUi*Gz;roQ~<-()_!(to*Aa46Eghe4sw3H799 z#=hILr}0DA4Pj!`%YJkhktX@5V#G{KIm{|+Xq>Aqpd22uCn^NNbGE$YkvPt+90I^w^Loxe)nqoc#F+2UuKE6Ig2`x^@TS8t_mXA1#oLX-{_a7p79_Csv( zQ(afDhBhLxX8gP^!Iczpo<{QzfFYieJI}lWd1`NT=?u<*m2(CG3^F_Wrv2U^W6JGh z(GDPiRbGO2ZlrOr;q`#or&y)VFBF!ozQw6)nbaqA<`+uAt{N+Tz9OTpK+(S6_RFMD zQ-vv=ICynya^h|qa*~~Wp@EidPAWYxM*f1gaPTT zk;m~(mo-CmUkYs;T5H$icG%5k$z|w@OH1$<>f92jZrj{?cspY3kW{Cdxb_ci^q!Xn z`K(aZ@`g;~aRgNthx587>THeK@-jKLU__^B#^Edn@axaAwjjg-y6+M(nJ;pxwiZS& z4FyJ7obT@w6hlT!K`8B*j*XI32I>eCi<=rgw4@})EJs*!>SO)-J1=<54%7ul*d(&A zoG^+QRSi6v=oYxo2Vhgt?eF4)t6mKS%OTfNu}xf=sbNN%y-R1NxQ_`Q_(lwnf=M5G zGfqKv5B)Ki_2nACH9>LLiYX?g3Y8m~RH^#cW`|&2dX7YpolRD+M!lpo83~k#n6%oF zGyKUqO{IZwLXJ{BE}Cn`+2{Uxfv-80O$Vs1DIuQoNH#xz(YmKZXV|gP3yO%66l;)NAA#35mdT5o1i|!aibX86p(bgmZ<^rYDpNuzdJH?xwI#*{P1Dwa@XA3Uf=hxI;46-6nxY zp5Rz2BC1d7%3&<0(6DM=VUIJld$d%}JRq-U}|0TLdoKBx%(MI<|uXgT?Wnp@S*BBTf8X07KNUMC^?(ZUgTVS zM2gJ}MfWVt4Zqg9EHnkR8rHa}V%g+kUgpuJ6PMc4C9OMN!?x?LNh@ESZtQk*?5}_y$277?I3egAVA2Kay&1?O0ARka&$C5cg`prxb4AGE$p1%wFntq2w#wQOk& zT`iJK5Rg%~DXoV%S-C*b-|slrEQ6i+`?JaXy&NcUye|WS_-);(pvbqvolM;kX54^H z-Jc#ZWJ6P)w|sezC!u}Mky5ZNIp+_4i*z1Fu$W5DZPGDb25Fhx!4Rg@76g*4?uRF3z35?JXP-Uj|K5nHK&_NnZub~d6I zgZE8Q>{Yf3@sB;UNhHcr6*Vn+9NP91cv+GgM&~rgjiRof_S6K(x$JxwAU8E%N}+RF zy@skVy{jYCV_WuCk8E#yZ=i18qESF_t^-Fyb}BZXBM++iw*AT0AV|4ZL*E0n_$pjy zVtlb(v9=}&8mA1(pGNerLcR#^+wWz5a>|BiuOHy-vgcWEC*}6#Wo{epqoR;s75$3P zX5sCYH^ZbAb>V%z4s*TImkoDA`0@N3hd}aoGaUoJKBk>b2xPq3EjhhD`${ zGa@v}n6$+khgFX(5n?_S{FJuC=phi_Qbh`rRW~= z6{PP?(WY01el-p|HlASDAViw$MYg&=Adq<@rh=7CDb!4BF)rlIqN$dEgx`B7?}G7b z;1?o?TQSllKD@o-hLT=1RPUFE3;{tM@H5ZJ45NxgI-U_K(Ww|JE|5s(yEfX|1QpBg ztwvd85QfQ@H2Oj`veX#~obxn=91Haa&DV81%gg*e>-Kh9aUox>PCQhut8Y}OWWf#| zWh2uR5gCJYGeAH*I#Is{LQ8h*k(^mi2p5;fbBJ$ex)a-5`_$8BMyY|@akJL)epy@J z3T3IB_>3{q$XkoAKJQG^yAHo~o5fj0n-b$FV9^uf*7E;O3LkP-{g$6eR0%fF2R>}S zkjY%56K!gJh~s&rXXl6eR%A#g;gW6|7(Bv{Ay^FG7@fPDRmO=3EFod+UDndn)}cHZ z0%*zNhk#>j9u&oqd$c?l$@Nck2d14wWBA?V!ApCXw~@IBksr816#LUI;LM>xBi*ih z){4z@S6!+Lfv8IW)z?qS&z&~j1-_@u0s-JUw&Ls*?5L=Ld);_*YQd>>{@`bjS#a!$ zQ*OOc(6?EcVBz!O==Sud+zX4A3wJIN>T1B? zhjG~#s{nimZ&vv!8BvI|@%olDCLwF>GW)yZY+^mPhuCyL)TgK*L1vsUiDL}CDy_Kt zbh@vlhe`S4!E%8I9YiDNGlPq-o$S=h)}o!MP08`J^GKDy9hDq+(KI9>iOpJjAIzw- z!SA={?6*@?&7Xofa>!Z)y^Ic&0ft*2qga1b{m5&QASiCU|Gh}Oz(q8(TwA^266NcR zu1Rsmryp(RcT;XDY zzw5uKE~tCpB{ptFT{}Ree54Vo7S^m^1vl6mQ~AJiQYPR;t-QgPv{IFYa;Kx9H$3Y* zkruaK_Vqbkl3#KdgRNr=dO;a{H1lP_1Y@W>ee^;VXh2EYJCs}&D1(I*bPb7hlfTse zz48{}W3_j<|1+{Qqr#m1a{lzhn$y^snpgp2s>7t8$h#XgiiJ1PL)1#R;_`GoM8CkfCM28tijd ze^D;ZTgeZ37GddT_z5VuiCz~*cSEq-LLXP&yXi+Z&J}+g4}W?jwMyRx& z%FsHMu*4@QSy3+f2eaRQgnzy625{VI;WqCdQSHlw^=SgG3VZ$X7>rO&EU= zg>PX)T{YnuG;gzJMXxpQgAWL=x$Pbf3{5T{rUEe*ZorRzFMP+pk@RObs-r=7wzNbr zRRLdN1=CW)B%_M!|f2?xkUX*yO`Y%cjA^=og{HmlgSX44br)CVh=I5@dr0 z-9B??yVRg$;^>u;pKFi%n1!sVH1~5bR=b9-GNgZ|+&~m`J3H=;J2XikMG>Ef&MTeHw6Ca`_@_0hdkhiASghrd3&X5SUgKEHGhcry)tTq} zVRp;fqKQEp_qb11bO0)C(aCVN!kKBpvC5s7esTNq?Q3-O&b&{WKgQ2ff@oB1l4Fs6 zrOG;j9j=pdVBcucczc8XXKQkxNn8o+DeHs)cCVsLlWUp30>yA>id5e+F^}; z51^=-v>i@`e&+FDt>zZ2E6gteS;6sHgkmz}bD-_DouYSY@dGuOwA!UIriT%0*vHR& z2?$>bvDjG;#Tn9+kP!|T(-|Q+n5v~4FRO>dd&g30OKNH;DQjajrfEf;POY@8GM-zV z`Xyo2H7RTMZqw92tz4X9RJ30Lve_ao-MPbA0mqvB8bxEqd$%EH*cZ}-&$hCO%PRWL z&OJ;u&4-^{`vH~JeSc383Y2oCu*=_p#@~{)jPE}QQ>9f`?%lghmK^;Eq$7V)Oiwdn zJ3JaNZ<1Kno?3c0S0Kikoh_%O*Pk?e#jNVZXr~zj0Z~9mMxemPtniGseYICW*Nje4 zPv*Gl@u${-#C4zaAT&*sX!D1cuP^*fw$D@f`RIO}TQ!i=0)La9(%Q)W^by;8AXkv} z+B$1=n~>8u-1p&$sh}NIdR%nEX||WDviYGaL%?!{urQ$rpK3damlQ zYODu)8YP@?8?d}1OQ?w)$H}rjRe1HVGo&ESpNwlis6G$k_n>r9CT_=IkNNmx`to%W zR`n^dzt`3|D7URi=V->tk+7h{@=+qm%bg&s%Md8OZCw?%YV1jj6gxv8iZmpUdUazE zi~_xV1uv!J^=Ju0yU|5$D=wV%Zvn`OX3~jEYlfn^sHh2epSd|k z+KcXm=o%4Dx%ZYh#L#WoYC3{DE)3tWIDp0h4y86xBA!~5AJ}Z)$z99B_Y5p6a7Baq z*K+IQa&)KS@LzO5zcC#@lWU&L!5PDC!^&fWP(M;~NnOg;v3!Zyx+nT(cI=*!Sum9L z;h42pJ~8vr%B=YGCaCh}3iTjC8L@1b{59y~W@TlGwiGi_1k=1LZq~sEiE^p#!+8Lr z9naO5{F)Y@1s}=Sr#Ujin((zXsHF{=^KielY8I|@-FS$ZCG%!uGQyVtt$0G&Kc6vx z{otW_;1%9gEf|l#`@~F5JC9e+p&_~tYr*!pW$usRm{!v;C zY&RPCywAxOk7kAE@o0K=vJJWFBHU~PdkoK(()!A-ktaM&=$Dl6Ek?RMJbIjs%1&qLGuy%pQ2GNL7{Fy2v7%aR5I#tx0az=w#SegTK_j?) zMyPYs@`aOLp1xX>$HbS&BSy@0bw&^SeFl2lPvMU$a}`eUtVV0iW>lq*A5GyGD(`}a z^v$yP)8a)9NZbQ0-^8NZl9nr>A7kiV`hS+q&$SA61K|IQ5~#wzwtb}iLl6EoIFL2S z*3#D0*(0Uh4{V6|ck}i{i|#D70mh1Q1MrA1jPSa`zI6%ric1l`)@AMl`mUjl*9{tTVDr}%||-{Jkskp5?p|HF~0|76)A13bVfI__Y6;{P?fzntm+ nag+iM2NOVS`KJ(vgK_cywFwRL*E7+dbMH?X?Pm5=bm%k=+R@% z?p3R1^`Gw9wPso>)MOG=Tqs1lfMHFmA&nGdAb=QPU|=v{f3rNq{?PxMt?MCPfcRURm`R@f&m6oI z%4q+9E9n{3)Yf1CQlgWz_y69}{GZfUU7G&^p7xWd5DmeGi8B6P(Byx}{;q4x-wFce zU(AWfEN{^G#Bp&~V>8n~)OUZI6h{+1HBHDHvstXP6&OuBJmQ zw}9v*^3+B(=UOJ38v)AQIoP-%MkX?fp&=2vIP{UrYl!sNE)g(hH#pA24Rt6Jkih8? zvI)tSW`v~U#ixK4oR1S7gW7*f)4nKhLhRucB>U#sRdN^>M_D2&45h;}wf@4@(Z+SJ zAFb#2go)D!Kz*XJ_W^-Cop@;QynBf=jKU9|w5qSLI$*~xnIv#2sSa#HnA&By3Jg$9 zg%Y;)h_Kj19mPf1xk(>HP5@#b@)cTeF5zIryalAWxYlpH(7@=UtV13P{o756nAJr% zS{ioe7)(tjWVCc=ZsC@??{o;-lkR*OBXvAhg!rn*Oe)ehyCuiyhZpwUxY?B2uyg%MwL416eB2U3B z2H)2WGpn}XQ+7zHE@05kB7LZVTiogCWl-~oZ$0shdp{4_7~=qzv!MJ0oWG&G<|bAU`6A;7AG#O+`@!a7Bze4998XouAO9cJ=InJEoLe|1JRzbAhSO z8Gy}XMfPqhm~uMx2JyZ=Z}mrR>maP-`+_0O--D*Rt3Sa{E4ix*v)>4*j4g)5#JN^N z47Ec;zmc-b$5Yg^3}^p%xaF7}2w^Eb$R>bcm+OES;W>PV9 zk*BXK<;09yuaU|#_zd37O%^_qwNg2xX!Bjfpm10QSy63(1VIU3Hw}k-#@-CG)9khB zni51mGYOGJKDKU3ecO++Ga-~D)u=k~!gILrIu)u5bAL8=!!)C>OZnq?jxK_(9IC3N zWnSiSv6b*<8yPE6VyqHv1YuNAwjJVDeYqmgu$|riL|1Ol*1ip{X?>Xq?Ed14%*e2) z(Fy(2*ww>>ihoH_%Bc<7!pI6`Mt9lwcLdN-Yt4(+_`5q%A1*OooX003IDhTj>rWoM z#3aEHt%2nJ1jR>G%@%qUSTI>cn1S-dwrno9NHmdaIvpef$pK`6|ftJjREkeT>CZ zXy`;YH=b>dHGlka+ttD^CWEPYI6(3#8?l3)NRCv_8!Y=Gj&__jz1I3&^-{EA&AGtZ z?h+%}@?wNG>0zB)th>U~j9FBd=w%PIr6J}XC<}a6a|@9T)^@f*m=?EbY_g;_Fo(t3 zi9MtEMI)yl=6j+w1pEbQC}dJl&DoaUOWpi1kLj9WqDEetqSpEgILv)H@BsdxMAkfD zg2vM00gXfFXmhbB_@p+SMN@lkcxcU$_w0w`0iOC|r#2)z?1z(!` zRczc+4Gl;M3?<{)%n0_=;08fzng>d=?SR^t$^hDkvnx6NNVJp>xRqdbc)#*|G`1w<8^?VNgZDq_Yh1z!me?uF^=XSeWY3FS*M6zz-_Ex~8rf10GYiTkAHpNi z1MjeFi4E+)y~V$Q3d^MHL_HWt{V`2bJ^pxjfX%8)V=Bq1SJhL^h&9bt<8w}fk;ZRy zgs5iMTBljk>tkZczPSl1S97l{uhjWKBrm3KGPhh2d{e`#PcUw!*UEUeJDif;7b%ep z4S!?~3<(Nnh|$cDciuhT)>CwSGmuVHovta>5$#H2R2Wf-?x3M=gxsGPoF2V?KJJvb zq$Fh6JmGPynp?9vHv=b4#uPbL8v$j`Pho#x`k-e>+Uv$!Oe=usmmkNkRQn=A?D-Be zt)(c@;E(SB{$y>stne=?@CA;R!s@_CkfHEap(;Hseb?pup4Ja9WIcUfjGYBhk!l@H zIxkCxrT#oyL3N94!M4^G8m+aqetNC%ZT&U$#t4Voc+|?O6mS+HO$a*F6s2&P<00Kw z%aMd2uj@DY-b2vl$Ls5Itbhsx(~PzXt18ez+blMVF$F!%x=$m!f5hL)guuJU-2jHaE>HysT5leKs zfO#NL%x6O-719XcN~T!U{(lnb-#YMb9=>M~E%A>mz$!4dt9TE%VV`Jaxs(o31W$pr}aay`k1mQ z-(Xyo_xGw2>(r4&)gLdXYokUZ=r#IpxOYL!#i?ibm9fvrV)0^u*mUNYLo9{?vj$(G z5>*|={0x`z#U&_iBbeLTb8*c{NywzIrvrCO^YILH=fv8nHKTUh`L&~l245awe7v#~ zi!0_(rK}q%LVyCf-Xs~qy@SRHH9neETi)%`o^(|)D+`hN;0zKGJCmiqTKw*YOxHnv zGmr|zgH8F{UvBP>mC0p({(7a_hl7&|%6urZx;{sLVfN`1%4%rL&+M$Gqs@;`h&C%P zE$D``PY_crA3k;+zX7V`q}&_KZC9JzTl(Spmn!j$rX3Wte25@SroZv_ZBeSt1})4< zI$mAnCHy8Pu7aeMlWrM?%jn7-zLEg(7%#`sDM4kc(ZQrW@NtdDB{uAs9^GTR2vykQ zD<{vM*@UQEJ1UJWfj(Rqps5k2zL{ZLP9(&e*7KQE{&I5LM->%GlL%#)cPKn7_)7U9 zHrPw-Up354D(*hV->29kat5K1_fAq~=+n+HdHf1dCV_>RQ?-=QkpY&KFSHA4%eNko z;V~EJ^utB`=3p>u^3$BC3p!Nz*y*?RAK~t(#N?-kN$xRmaL2RnTG;wY2C5*QvqW$L zt+_H8A){HueeT-4FpAM~p<2y8W>A*6E2-Wnkxk$146!k4qXQ4NM0ZeT(G(JuL&*>G zrd>)`<*bemxo0wgun`H7aGV*m!#-4vPkY5z2@ME@36*5J(=k+}A>a>H zOW2>*dz5&F$1qfk5%<6HM%ZLnt~1&I3qUzRgf>DC4-Vt5?}ZhFWS0VA{IaYR6EA=P zA7nLZ+Ydr(Gi<$x0T|BWybzwp0E$&rRi?L8^OE3rh;EC(?-b25ue1(Mc03bJo2H<% zu1Tz|#HWY~8C)oNxO|wiV0cy`f+(=0rN8JzJWE1qY4*X##M=Huo2(14PHhh!2rl+@ zH0=Z?^^$&bBZvM!NN__>C@1BN)*3+5Lc>h!c>(XU1n?7tc{xf5?!g zf3nRPefi0wPg@zL2t|7h(?F6G95!?3Fa^cw?|6;tF|z7TGeQJPklh|Y7p&TwtW1m; z`1F<_5v#N*1nGu2)h4^C$q;t&26nl+r_|t?br5}C$!Q0j9M++{FNn&5o2bWl^BZW? zDZ!mzyG?stzj__Ld)3<{awCb_&N##SseAuwoe7LturJ&xLjKXUo-|{+Ih8FH?C?;j zyBcGE{L;_*^`QoIr9B9_Ph@D~?xHL5D8JQFdAp(3^X>FA=b~lGjMeN#74>3S9t&QO zxlz)nc5(0z(F^Nzur;X`l;?m9ooPgF-sfGG&N3n4PwLF$V;q3Paj#nxOnh+^(4 zOMde$BbNQ^nYscI258#%AfGp{pHoM=64*pAkYvne+~<`M&_ zn@1Xa6)TSl5U4!Pjg5uqa}N}8q_5ss+KWg{7&0E6xLQZS>CUtu#Tzh`p3 zY$E#lvqD)NZ}k?RK;(-dyWsb4`h0Jv(bVa)UP!&04LJ%9_dw_5XOvtX7L_d2^<^+p z(`Mx80LO~78quzsBv*xpeu^)$b_j`5oyVox|sFV6RZ})e5J9T|Zyu$e?Xa^Sj7rU9?nw%aUp<=ND zvOnRJCaEeLnsqwGY3C9Wo>$0#s%9&(6F#W8>f8*KZ|?QuQ36EXYy!`fHzHIu67Tt#uPAue(N_^QRTu86 z9rBPq=Sbkt>TjW@)X^+k3^L)iQN_HI%{Ot)5@J18JZM4KyEA}mx=k^6%Gl4 zu$QJW6fo{!Qm@W=(ooFNI(nTU5x`^+5Qi0%E|h_K3^u(Lg?a z;w8z(;Ovn4rYU|14s3+FRxsyMB|72)b?>nY%iLK{Q}+NJgI6nxzLI?hax`_tNrM}g z^~uyk_uJ&nupwUr@HeQuR2PSO6Lcg0zE1t~Pt|1y(fb+bxO6xe!?=~jK*dqP6~U|s zpGF3k%?f|0c5x5S6+*0;^qXs4|B*Kq*%8iQB5U-=^4NXfC}Vxml3B8ifbsJZX!)6P z%;S|2r|V7OTMP3uHY#kFy?YRtPtT9c@Qu(2J1V34B6dpYFBBbwh3I=-xD{P&C0Sjh zeM4ks=R=v_UtaZG>p5v?Q>T63ikP_pyeeNpU_L|fp^p=d*=hB=tIFyLMg7Eq;)-{q zcXAlff(v2(++lDYbyoL7xU$g@9rU4dB}3g}bh=ZUh13}xl3M=$<0n79dQ^sFGbv&A zL1ezxC@UckZAOL;fIUoPxQWDDql#)xrY>qsi=f8`ynHl4V~&2@lEa>*v=4(`N@*|6 z69c;Tuv#4#PfOerjtlp!o&l{(*~nyx`6^IQ!^XkTM1!&ywamVQmW>cgo=?Q~3uozZU)bYNp^5_-f*VmHk@+yP- zm^TI`RF*8_&(G)cf)#Fm-hY0|1+csd3!5Nq_?giw?pNUFs}53kc=8OyN&^?*S0sjs z>@2BQC?{Q2u?~EA1FFW^C<tz3oA87Sk56H zOpH~eb?Jr96HQ_V)ePI#_%sT7cag3^uro>;A*>59B}Q2 zpcrSEce*vmQ=n*57}Q;VX8GicqtLZkCDcFt8p+1S%>H!_E&xLJ?_h4!$U{dqrMii8 zrPEz=47p8@!LxtjxCAqK{b?znF;j^m@C^2 zzwgl#9H(!Pb(YjP^d?r9Vk(qX3btOMQ2oHI$-ajLJVG)Rp(QB0`#dIa`fwfLly}il z&cm2tRr#iuFa*+e8<@+0GGzDy*S3Q6ipb~JCfkl~-*Mx%oWsF}`h#sy$KpP}nsP$p zQfH6_nYH1zLC#k=!2u>1#UdekL@@9}TcVnbC6mjQAO19eIF+MvomfjpnA04 z!lMy(Ou`l1$hB>K!^`dW^W7I4L-t4q#2CreU9#wI{ju&2$7Mk)>(0(byeog2-0QyQ z5msWAlq(1u`V`EQ4Y@5myV9=!V_?ogzY5xUsd5@_h|U6xfvgF8()LiUC4_(KDx?&MvH=G6q~0qD$F%5=FkD}l2)VQ-P4Uh)$UFjnw}zI6~vUqn9-OufcyjblKo8T|Q_#y9IZJ0A+Ix0n?Pp7Bfx)sVKe z8JlK>Crlmo-~CjB(P3Yr7LG-eSz}rz) zV$%WFVG8gK2?oMTpI(Yp%+wHKTRU-pUi3@XTcw1;+zmWM@!n33zh?nXbc5PSyPrWi zC6M|PDRITR*^mL-x6*|QOv=1>+v`&*O0#qooSmc@j4>7gu6qPW0X4f2#^mvuzBpk! z=wH3o0HbI9%tm=&`S$e94q6 zSSASdj&!WX&Ttzg8=B@P6|Mn{REHq;zE>HjfmMer!eg?)dWsW*Dz4+NaqA1RkOcS^ z+~v_!Y>snmGuF|AK7zy^}l~(5qd>p+Zp5M<1 zjD7h`%vqXF_%cd1W4wX@Sqd-owqIlXx77{yY(Fk41>%Rj|fj%%sO(8*NahMwG zs?&uy%tr3NAw=3B#r;}P1>E}6?AA2T1>ucnXp2x9SY_Gk95hj4AOkzI+#8pD;qDe_ zPJ=g{+L}VPo{qb&Dsg-zb>ar$FoDxg@1m9(4WG7y2Rh*oo}&d&k35|=Je<8~6vykk zAU}16!C+fn?R?E~>v$~^yuJV_Fr$!*R(eaMOL-%dmhr79EBG&lWQ^KMmyXpWgHt|D zxJCMJbtCbL1T?5#NY&uRhoGy4=}6M{wWUk(iNy{cn2CG7DT&nrxEdb4!9(QLf4E;K zUZCEK+~jxd?Z8pYJDKX$p+k?|9I1%##|wRRri9p(8+>a~AEK!}*{J{(U)*3SzWyMB zFB(bKNM4ZY`l9WXVu9>!x&pl-q;AXtk#lXTEhuIk7Qh1Zt#lWD)x~BI| zPP%uy*RORBnY6Vgi9i`t-_&H^jM9LxYA#xvBnU)a^QWf!S*L%%&PHjad1SK2UF*K7 z{009fb|n-ih1&Akc@xCFZR`5P?hg)uMQSpIVcAQR0qRaSwEhZGC!_xk-D=s2QhMoDT z)0ftjiCLd7RULtyWwtC;jZz(J6vsB1NsHQQH}qv#+HShG-2#neg)2q3;O}OuE;UOX zjJWUg+d2Dgx>x6*PicELHT2(pey?aE_hCF_f7P`g1$GoWzXsw>!80*>*3$cXieX>5 zH&mpsz8bI66kI`nq(;)~7il9QoRY0C$h1@lC46e_{Cf0|u(*0Ez|gpuslN;Z>SG8t zP3HB08)Yy5{r*{+v0fvPP}0#*R*EbZq3Gg)5D}u=S4>Y`P^T&cyrNF|J*@q$foQgM#4Lfpm$&v34&YZ%A5r$#0vTNtgxbqN0>t` zvALC2@iN=zEMb(A@~O&?{bqUjZhpDn#p(6H#p@4(kdC2>fYKJNCx=DHyDC{A3g;qZl0oXXw>uRQaXgHhkFVj^3XFc_O3 z(0_M1;r?{H!N8zH3{zh`0N#Xud&K`b|Gf$NC#~8glgZnXn(PVi2mNcn-#DKp<(w(^ zIpnhP#$A$IB6+;jmPQV+7L~u62{=C4qm|NBS3hpO%mfAvCapW$!*^qSV( zwL2R5#e3XS+mSv@U)%USpR_8MA!RV54 zCcP^#l@i=nc0bFuu!^OYL$^11L$NVM4}i+QZI#hQ-0k%+B~>s-4S2DZJ%SglreRCx z?Y^e%l~kW8X3S`18(E4ktYl9=l>e{Rs-=p z3ZC%V=g{#0LKm%-$7I5JYEv9*Z2#4&w- zzaS+SQUA!x*`=2L!5V$C>pRis9m8`R$g{Pr#7VedXS1;NB4jIV!@V+3BeUg##F#k)hcDvF)Nny15+=QQ z^eu)x4ws(>e?Vmc)J)WH)L1(^WnyIQFI91;85=aIkP>g9^SanqmJ=RUswvr(f_hA5 zV0kLPr@oZ%I9LvnBvFEokVi~s#(UNqMbU06s!EfNXqooj7ysJH;x%$_5#?icE`dn9 zpPXb2e|P@cdEeg8d&7XOA^-sb`26ywEx45F4*m4IfTeR`nS~yJFLT$s*s1WqkHJTn zRi@g6Fea>F{IHE7qDP0kbi;R~x?_R*Io^S#<2rmgms)ln$>RX~?xKgkWoikSnV!S;q zR`RBk02|~9&6}3Jc1@s77i}YH3N-g%qyYzn!dJ@F9Pw|R$Z|2q&}umRCV2+t<5sA~ zX>qEJq`1$e&J)UCQhwAnAFZ<^Nd~r9#i!DK$)Nl6XNxN5h5C%s2J)_UJn8=o|6Kwl~@{QiuoG-oX_&rI~t%qY^AbgyrV4F8u z(+z-M8E=S7ZsZ`B`5L=qkH;yKzd)B7-A}wBZzj3ln2WH5;_+*Zy8bkSb~BEwo_6Tr z({CDJuQIs3qICkuCA7uK@KW=Ose<~+bI5_9Z6#JJP(IN2kVvtq5S?APifBTsQ1T7S z5W(@a%aVi-Zkgty9Vd#Rz;H+zjDD)AV z5}nW-a%@@Gdg!S~@>0t2w&zpFa7qovtYFYQlm{=i&lGV$zu@~hV<5h@=kHn!J&}*$ z+iFL(^qqkLq*}~Ia2sT_Upl9EYxOqdOPhH}E|x!oMBZMQ!ujMge%eY0Wqz(xZmAb# z4h1Xkl(u-sJ{S|88HO$gl5dKsK>aj5#YT;21IQe&yz};zSqf=@wu9!`;`S?_#%Y=Q`V{Rg; z^&Sq383_FD&`x%j-zqU99836cau>Z5m zrMdPE zIDiG>PscU&Dgb~ddSM6vvvT%eR2Bt-75)(^sw$9R(0_Zc|4IDc{aG+DG%(bE6b%53 z=uh-bb&mj$p#Qt*-*Trs0)Qu7<_-Xp6;lzSmy#3zZ=S!;{+~SmCxM{Bz$mc5z_9;| zN5CT$H4;F9{_m=P6G=w`@L1Eo0l;i6ZB1P~9Zdi0dH$yY66)`w3H}@he@uJI{(wJM F{|{^Kuk`=` diff --git a/openpype/hosts/aftereffects/api/extension/js/main.js b/openpype/hosts/aftereffects/api/extension/js/main.js index 2105ea82dc..bb0f3b1f0c 100644 --- a/openpype/hosts/aftereffects/api/extension/js/main.js +++ b/openpype/hosts/aftereffects/api/extension/js/main.js @@ -237,7 +237,7 @@ function main(websocket_url){ RPC.addRoute('AfterEffects.get_render_info', function (data) { log.warn('Server called client route "get_render_info":', data); - return runEvalScript("getRenderInfo()") + return runEvalScript("getRenderInfo(" + data.comp_id +")") .then(function(result){ log.warn("get_render_info: " + result); return result; @@ -289,7 +289,7 @@ function main(websocket_url){ RPC.addRoute('AfterEffects.render', function (data) { log.warn('Server called client route "render":', data); var escapedPath = EscapeStringForJSX(data.folder_url); - return runEvalScript("render('" + escapedPath +"')") + return runEvalScript("render('" + escapedPath +"', " + data.comp_id + ")") .then(function(result){ log.warn("render: " + result); return result; diff --git a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx index 91df433908..c72b423af8 100644 --- a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx +++ b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx @@ -395,25 +395,52 @@ function saveAs(path){ app.project.save(fp = new File(path)); } -function getRenderInfo(){ +function getRenderInfo(comp_id){ /*** Get info from render queue. Currently pulls only file name to parse extension and if it is sequence in Python **/ - try{ - var render_item = app.project.renderQueue.item(1); - if (render_item.status == RQItemStatus.DONE){ - render_item.duplicate(); // create new, cannot change status if DONE - render_item.remove(); // remove existing to limit duplications - render_item = app.project.renderQueue.item(1); - } + var item = app.project.itemByID(comp_id); + if (!item){ + return _prepareError("Composition with '" + comp_id + "' wasn't found! Recreate publishable instance(s)") + } - render_item.render = true; // always set render queue to render - var item = render_item.outputModule(1); + var comp_name = item.name; + try{ + var comp_id_count = 0; + for (i = 1; i <= app.project.renderQueue.numItems; ++i){ + var render_item = app.project.renderQueue.item(i); + if (render_item.comp.id != comp_id){ + continue; + } + comp_id_count += 1; + + if (render_item.status == RQItemStatus.DONE){ + var new_item = render_item.duplicate(); // create new, cannot change status if DONE + render_item.remove(); // remove existing to limit duplications + render_item = new_item; + } + + render_item.render = true; // always set render queue to render + var item = render_item.outputModule(1); + } } catch (error) { return _prepareError("There is no render queue, create one"); } + + if (comp_id_count > 1){ + return _prepareError("There cannot be more items in Render Queue for '" + comp_name + "'!") + } + + if (comp_id_count == 0){ + return _prepareError("There is no item in Render Queue for '" + comp_name + "'! Add composition to Render Queue.") + } + + if (render_item.numOutputModules !=1){ + return _prepareError("There must be just 1 Output Module in Render Queue for '" + comp_name + "'! Keep only correct one.") + } + var file_url = item.file.toString(); return JSON.stringify({ @@ -689,30 +716,42 @@ function isFileSequence (item){ return false; } -function render(target_folder){ +function render(target_folder, comp_id){ var out_dir = new Folder(target_folder); var out_dir = out_dir.fsName; for (i = 1; i <= app.project.renderQueue.numItems; ++i){ var render_item = app.project.renderQueue.item(i); - var om1 = app.project.renderQueue.item(i).outputModule(1); - var file_name = File.decode( om1.file.name ).replace('℗', ''); // Name contains special character, space? + var composition = render_item.comp; + if (composition.id == comp_id){ + if (render_item.status == RQItemStatus.DONE){ + var new_item = render_item.duplicate(); + render_item.remove(); + render_item = new_item; + } + + render_item.render = true; + + var om1 = app.project.renderQueue.item(i).outputModule(1); + var file_name = File.decode( om1.file.name ).replace('℗', ''); // Name contains special character, space? + + var omItem1_settable_str = app.project.renderQueue.item(i).outputModule(1).getSettings( GetSettingsFormat.STRING_SETTABLE ); + + var targetFolder = new Folder(target_folder); + if (!targetFolder.exists) { + targetFolder.create(); + } + + om1.file = new File(targetFolder.fsName + '/' + file_name); + }else{ + if (render_item.status != RQItemStatus.DONE){ + render_item.render = false; + } + } - var omItem1_settable_str = app.project.renderQueue.item(i).outputModule(1).getSettings( GetSettingsFormat.STRING_SETTABLE ); - - if (render_item.status == RQItemStatus.DONE){ - render_item.duplicate(); - render_item.remove(); - continue; - } - - var targetFolder = new Folder(target_folder); - if (!targetFolder.exists) { - targetFolder.create(); - } - - om1.file = new File(targetFolder.fsName + '/' + file_name); } + app.beginSuppressDialogs(); app.project.renderQueue.render(); + app.endSuppressDialogs(false); } function close(){ @@ -730,3 +769,6 @@ function _prepareSingleValue(value){ function _prepareError(error_msg){ return JSON.stringify({"error": error_msg}) } + +// render("c:/projects/test", 2); +getRenderInfo(1); \ No newline at end of file diff --git a/openpype/hosts/aftereffects/api/ws_stub.py b/openpype/hosts/aftereffects/api/ws_stub.py index 8719a8f46e..32125a7d99 100644 --- a/openpype/hosts/aftereffects/api/ws_stub.py +++ b/openpype/hosts/aftereffects/api/ws_stub.py @@ -418,14 +418,15 @@ class AfterEffectsServerStub(): return self._handle_return(res) - def get_render_info(self): + def get_render_info(self, comp_id): """ Get render queue info for render purposes Returns: (AEItem): with 'file_name' field """ res = self.websocketserver.call(self.client.call - ('AfterEffects.get_render_info')) + ('AfterEffects.get_render_info', + comp_id=comp_id)) records = self._to_records(self._handle_return(res)) if records: @@ -522,7 +523,7 @@ class AfterEffectsServerStub(): if records: return records.pop() - def render(self, folder_url): + def render(self, folder_url, comp_id): """ Render all renderqueueitem to 'folder_url' Args: @@ -531,7 +532,8 @@ class AfterEffectsServerStub(): """ res = self.websocketserver.call(self.client.call ('AfterEffects.render', - folder_url=folder_url)) + folder_url=folder_url, + comp_id=comp_id)) return self._handle_return(res) def get_extension_version(self): diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index d444ead6dc..2b37c1f101 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -64,14 +64,13 @@ class CollectAERender(publish.AbstractCollectRender): if family not in ["render", "renderLocal"]: # legacy continue - item_id = inst.data["members"][0] + comp_id = int(inst.data["members"][0]) - work_area_info = CollectAERender.get_stub().get_work_area( - int(item_id)) + work_area_info = CollectAERender.get_stub().get_work_area(comp_id) if not work_area_info: self.log.warning("Orphaned instance, deleting metadata") - inst_id = inst.get("instance_id") or item_id + inst_id = inst.get("instance_id") or str(comp_id) CollectAERender.get_stub().remove_instance(inst_id) continue @@ -84,7 +83,7 @@ class CollectAERender(publish.AbstractCollectRender): task_name = inst.data.get("task") # legacy - render_q = CollectAERender.get_stub().get_render_info() + render_q = CollectAERender.get_stub().get_render_info(comp_id) if not render_q: raise ValueError("No file extension set in Render Queue") @@ -118,13 +117,13 @@ class CollectAERender(publish.AbstractCollectRender): file_name=render_q.file_name ) - comp = compositions_by_id.get(int(item_id)) + comp = compositions_by_id.get(comp_id) if not comp: raise ValueError("There is no composition for item {}". - format(item_id)) + format(comp_id)) instance.outputDir = self._get_output_dir(instance) instance.comp_name = comp.name - instance.comp_id = item_id + instance.comp_id = comp_id is_local = "renderLocal" in inst.data["family"] # legacy if inst.data.get("creator_attributes"): diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index dc65cee61d..f2ae91c341 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -24,22 +24,28 @@ class ExtractLocalRender(publish.Extractor): self.log.info("staging_dir::{}".format(staging_dir)) # pull file name from Render Queue Output module - render_q = stub.get_render_info() - stub.render(staging_dir) + comp_id = instance.data['comp_id'] + render_q = stub.get_render_info(comp_id) # re queue render item if not render_q: raise ValueError("No file extension set in Render Queue") + + stub.render(staging_dir, comp_id) + _, ext = os.path.splitext(os.path.basename(render_q.file_name)) ext = ext[1:] first_file_path = None files = [] - self.log.info("files::{}".format(os.listdir(staging_dir))) for file_name in os.listdir(staging_dir): files.append(file_name) if first_file_path is None: first_file_path = os.path.join(staging_dir, file_name) + self.log.debug("files::{}".format(os.listdir(staging_dir))) + if not files: + raise ValueError("Nothing rendered!") + resulting_files = files if len(files) == 1: resulting_files = files[0] From d0f8233b8bedf5a3742d35a090f58ec99c2825a2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 10 Nov 2022 17:40:33 +0100 Subject: [PATCH 32/75] OP-4361 - updated documentation --- website/docs/artist_hosts_aftereffects.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/website/docs/artist_hosts_aftereffects.md b/website/docs/artist_hosts_aftereffects.md index d235cf44f0..ede2f7e86e 100644 --- a/website/docs/artist_hosts_aftereffects.md +++ b/website/docs/artist_hosts_aftereffects.md @@ -38,8 +38,6 @@ In AfterEffects you'll find the tools in the `OpenPype` extension: You can show the extension panel by going to `Window` > `Extensions` > `OpenPype`. -Because of current rendering limitations, it is expected that only single composition will be marked for publishing! - ### Publish When you are ready to share some work, you will need to publish it. This is done by opening the `Publisher` through the `Publish...` button. @@ -69,7 +67,8 @@ Publisher allows publishing into different context, just click on any instance, #### RenderQueue -AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue in an Output module. Currently its expected to have only single render item and single output module in the Render Queue. +AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue in an Output module. +Currently its expected to have only single render item and single output module per composition in the Render Queue. AE might throw some warning windows during publishing locally, so please pay attention to them in a case publishing seems to be stuck in a `Extract Local Render`. From 104fc1fd83af7b5993db2ef59f2eac63a3726e07 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 10 Nov 2022 17:49:35 +0100 Subject: [PATCH 33/75] OP-4361 - removed forgotten debug method call --- openpype/hosts/aftereffects/api/extension.zxp | Bin 101272 -> 101245 bytes .../api/extension/jsx/hostscript.jsx | 3 --- 2 files changed, 3 deletions(-) diff --git a/openpype/hosts/aftereffects/api/extension.zxp b/openpype/hosts/aftereffects/api/extension.zxp index e7b3bcac8ae26be2d55a122a77701e43e265fa06..339560fa387be6b792d2da0da71c879111b40b40 100644 GIT binary patch delta 7977 zcmZvBWl$VpwCpbK?(PsYxVyUq_u#g;Ew)%7Sa5d<=(I$Mx?f!0XN`Tye($#ntD%2K;_5cnf7r4>0Sv->Cwja<2iSKqECdurc_(o4 zz-**<^8FJq6!D!f837kypz*%hrNWp2J!u^N$^563nj$m|4gdf^0Q@`F+QK=^X}|>l zmi1F}%z$2S|F<)h*c`|PQ3$1>!x^EKTg~{nc}{x%axP!p0gMA6ytrhIq>^g!W6I}E zA3D0fP1zh@T>8e-=?zsEYLL{GS3bny+Ok}znhZZ@lsr<6r3%gB)8<~UN*MY7Wf(Eb z(ULti33nh|zp589d%H~%7I{9N-rfOcs1_u>eEw;?Z~IHJ^J!8Z!lQhW)?iFP){P%- z%Yk_T$@%g}`p{4uY(Fdku`3$Aykv;y*`Q=81tLO-6;}Z5-wRy`QuLAAf{$i5< zv=}3686Kijg)&_+BM&o7`u|4Ui%ibu-9=h=!WheBG5igy zDCcj_QYwOZYVa<)^l7M)bcR7Edd5)6=JPwM@LSSui5>2B-5MPBefyhwi)oyLk3BZt9m3K58M8o;*Z5@J`k!vjQ945|dEGHR4+h zIolqMJdhhA=b2(5L`I;nIz6tf^TR0%O`LpTI)1%ERP9l{Jn^kydpoIjVK*axnY0Dn zCFKHR7@)z>(`!Sp!5TGY^BAN9T^O6jde7k_92`)N&UoO_xLfeG__>7lKpb7*_Lsa=v!twN__+ zN@y%=J8saXhsO&C|4E9LoOW4gPt<&D>#PIl0wOl9XI8kV4XS6#J&P&t+(mB|au#)S znIq!z;~Emv>>coP9-Qi^x6SFZD|4USWVyZ8XI{=+a&VHY!dF&%kRs1r@q>jQPND0$ z>aB&a4~$yLkFp`<1f^>s=f-SH_Cay?1`2#UTpC96AI7ozlCZ64Ln}}AhYPYRDvM_| zWV+?jLw|9W(etx}n*sr=W)qV)Cr$TP9w}?!SDyx552pNso(d}KhO}; zYpQ^MxD$VfE~d19?1@t@^j>KS2b&zvT#rN>&AqG3S@kQZ?~|t`$F+B1r4Kbgc7uJ|&$l zaESBEr!5OBw;bJ}n|3!}o{!}ZyQ?0r(kK_W&Y#y%=NIN{5Vu+{Am}v8Am>A*&h7bu z=X~kv-Farb(~+d4L_av84}nBp6bTDE1YVC6hXHS`sHH_)G}u~6%KT|*O5iL(pT_nv z#5<#PCjQn@HHmZo&J&PkXl}d}*kL<##ap+I`%=wVMSRwPQ2f)Ov-s`h2j;C|ZPMt= zy72pIFR8`QzhOL<&o>FWEpiaEO*aeOCW`$(4z7R^-r?k-3U*w^)T7q@B} z)ZgYBT#VyTpS1h=o7Cd?K&j+|Ci8CrcJ+eRzV)kqP``_jlN!*gat!({phnCSd+EJQ6` zUdw--P#wyD$ChxqEzb0Egt>`|Gv6Q77o&i`jbFCPW{$^}R`}*d$v?2ZUbjk+oBNfi z&G9U~Q_Ep=xXWunTkIM#&%1gvP2NE_iH0uTID_k@L(zsG5<1Zd_c9wr&h`X-OX_?4 zh~itGe~yOzF�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 BB$faG delta 8041 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 diff --git a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx index c72b423af8..cfb72bacf5 100644 --- a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx +++ b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx @@ -769,6 +769,3 @@ function _prepareSingleValue(value){ function _prepareError(error_msg){ return JSON.stringify({"error": error_msg}) } - -// render("c:/projects/test", 2); -getRenderInfo(1); \ No newline at end of file From 2edcb15fbb1640dd57286d83828d9bb05e908c42 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 11 Nov 2022 17:55:26 +0800 Subject: [PATCH 34/75] fixing te multipart boolean option --- openpype/hosts/maya/api/lib_renderproducts.py | 20 ++++++++++++------- .../plugins/publish/submit_publish_job.py | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index a95c1c4932..78a0a89472 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -536,7 +536,11 @@ class RenderProductsArnold(ARenderProducts): products = [] aov_name = self._get_attr(aov, "name") - multipart = bool(self._get_attr("defaultArnoldDriver.multipart")) + multipart = False + multilayer = bool(self._get_attr("defaultArnoldDriver.multipart")) + merge_AOVs = bool(self._get_attr("defaultArnoldDriver.mergeAOVs")) + if multilayer or merge_AOVs: + multipart = True ai_drivers = cmds.listConnections("{}.outputs".format(aov), source=True, destination=False, @@ -1018,9 +1022,11 @@ class RenderProductsRedshift(ARenderProducts): # due to some AOVs still being written into separate files, # like Cryptomatte. # AOVs are merged in multi-channel file - - multipart = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) \ - or bool(self._get_attr("redshiftOptions.exrMultipart")) + multipart = False + force_layer = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) + exMultipart = bool(self._get_attr("redshiftOptions.exrMultipart")) + if exMultipart or force_layer: + multipart = True # Get Redshift Extension from image format image_format = self._get_attr("redshiftOptions.imageFormat") # integer @@ -1048,7 +1054,7 @@ class RenderProductsRedshift(ARenderProducts): # Any AOVs that still get processed, like Cryptomatte # by themselves are not multipart files. - aov_multipart = not multipart + # aov_multipart = not multipart # Redshift skips rendering of masterlayer without AOV suffix # when a Beauty AOV is rendered. It overrides the main layer. @@ -1079,7 +1085,7 @@ class RenderProductsRedshift(ARenderProducts): productName=aov_light_group_name, aov=aov_name, ext=ext, - multipart=aov_multipart, + multipart=multipart, camera=camera) products.append(product) @@ -1093,7 +1099,7 @@ class RenderProductsRedshift(ARenderProducts): product = RenderProduct(productName=aov_name, aov=aov_name, ext=ext, - multipart=aov_multipart, + multipart=multipart, camera=camera) products.append(product) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 27400bb269..e87cc6beeb 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -500,7 +500,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if instance_data.get("multipartExr"): preview = True - + self.log.info("preview:{}".format(preview)) new_instance = deepcopy(instance_data) new_instance["subset"] = subset_name new_instance["subsetGroup"] = group_name From 81d09b98ffa87983d08ee8fb6e5ef83f23f231d2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 11 Nov 2022 17:58:26 +0800 Subject: [PATCH 35/75] fixing te multipart boolean option --- openpype/hosts/maya/api/lib_renderproducts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 78a0a89472..58fcd2d281 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -1023,7 +1023,7 @@ class RenderProductsRedshift(ARenderProducts): # like Cryptomatte. # AOVs are merged in multi-channel file multipart = False - force_layer = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) + force_layer = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) # noqa exMultipart = bool(self._get_attr("redshiftOptions.exrMultipart")) if exMultipart or force_layer: multipart = True From 9324bf25383a773d8789a7d6debeea200b179b6f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 11 Nov 2022 18:05:42 +0800 Subject: [PATCH 36/75] fixing te multipart boolean option --- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index e87cc6beeb..c1e9dd4015 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -495,8 +495,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): render_file_name = os.path.basename(col) aov_patterns = self.aov_filter - # toggle preview on if multipart is on preview = match_aov_pattern(app, aov_patterns, render_file_name) + # toggle preview on if multipart is on if instance_data.get("multipartExr"): preview = True From 5f302eb95c970618497ed85b9e1ebbf713423963 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Nov 2022 12:20:19 +0100 Subject: [PATCH 37/75] OP-4361 - streamline local render Do not do get render queue again, should be already collected. --- .../plugins/publish/extract_local_render.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index f2ae91c341..309855f1c7 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -21,30 +21,32 @@ class ExtractLocalRender(publish.Extractor): def process(self, instance): stub = get_stub() staging_dir = instance.data["stagingDir"] - self.log.info("staging_dir::{}".format(staging_dir)) + self.log.debug("staging_dir::{}".format(staging_dir)) - # pull file name from Render Queue Output module - comp_id = instance.data['comp_id'] - render_q = stub.get_render_info(comp_id) # re queue render item - if not render_q: + # pull file name collected value from Render Queue Output module + if not instance.data["file_name"]: raise ValueError("No file extension set in Render Queue") + comp_id = instance.data['comp_id'] stub.render(staging_dir, comp_id) - _, ext = os.path.splitext(os.path.basename(render_q.file_name)) + _, ext = os.path.splitext(os.path.basename(instance.data["file_name"])) ext = ext[1:] first_file_path = None files = [] for file_name in os.listdir(staging_dir): + if not file_name.endswith(ext): + continue + files.append(file_name) if first_file_path is None: first_file_path = os.path.join(staging_dir, file_name) - self.log.debug("files::{}".format(os.listdir(staging_dir))) if not files: - raise ValueError("Nothing rendered!") + self.log.info("no files") + return resulting_files = files if len(files) == 1: From 9ba1028e615b4d39388dd2f39f4ed9cf473d98d6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Nov 2022 12:53:22 +0100 Subject: [PATCH 38/75] OP-4361 - fixed issue in render info Weird synchronization issue was happening in getRenderInfo when re-publishing published workfile. This implementation is ugly, but seems to work. --- openpype/hosts/aftereffects/api/extension.zxp | Bin 101245 -> 101376 bytes .../api/extension/jsx/hostscript.jsx | 120 ++++++++++-------- 2 files changed, 65 insertions(+), 55 deletions(-) diff --git a/openpype/hosts/aftereffects/api/extension.zxp b/openpype/hosts/aftereffects/api/extension.zxp index 339560fa387be6b792d2da0da71c879111b40b40..f80787fef8a989c46d4fda489a39b7792ff9500b 100644 GIT binary patch delta 8538 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�j3y*|tVkd!*Ct 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 TKf9p%eQl!x00Du(-*5i|E_3Gr diff --git a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx index cfb72bacf5..cdaa2e3b83 100644 --- a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx +++ b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx @@ -395,61 +395,6 @@ function saveAs(path){ app.project.save(fp = new File(path)); } -function getRenderInfo(comp_id){ - /*** - Get info from render queue. - Currently pulls only file name to parse extension and - if it is sequence in Python - **/ - var item = app.project.itemByID(comp_id); - if (!item){ - return _prepareError("Composition with '" + comp_id + "' wasn't found! Recreate publishable instance(s)") - } - - var comp_name = item.name; - try{ - var comp_id_count = 0; - for (i = 1; i <= app.project.renderQueue.numItems; ++i){ - var render_item = app.project.renderQueue.item(i); - if (render_item.comp.id != comp_id){ - continue; - } - comp_id_count += 1; - - if (render_item.status == RQItemStatus.DONE){ - var new_item = render_item.duplicate(); // create new, cannot change status if DONE - render_item.remove(); // remove existing to limit duplications - render_item = new_item; - } - - render_item.render = true; // always set render queue to render - var item = render_item.outputModule(1); - } - } catch (error) { - return _prepareError("There is no render queue, create one"); - } - - if (comp_id_count > 1){ - return _prepareError("There cannot be more items in Render Queue for '" + comp_name + "'!") - } - - if (comp_id_count == 0){ - return _prepareError("There is no item in Render Queue for '" + comp_name + "'! Add composition to Render Queue.") - } - - if (render_item.numOutputModules !=1){ - return _prepareError("There must be just 1 Output Module in Render Queue for '" + comp_name + "'! Keep only correct one.") - } - - var file_url = item.file.toString(); - - return JSON.stringify({ - "file_name": file_url, - "width": render_item.comp.width, - "height": render_item.comp.height - }) -} - function getAudioUrlForComp(comp_id){ /** * Searches composition for audio layer @@ -716,6 +661,71 @@ function isFileSequence (item){ return false; } +function getRenderInfo(comp_id){ + /*** + Get info from render queue. + Currently pulls only file name to parse extension and + if it is sequence in Python + **/ + var item = app.project.itemByID(comp_id); + if (!item){ + return _prepareError("Composition with '" + comp_id + "' wasn't found! Recreate publishable instance(s)") + } + + var comp_name = item.name; + try{ + // render_item.duplicate() should create new item on renderQueue + // BUT it works only sometimes, there are some weird synchronization issue + // this method will be called always before render, so prepare items here + // for render to spare the hassle + for (i = 1; i <= app.project.renderQueue.numItems; ++i){ + var render_item = app.project.renderQueue.item(i); + if (render_item.comp.id != comp_id){ + continue; + } + + if (render_item.status == RQItemStatus.DONE){ + render_item.duplicate(); // create new, cannot change status if DONE + render_item.remove(); // remove existing to limit duplications + continue; + } + } + + // properly validate as `numItems` won't change magically + var comp_id_count = 0; + for (i = 1; i <= app.project.renderQueue.numItems; ++i){ + var render_item = app.project.renderQueue.item(i); + if (render_item.comp.id != comp_id){ + continue; + } + comp_id_count += 1; + var item = render_item.outputModule(1); + } + } catch (error) { + return _prepareError("There is no render queue, create one"); + } + + if (comp_id_count > 1){ + return _prepareError("There cannot be more items in Render Queue for '" + comp_name + "'!") + } + + if (comp_id_count == 0){ + return _prepareError("There is no item in Render Queue for '" + comp_name + "'! Add composition to Render Queue.") + } + + if (render_item.numOutputModules !=1){ + return _prepareError("There must be just 1 Output Module in Render Queue for '" + comp_name + "'! Keep only correct one.") + } + + var file_url = item.file.toString(); + + return JSON.stringify({ + "file_name": file_url, + "width": render_item.comp.width, + "height": render_item.comp.height + }) +} + function render(target_folder, comp_id){ var out_dir = new Folder(target_folder); var out_dir = out_dir.fsName; From cf0cba1dba0d14b60ca1bff0f9d9170aff88bb43 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 11 Nov 2022 15:43:38 +0100 Subject: [PATCH 39/75] fix variable check in collect anatomy instance data --- openpype/plugins/publish/collect_anatomy_instance_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py index f67d3373d9..909b49a07d 100644 --- a/openpype/plugins/publish/collect_anatomy_instance_data.py +++ b/openpype/plugins/publish/collect_anatomy_instance_data.py @@ -188,7 +188,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): for subset_doc in subset_docs: subset_id = subset_doc["_id"] last_version_doc = last_version_docs_by_subset_id.get(subset_id) - if last_version_docs_by_subset_id is None: + if last_version_doc is None: continue asset_id = subset_doc["parent"] From ae8342c57932806f05b7e13a7d82ad7d0b5c4d0b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 14 Nov 2022 18:40:20 +0800 Subject: [PATCH 40/75] aov Filtering --- vendor/configs/OpenColorIO-Configs | 1 - 1 file changed, 1 deletion(-) delete mode 160000 vendor/configs/OpenColorIO-Configs diff --git a/vendor/configs/OpenColorIO-Configs b/vendor/configs/OpenColorIO-Configs deleted file mode 160000 index 0bb079c08b..0000000000 --- a/vendor/configs/OpenColorIO-Configs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0bb079c08be410030669cbf5f19ff869b88af953 From 6a18fa0fdaef4ed2d85a91f2f74b0bd7bccdd611 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Nov 2022 12:11:26 +0100 Subject: [PATCH 41/75] OP-4361 - updated Creator to use composition name There is new flag to allow use composition name Updated subset template. Removed obsolete 'renderLocal', now it is only 'render' with a flag. --- .../plugins/create/create_render.py | 68 +++++++++++++------ .../defaults/project_settings/global.json | 6 +- 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 65a4bc4500..7010696df3 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -1,3 +1,5 @@ +import re + from openpype import resources from openpype.lib import BoolDef, UISeparatorDef from openpype.hosts.aftereffects import api @@ -8,6 +10,7 @@ from openpype.pipeline import ( legacy_io, ) from openpype.hosts.aftereffects.api.pipeline import cache_and_get_instances +from openpype.lib import prepare_template_data class RenderCreator(Creator): @@ -50,7 +53,7 @@ class RenderCreator(Creator): self.host.remove_instance(instance) self._remove_instance_from_context(instance) - def create(self, subset_name, data, pre_create_data): + def create(self, subset_name_from_ui, data, pre_create_data): stub = api.get_stub() # only after After Effects is up if pre_create_data.get("use_selection"): items = stub.get_selected_items( @@ -59,31 +62,40 @@ class RenderCreator(Creator): else: items = stub.get_items(comps=True, folders=False, footages=False) - if len(items) > 1: - raise CreatorError( - "Please select only single composition at time." - ) if not items: - raise CreatorError(( + raise CreatorError( "Nothing to create. Select composition " "if 'useSelection' or create at least " "one composition." - )) + ) - for inst in self.create_context.instances: - if subset_name == inst.subset_name: - raise CreatorError("{} already exists".format( - inst.subset_name)) + for item in items: + if pre_create_data.get("use_composition_name"): + composition_name = item.name + dynamic_fill = prepare_template_data({"composition": + composition_name}) + subset_name = subset_name_from_ui.format(**dynamic_fill) + data["composition_name"] = composition_name + else: + subset_name = subset_name_from_ui + re.sub(subset_name, r"{composition}", '', + flags=re.IGNORECASE) - data["members"] = [items[0].id] - new_instance = CreatedInstance(self.family, subset_name, data, self) - if "farm" in pre_create_data: - use_farm = pre_create_data["farm"] - new_instance.creator_attributes["farm"] = use_farm + for inst in self.create_context.instances: + if subset_name == inst.subset_name: + raise CreatorError("{} already exists".format( + inst.subset_name)) - api.get_stub().imprint(new_instance.id, - new_instance.data_to_store()) - self._add_instance_to_context(new_instance) + data["members"] = [items[0].id] + new_instance = CreatedInstance(self.family, subset_name, data, + self) + if "farm" in pre_create_data: + use_farm = pre_create_data["farm"] + new_instance.creator_attributes["farm"] = use_farm + + api.get_stub().imprint(new_instance.id, + new_instance.data_to_store()) + self._add_instance_to_context(new_instance) def get_default_variants(self): return self._default_variants @@ -94,6 +106,8 @@ class RenderCreator(Creator): def get_pre_create_attr_defs(self): output = [ BoolDef("use_selection", default=True, label="Use selection"), + BoolDef("use_composition_name", + label="Use composition name in subset"), UISeparatorDef(), BoolDef("farm", label="Render on farm") ] @@ -102,6 +116,22 @@ class RenderCreator(Creator): def get_detail_description(self): return """Creator for Render instances""" + def get_dynamic_data(self, variant, task_name, asset_doc, + project_name, host_name, instance): + dynamic_data = super(RenderCreator, self).get_dynamic_data( + variant, task_name, asset_doc, project_name, host_name, + instance + ) + + if instance is not None: + composition_name = instance.get("composition_name") + if composition_name: + dynamic_data["composition"] = composition_name + else: + dynamic_data["composition"] = "{composition}" + + return dynamic_data + def _handle_legacy(self, instance_data): """Converts old instances to new format.""" if not instance_data.get("members"): diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index b128564bc2..9ad90af63a 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -347,15 +347,13 @@ "template": "{family}{Task}" }, { - "families": [ - "renderLocal" - ], + "families": ["render"], "hosts": [ "aftereffects" ], "task_types": [], "tasks": [], - "template": "render{Task}{Variant}" + "template": "{family}{Task}{Composition}{Variant}" }, { "families": [ From 702edb0efe0e268f1a92d0b7e020bfda2b05d694 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Nov 2022 12:17:27 +0100 Subject: [PATCH 42/75] OP-4361 - rename composition after create Previously it was renaming composition with subset name, this was missing in new creator. Renamed items to more descriptive 'comps'. --- .../aftereffects/plugins/create/create_render.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 7010696df3..518f3070af 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -56,22 +56,22 @@ class RenderCreator(Creator): def create(self, subset_name_from_ui, data, pre_create_data): stub = api.get_stub() # only after After Effects is up if pre_create_data.get("use_selection"): - items = stub.get_selected_items( + comps = stub.get_selected_items( comps=True, folders=False, footages=False ) else: - items = stub.get_items(comps=True, folders=False, footages=False) + comps = stub.get_items(comps=True, folders=False, footages=False) - if not items: + if not comps: raise CreatorError( "Nothing to create. Select composition " "if 'useSelection' or create at least " "one composition." ) - for item in items: + for comp in comps: if pre_create_data.get("use_composition_name"): - composition_name = item.name + composition_name = comp.name dynamic_fill = prepare_template_data({"composition": composition_name}) subset_name = subset_name_from_ui.format(**dynamic_fill) @@ -86,7 +86,7 @@ class RenderCreator(Creator): raise CreatorError("{} already exists".format( inst.subset_name)) - data["members"] = [items[0].id] + data["members"] = [comp.id] new_instance = CreatedInstance(self.family, subset_name, data, self) if "farm" in pre_create_data: @@ -97,6 +97,8 @@ class RenderCreator(Creator): new_instance.data_to_store()) self._add_instance_to_context(new_instance) + stub.rename_item(comp.id, subset_name) + def get_default_variants(self): return self._default_variants From 2e1004c618f136999d02728471c4fa9c4acb98dd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Nov 2022 16:03:45 +0100 Subject: [PATCH 43/75] OP-4361 - rename composition after update, delete --- .../aftereffects/plugins/create/create_render.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 518f3070af..5684657bf9 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -47,11 +47,25 @@ class RenderCreator(Creator): for created_inst, _changes in update_list: api.get_stub().imprint(created_inst.get("instance_id"), created_inst.data_to_store()) + subset_change = _changes.get("subset") + if subset_change: + api.get_stub().rename_item(created_inst.data["members"][0], + subset_change[1]) def remove_instances(self, instances): for instance in instances: - self.host.remove_instance(instance) self._remove_instance_from_context(instance) + self.host.remove_instance(instance) + + subset = instance.data["subset"] + comp_id = instance.data["members"][0] + comp = api.get_stub().get_item(comp_id) + if comp: + new_comp_name = comp.name.replace(subset, '') + if not new_comp_name: + new_comp_name = "dummyCompName" + api.get_stub().rename_item(comp_id, + new_comp_name) def create(self, subset_name_from_ui, data, pre_create_data): stub = api.get_stub() # only after After Effects is up From fb154ad4f18b43b6d7e872f239889bd98381d00a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Nov 2022 16:11:55 +0100 Subject: [PATCH 44/75] OP-4361 - implemented multiple output modules per composition Working version for local rendering. --- openpype/hosts/aftereffects/api/extension.zxp | Bin 101376 -> 101426 bytes .../api/extension/jsx/hostscript.jsx | 176 +++++++++--------- openpype/hosts/aftereffects/api/ws_stub.py | 5 +- .../plugins/publish/collect_render.py | 43 +++-- .../plugins/publish/extract_local_render.py | 64 ++++--- 5 files changed, 151 insertions(+), 137 deletions(-) diff --git a/openpype/hosts/aftereffects/api/extension.zxp b/openpype/hosts/aftereffects/api/extension.zxp index f80787fef8a989c46d4fda489a39b7792ff9500b..b436f0ca0b67313cf4509d1ec5e7d4a1dfd2d106 100644 GIT binary patch delta 8603 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 diff --git a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx index cdaa2e3b83..9b211207de 100644 --- a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx +++ b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx @@ -395,13 +395,84 @@ function saveAs(path){ app.project.save(fp = new File(path)); } +function getRenderInfo(comp_id){ + /*** + Get info from render queue. + Currently pulls only file name to parse extension and + if it is sequence in Python + Args: + comp_id (int): id of composition + Return: + (list) [{file_name:"xx.png", width:00, height:00}] + **/ + var item = app.project.itemByID(comp_id); + if (!item){ + return _prepareError("Composition with '" + comp_id + "' wasn't found! Recreate publishable instance(s)") + } + + var comp_name = item.name; + var output_metadata = [] + try{ + // render_item.duplicate() should create new item on renderQueue + // BUT it works only sometimes, there are some weird synchronization issue + // this method will be called always before render, so prepare items here + // for render to spare the hassle + for (i = 1; i <= app.project.renderQueue.numItems; ++i){ + var render_item = app.project.renderQueue.item(i); + if (render_item.comp.id != comp_id){ + continue; + } + + if (render_item.status == RQItemStatus.DONE){ + render_item.duplicate(); // create new, cannot change status if DONE + render_item.remove(); // remove existing to limit duplications + continue; + } + } + + // properly validate as `numItems` won't change magically + var comp_id_count = 0; + for (i = 1; i <= app.project.renderQueue.numItems; ++i){ + var render_item = app.project.renderQueue.item(i); + if (render_item.comp.id != comp_id){ + continue; + } + comp_id_count += 1; + var item = render_item.outputModule(1); + + for (j = 1; j<= render_item.numOutputModules; ++j){ + var file_url = item.file.toString(); + output_metadata.push( + JSON.stringify({ + "file_name": file_url, + "width": render_item.comp.width, + "height": render_item.comp.height + }) + ); + } + } + } catch (error) { + return _prepareError("There is no render queue, create one"); + } + + if (comp_id_count > 1){ + return _prepareError("There cannot be more items in Render Queue for '" + comp_name + "'!") + } + + if (comp_id_count == 0){ + return _prepareError("There is no item in Render Queue for '" + comp_name + "'! Add composition to Render Queue.") + } + + return '[' + output_metadata.join() + ']'; +} + function getAudioUrlForComp(comp_id){ /** * Searches composition for audio layer - * + * * Only single AVLayer is expected! * Used for collecting Audio - * + * * Args: * comp_id (int): id of composition * Return: @@ -429,7 +500,7 @@ function addItemAsLayerToComp(comp_id, item_id, found_comp){ /** * Adds already imported FootageItem ('item_id') as a new * layer to composition ('comp_id'). - * + * * Args: * comp_id (int): id of target composition * item_id (int): FootageItem.id @@ -452,17 +523,17 @@ function addItemAsLayerToComp(comp_id, item_id, found_comp){ function importBackground(comp_id, composition_name, files_to_import){ /** * Imports backgrounds images to existing or new composition. - * + * * If comp_id is not provided, new composition is created, basic * values (width, heights, frameRatio) takes from first imported * image. - * + * * Args: * comp_id (int): id of existing composition (null if new) - * composition_name (str): used when new composition + * composition_name (str): used when new composition * files_to_import (list): list of absolute paths to import and * add as layers - * + * * Returns: * (str): json representation (id, name, members) */ @@ -484,7 +555,7 @@ function importBackground(comp_id, composition_name, files_to_import){ } } } - + if (files_to_import){ for (i = 0; i < files_to_import.length; ++i){ item = _importItem(files_to_import[i]); @@ -496,8 +567,8 @@ function importBackground(comp_id, composition_name, files_to_import){ if (!comp){ folder = app.project.items.addFolder(composition_name); imported_ids.push(folder.id); - comp = app.project.items.addComp(composition_name, item.width, - item.height, item.pixelAspect, + comp = app.project.items.addComp(composition_name, item.width, + item.height, item.pixelAspect, 1, 26.7); // hardcode defaults imported_ids.push(comp.id); comp.parentFolder = folder; @@ -506,7 +577,7 @@ function importBackground(comp_id, composition_name, files_to_import){ item.parentFolder = folder; addItemAsLayerToComp(comp.id, item.id, comp); - } + } } var item = {"name": comp.name, "id": folder.id, @@ -517,19 +588,19 @@ function importBackground(comp_id, composition_name, files_to_import){ function reloadBackground(comp_id, composition_name, files_to_import){ /** * Reloads existing composition. - * + * * It deletes complete composition with encompassing folder, recreates * from scratch via 'importBackground' functionality. - * + * * Args: * comp_id (int): id of existing composition (null if new) - * composition_name (str): used when new composition + * composition_name (str): used when new composition * files_to_import (list): list of absolute paths to import and * add as layers - * + * * Returns: * (str): json representation (id, name, members) - * + * */ var imported_ids = []; // keep track of members of composition comp = app.project.itemByID(comp_id); @@ -592,7 +663,7 @@ function reloadBackground(comp_id, composition_name, files_to_import){ function _get_file_name(file_url){ /** * Returns file name without extension from 'file_url' - * + * * Args: * file_url (str): full absolute url * Returns: @@ -607,7 +678,7 @@ function _delete_obsolete_items(folder, new_filenames){ /*** * Goes through 'folder' and removes layers not in new * background - * + * * Args: * folder (FolderItem) * new_filenames (array): list of layer names in new bg @@ -632,14 +703,14 @@ function _delete_obsolete_items(folder, new_filenames){ function _importItem(file_url){ /** * Imports 'file_url' as new FootageItem - * + * * Args: * file_url (str): file url with content * Returns: * (FootageItem) */ file_name = _get_file_name(file_url); - + //importFile prepared previously to return json item_json = importFile(file_url, file_name, JSON.stringify({"ImportAsType":"FOOTAGE"})); item_json = JSON.parse(item_json); @@ -661,71 +732,6 @@ function isFileSequence (item){ return false; } -function getRenderInfo(comp_id){ - /*** - Get info from render queue. - Currently pulls only file name to parse extension and - if it is sequence in Python - **/ - var item = app.project.itemByID(comp_id); - if (!item){ - return _prepareError("Composition with '" + comp_id + "' wasn't found! Recreate publishable instance(s)") - } - - var comp_name = item.name; - try{ - // render_item.duplicate() should create new item on renderQueue - // BUT it works only sometimes, there are some weird synchronization issue - // this method will be called always before render, so prepare items here - // for render to spare the hassle - for (i = 1; i <= app.project.renderQueue.numItems; ++i){ - var render_item = app.project.renderQueue.item(i); - if (render_item.comp.id != comp_id){ - continue; - } - - if (render_item.status == RQItemStatus.DONE){ - render_item.duplicate(); // create new, cannot change status if DONE - render_item.remove(); // remove existing to limit duplications - continue; - } - } - - // properly validate as `numItems` won't change magically - var comp_id_count = 0; - for (i = 1; i <= app.project.renderQueue.numItems; ++i){ - var render_item = app.project.renderQueue.item(i); - if (render_item.comp.id != comp_id){ - continue; - } - comp_id_count += 1; - var item = render_item.outputModule(1); - } - } catch (error) { - return _prepareError("There is no render queue, create one"); - } - - if (comp_id_count > 1){ - return _prepareError("There cannot be more items in Render Queue for '" + comp_name + "'!") - } - - if (comp_id_count == 0){ - return _prepareError("There is no item in Render Queue for '" + comp_name + "'! Add composition to Render Queue.") - } - - if (render_item.numOutputModules !=1){ - return _prepareError("There must be just 1 Output Module in Render Queue for '" + comp_name + "'! Keep only correct one.") - } - - var file_url = item.file.toString(); - - return JSON.stringify({ - "file_name": file_url, - "width": render_item.comp.width, - "height": render_item.comp.height - }) -} - function render(target_folder, comp_id){ var out_dir = new Folder(target_folder); var out_dir = out_dir.fsName; diff --git a/openpype/hosts/aftereffects/api/ws_stub.py b/openpype/hosts/aftereffects/api/ws_stub.py index 32125a7d99..e5d6d9ed89 100644 --- a/openpype/hosts/aftereffects/api/ws_stub.py +++ b/openpype/hosts/aftereffects/api/ws_stub.py @@ -422,15 +422,14 @@ class AfterEffectsServerStub(): """ Get render queue info for render purposes Returns: - (AEItem): with 'file_name' field + (list) of (AEItem): with 'file_name' field """ res = self.websocketserver.call(self.client.call ('AfterEffects.get_render_info', comp_id=comp_id)) records = self._to_records(self._handle_return(res)) - if records: - return records.pop() + return records def get_audio_url(self, item_id): """ Get audio layer absolute url for comp diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index 2b37c1f101..6153a426cf 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -22,7 +22,7 @@ class AERenderInstance(RenderInstance): stagingDir = attr.ib(default=None) app_version = attr.ib(default=None) publish_attributes = attr.ib(default={}) - file_name = attr.ib(default=None) + file_names = attr.ib(default=[]) class CollectAERender(publish.AbstractCollectRender): @@ -86,6 +86,7 @@ class CollectAERender(publish.AbstractCollectRender): render_q = CollectAERender.get_stub().get_render_info(comp_id) if not render_q: raise ValueError("No file extension set in Render Queue") + render_item = render_q[0] subset_name = inst.data["subset"] instance = AERenderInstance( @@ -102,8 +103,8 @@ class CollectAERender(publish.AbstractCollectRender): setMembers='', publish=True, name=subset_name, - resolutionWidth=render_q.width, - resolutionHeight=render_q.height, + resolutionWidth=render_item.width, + resolutionHeight=render_item.height, pixelAspect=1, tileRendering=False, tilesX=0, @@ -114,7 +115,7 @@ class CollectAERender(publish.AbstractCollectRender): fps=fps, app_version=app_version, publish_attributes=inst.data.get("publish_attributes", {}), - file_name=render_q.file_name + file_names=[item.file_name for item in render_q] ) comp = compositions_by_id.get(comp_id) @@ -162,28 +163,30 @@ class CollectAERender(publish.AbstractCollectRender): start = render_instance.frameStart end = render_instance.frameEnd - _, ext = os.path.splitext(os.path.basename(render_instance.file_name)) - base_dir = self._get_output_dir(render_instance) expected_files = [] - if "#" not in render_instance.file_name: # single frame (mov)W - path = os.path.join(base_dir, "{}_{}_{}.{}".format( - render_instance.asset, - render_instance.subset, - "v{:03d}".format(render_instance.version), - ext.replace('.', '') - )) - expected_files.append(path) - else: - for frame in range(start, end + 1): - path = os.path.join(base_dir, "{}_{}_{}.{}.{}".format( + for file_name in render_instance.file_names: + _, ext = os.path.splitext(os.path.basename(file_name)) + ext = ext.replace('.', '') + version_str = "v{:03d}".format(render_instance.version) + if "#" not in file_name: # single frame (mov)W + path = os.path.join(base_dir, "{}_{}_{}.{}".format( render_instance.asset, render_instance.subset, - "v{:03d}".format(render_instance.version), - str(frame).zfill(self.padding_width), - ext.replace('.', '') + version_str, + ext )) expected_files.append(path) + else: + for frame in range(start, end + 1): + path = os.path.join(base_dir, "{}_{}_{}.{}.{}".format( + render_instance.asset, + render_instance.subset, + version_str, + str(frame).zfill(self.padding_width), + ext + )) + expected_files.append(path) return expected_files def _get_output_dir(self, render_instance): diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index 309855f1c7..d535329eb4 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -24,46 +24,52 @@ class ExtractLocalRender(publish.Extractor): self.log.debug("staging_dir::{}".format(staging_dir)) # pull file name collected value from Render Queue Output module - if not instance.data["file_name"]: + if not instance.data["file_names"]: raise ValueError("No file extension set in Render Queue") comp_id = instance.data['comp_id'] stub.render(staging_dir, comp_id) - _, ext = os.path.splitext(os.path.basename(instance.data["file_name"])) - ext = ext[1:] + representations = [] + for file_name in instance.data["file_names"]: + _, ext = os.path.splitext(os.path.basename(file_name)) + ext = ext[1:] - first_file_path = None - files = [] - for file_name in os.listdir(staging_dir): - if not file_name.endswith(ext): - continue + first_file_path = None + files = [] + for found_file_name in os.listdir(staging_dir): + if not found_file_name.endswith(ext): + continue - files.append(file_name) - if first_file_path is None: - first_file_path = os.path.join(staging_dir, - file_name) + files.append(found_file_name) + if first_file_path is None: + first_file_path = os.path.join(staging_dir, + found_file_name) - if not files: - self.log.info("no files") - return + if not files: + self.log.info("no files") + return - resulting_files = files - if len(files) == 1: - resulting_files = files[0] + # single file cannot be wrapped in array + resulting_files = files + if len(files) == 1: + resulting_files = files[0] - repre_data = { - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], - "name": ext, - "ext": ext, - "files": resulting_files, - "stagingDir": staging_dir - } - if instance.data["review"]: - repre_data["tags"] = ["review"] + repre_data = { + "frameStart": instance.data["frameStart"], + "frameEnd": instance.data["frameEnd"], + "name": ext, + "ext": ext, + "files": resulting_files, + "stagingDir": staging_dir + } + first_repre = not representations + if instance.data["review"] and first_repre: + repre_data["tags"] = ["review"] - instance.data["representations"] = [repre_data] + representations.append(repre_data) + + instance.data["representations"] = representations ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") # Generate thumbnail. From a9249c1b75f8f8174697bd207bf908132283b825 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Nov 2022 16:17:16 +0100 Subject: [PATCH 45/75] OP-4361 - remove limitation on single output module --- website/docs/artist_hosts_aftereffects.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/docs/artist_hosts_aftereffects.md b/website/docs/artist_hosts_aftereffects.md index ede2f7e86e..9f10a4f08d 100644 --- a/website/docs/artist_hosts_aftereffects.md +++ b/website/docs/artist_hosts_aftereffects.md @@ -68,7 +68,8 @@ Publisher allows publishing into different context, just click on any instance, #### RenderQueue AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue in an Output module. -Currently its expected to have only single render item and single output module per composition in the Render Queue. +Currently its expected to have only single render item per composition in the Render Queue. + AE might throw some warning windows during publishing locally, so please pay attention to them in a case publishing seems to be stuck in a `Extract Local Render`. From 9a503fce6a025d9918f1653cb531dcd56f4d2087 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Nov 2022 16:49:40 +0100 Subject: [PATCH 46/75] OP-4361 - fix wrong usage of regex --- openpype/hosts/aftereffects/plugins/create/create_render.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 5684657bf9..11864dd68c 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -92,8 +92,8 @@ class RenderCreator(Creator): data["composition_name"] = composition_name else: subset_name = subset_name_from_ui - re.sub(subset_name, r"{composition}", '', - flags=re.IGNORECASE) + subset_name = re.sub(r"\{composition\}", '', subset_name, + flags=re.IGNORECASE) for inst in self.create_context.instances: if subset_name == inst.subset_name: From b0e433e520feab015e90ddc6de4d8e25527f3066 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 16 Nov 2022 11:24:36 +0100 Subject: [PATCH 47/75] OP-4362 - remove unnecessary call to super --- openpype/hosts/aftereffects/plugins/create/create_render.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 11864dd68c..8d38288257 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -134,11 +134,7 @@ class RenderCreator(Creator): def get_dynamic_data(self, variant, task_name, asset_doc, project_name, host_name, instance): - dynamic_data = super(RenderCreator, self).get_dynamic_data( - variant, task_name, asset_doc, project_name, host_name, - instance - ) - + dynamic_data = {} if instance is not None: composition_name = instance.get("composition_name") if composition_name: From 33974c39d4aac0bb28ebd87e007acc166b8cd003 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 19 Nov 2022 17:13:50 +0800 Subject: [PATCH 48/75] aov Filtering --- openpype/hosts/maya/api/lib_renderproducts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 58fcd2d281..6fde0df162 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -1110,7 +1110,7 @@ class RenderProductsRedshift(ARenderProducts): if light_groups_enabled: return products - beauty_name = "Beauty_other" if has_beauty_aov else "" + beauty_name = "BeautyAux" if has_beauty_aov else "" for camera in cameras: products.insert(0, RenderProduct(productName=beauty_name, From cdb91c03795db7bc9b249e69dd605769562c11bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 21 Nov 2022 17:56:02 +0100 Subject: [PATCH 49/75] Added helper class for version resolving and sorting --- .../custom/plugins/GlobalJobPreLoad.py | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 9b35c9502d..6c3dd092fe 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -14,6 +14,137 @@ from Deadline.Scripting import ( ProcessUtils, ) +VERSION_REGEX = re.compile( + r"(?P0|[1-9]\d*)" + r"\.(?P0|[1-9]\d*)" + r"\.(?P0|[1-9]\d*)" + r"(?:-(?P[a-zA-Z\d\-.]*))?" + r"(?:\+(?P[a-zA-Z\d\-.]*))?" +) + + +class OpenPypeVersion: + """Fake semver version class for OpenPype version purposes. + + The version + """ + def __init__(self, major, minor, patch, prerelease, origin=None): + self.major = major + self.minor = minor + self.patch = patch + self.prerelease = prerelease + + is_valid = True + if not major or not minor or not patch: + is_valid = False + self.is_valid = is_valid + + if origin is None: + base = "{}.{}.{}".format(str(major), str(minor), str(patch)) + if not prerelease: + origin = base + else: + origin = "{}-{}".format(base, str(prerelease)) + + self.origin = origin + + @classmethod + def from_string(cls, version): + """Create an object of version from string. + + Args: + version (str): Version as a string. + + Returns: + Union[OpenPypeVersion, None]: Version object if input is nonempty + string otherwise None. + """ + + if not version: + return None + valid_parts = VERSION_REGEX.findall(version) + if len(valid_parts) != 1: + # Return invalid version with filled 'origin' attribute + return cls(None, None, None, None, origin=str(version)) + + # Unpack found version + major, minor, patch, pre, post = valid_parts[0] + prerelease = pre + # Post release is not important anymore and should be considered as + # part of prerelease + # - comparison is implemented to find suitable build and builds should + # never contain prerelease part so "not proper" parsing is + # acceptable for this use case. + if post: + prerelease = "{}+{}".format(pre, post) + + return cls( + int(major), int(minor), int(patch), prerelease, origin=version + ) + + def has_compatible_release(self, other): + """Version has compatible release as other version. + + Both major and minor versions must be exactly the same. In that case + a build can be considered as release compatible with any version. + + Args: + other (OpenPypeVersion): Other version. + + Returns: + bool: Version is release compatible with other version. + """ + + if self.is_valid and other.is_valid: + return self.major == other.major and self.minor == other.minor + return False + + def __bool__(self): + return self.is_valid + + def __repr__(self): + return "<{} {}>".format(self.__class__.__name__, self.origin) + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return self.origin == other + return self.origin == other.origin + + def __lt__(self, other): + if not isinstance(other, self.__class__): + return None + + if not self.is_valid: + return True + + if not other.is_valid: + return False + + if self.origin == other.origin: + return None + + same_major = self.major == other.major + if not same_major: + return self.major < other.major + + same_minor = self.minor == other.minor + if not same_minor: + return self.minor < other.minor + + same_patch = self.patch == other.patch + if not same_patch: + return self.patch < other.patch + + if not self.prerelease: + return False + + if not other.prerelease: + return True + + pres = [self.prerelease, other.prerelease] + pres.sort() + return pres[0] == self.prerelease + def get_openpype_version_from_path(path, build=True): """Get OpenPype version from provided path. From b1e899d8ee2a79cd673bdf14bf4adf2134443dca Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 21 Nov 2022 17:57:10 +0100 Subject: [PATCH 50/75] Use full version for resolving and use specific build if matches requested version --- .../custom/plugins/GlobalJobPreLoad.py | 197 ++++++++++-------- 1 file changed, 110 insertions(+), 87 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 6c3dd092fe..375cf48b8f 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -152,9 +152,9 @@ def get_openpype_version_from_path(path, build=True): build (bool, optional): Get only builds, not sources Returns: - str or None: version of OpenPype if found. - + Union[OpenPypeVersion, None]: version of OpenPype if found. """ + # fix path for application bundle on macos if platform.system().lower() == "darwin": path = os.path.join(path, "Contents", "MacOS", "lib", "Python") @@ -177,8 +177,10 @@ def get_openpype_version_from_path(path, build=True): with open(version_file, "r") as vf: exec(vf.read(), version) - version_match = re.search(r"(\d+\.\d+.\d+).*", version["__version__"]) - return version_match[1] + version_str = version.get("__version__") + if version_str: + return OpenPypeVersion.from_string(version_str) + return None def get_openpype_executable(): @@ -190,6 +192,91 @@ def get_openpype_executable(): return exe_list, dir_list +def get_openpype_versions(exe_list, dir_list): + print(">>> Getting OpenPype executable ...") + openpype_versions = [] + + install_dir = DirectoryUtils.SearchDirectoryList(dir_list) + if install_dir: + print("--- Looking for OpenPype at: {}".format(install_dir)) + sub_dirs = [ + f.path for f in os.scandir(install_dir) + if f.is_dir() + ] + for subdir in sub_dirs: + version = get_openpype_version_from_path(subdir) + if not version: + continue + print(" - found: {} - {}".format(version, subdir)) + openpype_versions.append((version, subdir)) + return openpype_versions + + +def get_requested_openpype_executable( + exe, dir_list, requested_version +): + requested_version_obj = OpenPypeVersion.from_string(requested_version) + if not requested_version_obj: + print(( + ">>> Requested version does not match version regex \"{}\"" + ).format(VERSION_REGEX)) + return None + + print(( + ">>> Scanning for compatible requested version {}" + ).format(requested_version)) + openpype_versions = get_openpype_versions(dir_list) + if not openpype_versions: + return None + + # if looking for requested compatible version, + # add the implicitly specified to the list too. + if exe: + exe_dir = os.path.dirname(exe) + print("Looking for OpenPype at: {}".format(exe_dir)) + version = get_openpype_version_from_path(exe_dir) + if version: + print(" - found: {} - {}".format(version, exe_dir)) + openpype_versions.append((version, exe_dir)) + + matching_item = None + compatible_versions = [] + for version_item in openpype_versions: + version, version_dir = version_item + if requested_version_obj.has_compatible_release(version): + compatible_versions.append(version_item) + if version == requested_version_obj: + # Store version item if version match exactly + # - break if is found matching version + matching_item = version_item + break + + if not compatible_versions: + return None + + compatible_versions.sort(key=lambda item: item[0]) + if matching_item: + version, version_dir = matching_item + print(( + "*** Found exact match build version {} in {}" + ).format(version_dir, version)) + + else: + version, version_dir = compatible_versions[-1] + + print(( + "*** Latest compatible version found is {} in {}" + ).format(version_dir, version)) + + # create list of executables for different platform and let + # Deadline decide. + exe_list = [ + os.path.join(version_dir, "openpype_console.exe"), + os.path.join(version_dir, "openpype_console") + ] + return FileUtils.SearchFileList(";".join(exe_list)) + + def inject_openpype_environment(deadlinePlugin): """ Pull env vars from OpenPype and push them to rendering process. @@ -199,93 +286,29 @@ def inject_openpype_environment(deadlinePlugin): print(">>> Injecting OpenPype environments ...") try: - print(">>> Getting OpenPype executable ...") exe_list, dir_list = get_openpype_executable() - openpype_versions = [] - # if the job requires specific OpenPype version, - # lets go over all available and find compatible build. + exe = FileUtils.SearchFileList(exe_list) + requested_version = job.GetJobEnvironmentKeyValue("OPENPYPE_VERSION") if requested_version: - print(( - ">>> Scanning for compatible requested version {}" - ).format(requested_version)) - install_dir = DirectoryUtils.SearchDirectoryList(dir_list) - if install_dir: - print("--- Looking for OpenPype at: {}".format(install_dir)) - sub_dirs = [ - f.path for f in os.scandir(install_dir) - if f.is_dir() - ] - for subdir in sub_dirs: - version = get_openpype_version_from_path(subdir) - if not version: - continue - print(" - found: {} - {}".format(version, subdir)) - openpype_versions.append((version, subdir)) + exe = get_requested_openpype_executable( + exe, dir_list, requested_version + ) + if exe is None: + raise RuntimeError(( + "Cannot find compatible version available for version {}" + " requested by the job. Please add it through plugin" + " configuration in Deadline or install it to configured" + " directory." + ).format(requested_version)) - exe = FileUtils.SearchFileList(exe_list) - if openpype_versions: - # if looking for requested compatible version, - # add the implicitly specified to the list too. - print("Looking for OpenPype at: {}".format(os.path.dirname(exe))) - version = get_openpype_version_from_path( - os.path.dirname(exe)) - if version: - print(" - found: {} - {}".format( - version, os.path.dirname(exe) - )) - openpype_versions.append((version, os.path.dirname(exe))) - - if requested_version: - # sort detected versions - if openpype_versions: - # use natural sorting - openpype_versions.sort( - key=lambda ver: [ - int(t) if t.isdigit() else t.lower() - for t in re.split(r"(\d+)", ver[0]) - ]) - print(( - "*** Latest available version found is {}" - ).format(openpype_versions[-1][0])) - requested_major, requested_minor, _ = requested_version.split(".")[:3] # noqa: E501 - compatible_versions = [] - for version in openpype_versions: - v = version[0].split(".")[:3] - if v[0] == requested_major and v[1] == requested_minor: - compatible_versions.append(version) - if not compatible_versions: - raise RuntimeError( - ("Cannot find compatible version available " - "for version {} requested by the job. " - "Please add it through plugin configuration " - "in Deadline or install it to configured " - "directory.").format(requested_version)) - # sort compatible versions nad pick the last one - compatible_versions.sort( - key=lambda ver: [ - int(t) if t.isdigit() else t.lower() - for t in re.split(r"(\d+)", ver[0]) - ]) - print(( - "*** Latest compatible version found is {}" - ).format(compatible_versions[-1][0])) - # create list of executables for different platform and let - # Deadline decide. - exe_list = [ - os.path.join( - compatible_versions[-1][1], "openpype_console.exe"), - os.path.join( - compatible_versions[-1][1], "openpype_console") - ] - exe = FileUtils.SearchFileList(";".join(exe_list)) - if exe == "": - raise RuntimeError( - "OpenPype executable was not found " + - "in the semicolon separated list " + - "\"" + ";".join(exe_list) + "\". " + - "The path to the render executable can be configured " + - "from the Plugin Configuration in the Deadline Monitor.") + if not exe: + raise RuntimeError(( + "OpenPype executable was not found in the semicolon " + "separated list \"{}\"." + "The path to the render executable can be configured" + " from the Plugin Configuration in the Deadline Monitor." + ).format(";".join(exe_list))) print("--- OpenPype executable: {}".format(exe)) From dbc72502b4cbf9859493d43ce90141f84ecc9420 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 21 Nov 2022 17:57:37 +0100 Subject: [PATCH 51/75] few formatting changes --- .../custom/plugins/GlobalJobPreLoad.py | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 375cf48b8f..78e1371eee 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -326,22 +326,22 @@ def inject_openpype_environment(deadlinePlugin): export_url ] - add_args = {} - add_args['project'] = \ - job.GetJobEnvironmentKeyValue('AVALON_PROJECT') - add_args['asset'] = job.GetJobEnvironmentKeyValue('AVALON_ASSET') - add_args['task'] = job.GetJobEnvironmentKeyValue('AVALON_TASK') - add_args['app'] = job.GetJobEnvironmentKeyValue('AVALON_APP_NAME') - add_args["envgroup"] = "farm" + add_kwargs = { + "project": job.GetJobEnvironmentKeyValue("AVALON_PROJECT"), + "asset": job.GetJobEnvironmentKeyValue("AVALON_ASSET"), + "task": job.GetJobEnvironmentKeyValue("AVALON_TASK"), + "app": job.GetJobEnvironmentKeyValue("AVALON_APP_NAME"), + "envgroup": "farm" + } + if all(add_kwargs.values()): + for key, value in add_kwargs.items(): + args.extend(["--{}".format(key), value]) - if all(add_args.values()): - for key, value in add_args.items(): - args.append("--{}".format(key)) - args.append(value) else: - msg = "Required env vars: AVALON_PROJECT, AVALON_ASSET, " + \ - "AVALON_TASK, AVALON_APP_NAME" - raise RuntimeError(msg) + raise RuntimeError(( + "Missing required env vars: AVALON_PROJECT, AVALON_ASSET," + " AVALON_TASK, AVALON_APP_NAME" + )) if not os.environ.get("OPENPYPE_MONGO"): print(">>> Missing OPENPYPE_MONGO env var, process won't work") @@ -362,12 +362,12 @@ def inject_openpype_environment(deadlinePlugin): print(">>> Loading file ...") with open(export_url) as fp: contents = json.load(fp) - for key, value in contents.items(): - deadlinePlugin.SetProcessEnvironmentVariable(key, value) + + for key, value in contents.items(): + deadlinePlugin.SetProcessEnvironmentVariable(key, value) script_url = job.GetJobPluginInfoKeyValue("ScriptFilename") if script_url: - script_url = script_url.format(**contents).replace("\\", "/") print(">>> Setting script path {}".format(script_url)) job.SetJobPluginInfoKeyValue("ScriptFilename", script_url) From 7bf1d0bc9b2efb05e9904a4784ded5ea2da5b717 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 22 Nov 2022 18:56:20 +0800 Subject: [PATCH 52/75] aov filtering --- openpype/hosts/maya/api/lib_renderproducts.py | 1 - .../modules/deadline/plugins/publish/submit_publish_job.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 6fde0df162..c54e3ab3e0 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -1054,7 +1054,6 @@ class RenderProductsRedshift(ARenderProducts): # Any AOVs that still get processed, like Cryptomatte # by themselves are not multipart files. - # aov_multipart = not multipart # Redshift skips rendering of masterlayer without AOV suffix # when a Beauty AOV is rendered. It overrides the main layer. diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index c1e9dd4015..6362b4ca65 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -500,7 +500,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if instance_data.get("multipartExr"): preview = True - self.log.info("preview:{}".format(preview)) + self.log.debug("preview:{}".format(preview)) new_instance = deepcopy(instance_data) new_instance["subset"] = subset_name new_instance["subsetGroup"] = group_name @@ -543,7 +543,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if new_instance.get("extendFrames", False): self._copy_extend_frames(new_instance, rep) instances.append(new_instance) - self.log.info("instances:{}".format(instances)) + self.log.debug("instances:{}".format(instances)) return instances def _get_representations(self, instance, exp_files): From ac9b9b208e055c856c32313a87d20bf1dbf403c3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 22 Nov 2022 12:54:28 +0100 Subject: [PATCH 53/75] OP-4196 - safer getter for published_path published_path might be missing in case of thumbnail not getting published. This implementation takes from staging if published_path not present --- .../slack/plugins/publish/integrate_slack_api.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index 643e55915b..f40a13db9f 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -142,13 +142,19 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): def _get_thumbnail_path(self, instance): """Returns abs url for thumbnail if present in instance repres""" - published_path = None + thumbnail_path = None for repre in instance.data.get("representations", []): if repre.get('thumbnail') or "thumbnail" in repre.get('tags', []): - if os.path.exists(repre["published_path"]): - published_path = repre["published_path"] + self.log.info(repre) + repre_thumbnail_path = ( + repre.get("published_path") or + os.path.join(repre["stagingDir"], repre["files"]) + ) + if os.path.exists(repre_thumbnail_path): + self.log.info("exists") + thumbnail_path = repre_thumbnail_path break - return published_path + return thumbnail_path def _get_review_path(self, instance): """Returns abs url for review if present in instance repres""" From c61098b782492728f7dbbe667b2540b2805b35ba Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 22 Nov 2022 13:00:34 +0100 Subject: [PATCH 54/75] OP-4196 - fix when task_data is not dict In legacy cases task might be only string with its name, not structure with additional metadata (type etc.). This implementation handles that. --- .../modules/slack/plugins/publish/integrate_slack_api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index f40a13db9f..6138671180 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -121,10 +121,13 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): ): fill_pairs.append(("task", task_data["name"])) - else: + elif isinstance(task_data, dict): for key, value in task_data.items(): fill_key = "task[{}]".format(key) fill_pairs.append((fill_key, value)) + else: + # fallback for legacy - if task_data is only task name + fill_pairs.append(("task", task_data)) self.log.debug("fill_pairs ::{}".format(fill_pairs)) multiple_case_variants = prepare_template_data(fill_pairs) From f993842c4ec7a4e91b5a42cbd61ddba0f9387a35 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 22 Nov 2022 13:01:27 +0100 Subject: [PATCH 55/75] OP-4196 - remove unnecessary logging --- openpype/modules/slack/plugins/publish/integrate_slack_api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index 6138671180..e43b07b228 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -148,13 +148,11 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): thumbnail_path = None for repre in instance.data.get("representations", []): if repre.get('thumbnail') or "thumbnail" in repre.get('tags', []): - self.log.info(repre) repre_thumbnail_path = ( repre.get("published_path") or os.path.join(repre["stagingDir"], repre["files"]) ) if os.path.exists(repre_thumbnail_path): - self.log.info("exists") thumbnail_path = repre_thumbnail_path break return thumbnail_path From ab17acddc7c192dab58727e87fe87b51e242a3df Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 22 Nov 2022 13:39:24 +0100 Subject: [PATCH 56/75] OP-4196 - better handling of data It should take task from instance anatomyData, then from context and handle non dict items. --- .../slack/plugins/publish/integrate_slack_api.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index e43b07b228..2c6f3d21bd 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -112,7 +112,13 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): if review_path: fill_pairs.append(("review_filepath", review_path)) - task_data = fill_data.get("task") + task_data = ( + copy.deepcopy(instance.data.get("anatomyData", [])).get("task") + or fill_data.get("task") + ) + if not isinstance(task_data, dict): + # fallback for legacy - if task_data is only task name + task_data["name"] = task_data if task_data: if ( "{task}" in message_templ @@ -121,13 +127,10 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): ): fill_pairs.append(("task", task_data["name"])) - elif isinstance(task_data, dict): + else: for key, value in task_data.items(): fill_key = "task[{}]".format(key) fill_pairs.append((fill_key, value)) - else: - # fallback for legacy - if task_data is only task name - fill_pairs.append(("task", task_data)) self.log.debug("fill_pairs ::{}".format(fill_pairs)) multiple_case_variants = prepare_template_data(fill_pairs) From 3cd241d2dbfa56a43ae2199fb1c38bd236497cd1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 22 Nov 2022 14:01:10 +0100 Subject: [PATCH 57/75] OP-4196 - fix wrong return type --- openpype/modules/slack/plugins/publish/integrate_slack_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index 2c6f3d21bd..9539d03306 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -113,7 +113,7 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): fill_pairs.append(("review_filepath", review_path)) task_data = ( - copy.deepcopy(instance.data.get("anatomyData", [])).get("task") + copy.deepcopy(instance.data.get("anatomyData", {})).get("task") or fill_data.get("task") ) if not isinstance(task_data, dict): From 8a121bc0ff43e86bbe42d660a29e1d1fed13e08c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 22 Nov 2022 15:11:58 +0100 Subject: [PATCH 58/75] move default settings from 'project_settings/global/tools/publish/template_name_profiles' to legacy place --- .../defaults/project_settings/global.json | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index b8995de99e..46b8b1b0c8 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -288,6 +288,17 @@ "task_types": [], "tasks": [], "template_name": "maya2unreal" + }, + { + "families": [ + "online" + ], + "hosts": [ + "traypublisher" + ], + "task_types": [], + "tasks": [], + "template_name": "online" } ] }, @@ -484,19 +495,7 @@ ] }, "publish": { - "template_name_profiles": [ - { - "families": [ - "online" - ], - "hosts": [ - "traypublisher" - ], - "task_types": [], - "task_names": [], - "template_name": "online" - } - ], + "template_name_profiles": [], "hero_template_name_profiles": [] } }, From 6af4412591b45f2001a9f01e998a36e871666ec9 Mon Sep 17 00:00:00 2001 From: clement hector Date: Tue, 22 Nov 2022 16:08:03 +0100 Subject: [PATCH 59/75] set creator window as parent of pop up window --- .../hosts/photoshop/plugins/create/create_legacy_image.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py index 2792a775e0..7672458165 100644 --- a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py @@ -29,7 +29,8 @@ class CreateImage(create.LegacyCreator): if len(selection) > 1: # Ask user whether to create one image or image per selected # item. - msg_box = QtWidgets.QMessageBox() + active_window = QtWidgets.QApplication.activeWindow() + msg_box = QtWidgets.QMessageBox(parent=active_window) msg_box.setIcon(QtWidgets.QMessageBox.Warning) msg_box.setText( "Multiple layers selected." @@ -102,7 +103,7 @@ class CreateImage(create.LegacyCreator): if group.long_name: for directory in group.long_name[::-1]: name = directory.replace(stub.PUBLISH_ICON, '').\ - replace(stub.LOADED_ICON, '') + replace(stub.LOADED_ICON, '') long_names.append(name) self.data.update({"subset": subset_name}) From 3b81c7f5731dfc5c018bff11e9758fc3e5e26450 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 22 Nov 2022 18:15:27 +0100 Subject: [PATCH 60/75] OP-4196 - better logging of file upload errors --- .../slack/plugins/publish/integrate_slack_api.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index 9539d03306..0cd5ec9de8 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -188,10 +188,17 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): channel=channel, title=os.path.basename(p_file) ) - attachment_str += "\n<{}|{}>".format( - response["file"]["permalink"], - os.path.basename(p_file)) - file_ids.append(response["file"]["id"]) + if response.get("error"): + error_str = self._enrich_error( + str(response.get("error")), + channel) + self.log.warning( + "Error happened: {}".format(error_str)) + else: + attachment_str += "\n<{}|{}>".format( + response["file"]["permalink"], + os.path.basename(p_file)) + file_ids.append(response["file"]["id"]) if publish_files: message += attachment_str From 855e7d1c61c16093706b276435aed02fbb108e91 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 22 Nov 2022 18:28:01 +0100 Subject: [PATCH 61/75] OP-4196 - fix filtering profiles Task types didn't work. --- .../modules/slack/plugins/publish/collect_slack_family.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/collect_slack_family.py b/openpype/modules/slack/plugins/publish/collect_slack_family.py index 39b05937dc..27e899d59a 100644 --- a/openpype/modules/slack/plugins/publish/collect_slack_family.py +++ b/openpype/modules/slack/plugins/publish/collect_slack_family.py @@ -18,15 +18,15 @@ class CollectSlackFamilies(pyblish.api.InstancePlugin): profiles = None def process(self, instance): - task_name = legacy_io.Session.get("AVALON_TASK") + task_data = instance.data["anatomyData"].get("task", {}) family = self.main_family_from_instance(instance) key_values = { "families": family, - "tasks": task_name, + "tasks": task_data.get("name"), + "task_types": task_data.get("type"), "hosts": instance.data["anatomyData"]["app"], "subsets": instance.data["subset"] } - profile = filter_profiles(self.profiles, key_values, logger=self.log) From c3e5b7a169c670b35889a5fb0038ba4b50bf7841 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Nov 2022 21:21:42 +0100 Subject: [PATCH 62/75] update history.md --- HISTORY.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index f6cc74e114..7365696f96 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,40 @@ # Changelog +## [3.14.6](https://github.com/pypeclub/OpenPype/tree/3.14.6) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.5...3.14.6) + +### 📖 Documentation + +- Documentation: Minor updates to dev\_requirements.md [\#4025](https://github.com/pypeclub/OpenPype/pull/4025) + +**🆕 New features** + +- Nuke: add 13.2 variant [\#4041](https://github.com/pypeclub/OpenPype/pull/4041) + +**🚀 Enhancements** + +- Publish Report Viewer: Store reports locally on machine [\#4040](https://github.com/pypeclub/OpenPype/pull/4040) +- General: More specific error in burnins script [\#4026](https://github.com/pypeclub/OpenPype/pull/4026) +- General: Extract review does not crash with old settings overrides [\#4023](https://github.com/pypeclub/OpenPype/pull/4023) +- Publisher: Convertors for legacy instances [\#4020](https://github.com/pypeclub/OpenPype/pull/4020) +- workflows: adding milestone creator and assigner [\#4018](https://github.com/pypeclub/OpenPype/pull/4018) +- Publisher: Catch creator errors [\#4015](https://github.com/pypeclub/OpenPype/pull/4015) + +**🐛 Bug fixes** + +- Hiero - effect collection fixes [\#4038](https://github.com/pypeclub/OpenPype/pull/4038) +- Nuke - loader clip correct hash conversion in path [\#4037](https://github.com/pypeclub/OpenPype/pull/4037) +- Maya: Soft fail when applying capture preset [\#4034](https://github.com/pypeclub/OpenPype/pull/4034) +- Igniter: handle missing directory [\#4032](https://github.com/pypeclub/OpenPype/pull/4032) +- StandalonePublisher: Fix thumbnail publishing [\#4029](https://github.com/pypeclub/OpenPype/pull/4029) +- Experimental Tools: Fix publisher import [\#4027](https://github.com/pypeclub/OpenPype/pull/4027) +- Houdini: fix wrong path in ASS loader [\#4016](https://github.com/pypeclub/OpenPype/pull/4016) + +**🔀 Refactored code** + +- General: Import lib functions from lib [\#4017](https://github.com/pypeclub/OpenPype/pull/4017) + ## [3.14.5](https://github.com/pypeclub/OpenPype/tree/3.14.5) (2022-10-24) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.4...3.14.5) From e600cd1b3d2963a2a2e26dce79e07818bb4c5d28 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Nov 2022 21:24:11 +0100 Subject: [PATCH 63/75] updating to 3.14.7 --- CHANGELOG.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 707b61676f..c3cccf2d1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,63 @@ # Changelog -## [3.14.6](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.14.7](https://github.com/pypeclub/OpenPype/tree/3.14.7) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.5...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.6...3.14.7) + +**🆕 New features** + +- Hiero: loading effect family to timeline [\#4055](https://github.com/pypeclub/OpenPype/pull/4055) + +**🚀 Enhancements** + +- Ftrack: Event server status give more information about version locations [\#4112](https://github.com/pypeclub/OpenPype/pull/4112) +- General: Allow higher numbers in frames and clips [\#4101](https://github.com/pypeclub/OpenPype/pull/4101) +- Publisher: Settings for validate frame range [\#4097](https://github.com/pypeclub/OpenPype/pull/4097) +- Publisher: Ignore escape button [\#4090](https://github.com/pypeclub/OpenPype/pull/4090) +- Flame: Loading clip with native colorspace resolved from mapping [\#4079](https://github.com/pypeclub/OpenPype/pull/4079) +- General: Extract review single frame output [\#4064](https://github.com/pypeclub/OpenPype/pull/4064) +- Publisher: Prepared common function for instance data cache [\#4063](https://github.com/pypeclub/OpenPype/pull/4063) +- Publisher: Easy access to publish page from create page [\#4058](https://github.com/pypeclub/OpenPype/pull/4058) +- General/TVPaint: Attribute defs dialog [\#4052](https://github.com/pypeclub/OpenPype/pull/4052) +- Publisher: Better reset defer [\#4048](https://github.com/pypeclub/OpenPype/pull/4048) +- Publisher: Add thumbnail sources [\#4042](https://github.com/pypeclub/OpenPype/pull/4042) + +**🐛 Bug fixes** + +- General: Move default settings for template name [\#4119](https://github.com/pypeclub/OpenPype/pull/4119) +- Nuke: loaded nodes set to first tab [\#4114](https://github.com/pypeclub/OpenPype/pull/4114) +- Nuke: load image first frame [\#4113](https://github.com/pypeclub/OpenPype/pull/4113) +- Files Widget: Ignore case sensitivity of extensions [\#4096](https://github.com/pypeclub/OpenPype/pull/4096) +- Webpublisher: extension is lowercased in Setting and in uploaded files [\#4095](https://github.com/pypeclub/OpenPype/pull/4095) +- Publish Report Viewer: Fix small bugs [\#4086](https://github.com/pypeclub/OpenPype/pull/4086) +- Igniter: fix regex to match semver better [\#4085](https://github.com/pypeclub/OpenPype/pull/4085) +- Maya: aov filtering [\#4083](https://github.com/pypeclub/OpenPype/pull/4083) +- Flame/Flare: Loading to multiple batches [\#4080](https://github.com/pypeclub/OpenPype/pull/4080) +- hiero: creator from settings with set maximum [\#4077](https://github.com/pypeclub/OpenPype/pull/4077) +- Nuke: resolve hashes in file name only for frame token [\#4074](https://github.com/pypeclub/OpenPype/pull/4074) +- Publisher: Fix cache of asset docs [\#4070](https://github.com/pypeclub/OpenPype/pull/4070) +- Webpublisher: cleanup wp extract thumbnail [\#4067](https://github.com/pypeclub/OpenPype/pull/4067) +- Settings UI: Locked setting can't bypass lock [\#4066](https://github.com/pypeclub/OpenPype/pull/4066) +- Loader: Fix comparison of repre name [\#4053](https://github.com/pypeclub/OpenPype/pull/4053) +- Deadline: Extract environment subprocess failure [\#4050](https://github.com/pypeclub/OpenPype/pull/4050) + +**🔀 Refactored code** + +- General: Collect entities plugin minor changes [\#4089](https://github.com/pypeclub/OpenPype/pull/4089) +- General: Direct interfaces import [\#4065](https://github.com/pypeclub/OpenPype/pull/4065) + +**Merged pull requests:** + +- Bump loader-utils from 1.4.1 to 1.4.2 in /website [\#4100](https://github.com/pypeclub/OpenPype/pull/4100) +- Online family for Tray Publisher [\#4093](https://github.com/pypeclub/OpenPype/pull/4093) +- Bump loader-utils from 1.4.0 to 1.4.1 in /website [\#4081](https://github.com/pypeclub/OpenPype/pull/4081) +- remove underscore from subset name [\#4059](https://github.com/pypeclub/OpenPype/pull/4059) +- Alembic Loader as Arnold Standin [\#4047](https://github.com/pypeclub/OpenPype/pull/4047) + + +## [3.14.6](https://github.com/pypeclub/OpenPype/tree/3.14.6) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.5...3.14.6) ### 📖 Documentation From c63f468484b32628c6d87a35df993bf2303ecb83 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 23 Nov 2022 03:35:08 +0000 Subject: [PATCH 64/75] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 0116b49f4d..a4af8b7a99 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.7-nightly.6" +__version__ = "3.14.7-nightly.7" From 0d88af8aec4c6112be2629865da7ffce4a7cce4d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Nov 2022 11:40:16 +0100 Subject: [PATCH 65/75] update latest 3.14.7 --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3cccf2d1e..0c5f2cf8b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ **🚀 Enhancements** +- Photoshop: bug with pop-up window on Instance Creator [\#4121](https://github.com/pypeclub/OpenPype/pull/4121) +- Publisher: Open on specific tab [\#4120](https://github.com/pypeclub/OpenPype/pull/4120) +- Publisher: Hide unknown publish values [\#4116](https://github.com/pypeclub/OpenPype/pull/4116) - Ftrack: Event server status give more information about version locations [\#4112](https://github.com/pypeclub/OpenPype/pull/4112) - General: Allow higher numbers in frames and clips [\#4101](https://github.com/pypeclub/OpenPype/pull/4101) - Publisher: Settings for validate frame range [\#4097](https://github.com/pypeclub/OpenPype/pull/4097) @@ -25,6 +28,7 @@ **🐛 Bug fixes** - General: Move default settings for template name [\#4119](https://github.com/pypeclub/OpenPype/pull/4119) +- Slack: notification fail in new tray publisher [\#4118](https://github.com/pypeclub/OpenPype/pull/4118) - Nuke: loaded nodes set to first tab [\#4114](https://github.com/pypeclub/OpenPype/pull/4114) - Nuke: load image first frame [\#4113](https://github.com/pypeclub/OpenPype/pull/4113) - Files Widget: Ignore case sensitivity of extensions [\#4096](https://github.com/pypeclub/OpenPype/pull/4096) @@ -54,7 +58,6 @@ - remove underscore from subset name [\#4059](https://github.com/pypeclub/OpenPype/pull/4059) - Alembic Loader as Arnold Standin [\#4047](https://github.com/pypeclub/OpenPype/pull/4047) - ## [3.14.6](https://github.com/pypeclub/OpenPype/tree/3.14.6) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.5...3.14.6) From 6725c1f6d8dc025f13bffbbb1c92a242c49b618f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Nov 2022 11:40:57 +0100 Subject: [PATCH 66/75] udpate history --- HISTORY.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 7365696f96..04a1073c07 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,64 @@ # Changelog + +## [3.14.7](https://github.com/pypeclub/OpenPype/tree/3.14.7) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.6...3.14.7) + +**🆕 New features** + +- Hiero: loading effect family to timeline [\#4055](https://github.com/pypeclub/OpenPype/pull/4055) + +**🚀 Enhancements** + +- Photoshop: bug with pop-up window on Instance Creator [\#4121](https://github.com/pypeclub/OpenPype/pull/4121) +- Publisher: Open on specific tab [\#4120](https://github.com/pypeclub/OpenPype/pull/4120) +- Publisher: Hide unknown publish values [\#4116](https://github.com/pypeclub/OpenPype/pull/4116) +- Ftrack: Event server status give more information about version locations [\#4112](https://github.com/pypeclub/OpenPype/pull/4112) +- General: Allow higher numbers in frames and clips [\#4101](https://github.com/pypeclub/OpenPype/pull/4101) +- Publisher: Settings for validate frame range [\#4097](https://github.com/pypeclub/OpenPype/pull/4097) +- Publisher: Ignore escape button [\#4090](https://github.com/pypeclub/OpenPype/pull/4090) +- Flame: Loading clip with native colorspace resolved from mapping [\#4079](https://github.com/pypeclub/OpenPype/pull/4079) +- General: Extract review single frame output [\#4064](https://github.com/pypeclub/OpenPype/pull/4064) +- Publisher: Prepared common function for instance data cache [\#4063](https://github.com/pypeclub/OpenPype/pull/4063) +- Publisher: Easy access to publish page from create page [\#4058](https://github.com/pypeclub/OpenPype/pull/4058) +- General/TVPaint: Attribute defs dialog [\#4052](https://github.com/pypeclub/OpenPype/pull/4052) +- Publisher: Better reset defer [\#4048](https://github.com/pypeclub/OpenPype/pull/4048) +- Publisher: Add thumbnail sources [\#4042](https://github.com/pypeclub/OpenPype/pull/4042) + +**🐛 Bug fixes** + +- General: Move default settings for template name [\#4119](https://github.com/pypeclub/OpenPype/pull/4119) +- Slack: notification fail in new tray publisher [\#4118](https://github.com/pypeclub/OpenPype/pull/4118) +- Nuke: loaded nodes set to first tab [\#4114](https://github.com/pypeclub/OpenPype/pull/4114) +- Nuke: load image first frame [\#4113](https://github.com/pypeclub/OpenPype/pull/4113) +- Files Widget: Ignore case sensitivity of extensions [\#4096](https://github.com/pypeclub/OpenPype/pull/4096) +- Webpublisher: extension is lowercased in Setting and in uploaded files [\#4095](https://github.com/pypeclub/OpenPype/pull/4095) +- Publish Report Viewer: Fix small bugs [\#4086](https://github.com/pypeclub/OpenPype/pull/4086) +- Igniter: fix regex to match semver better [\#4085](https://github.com/pypeclub/OpenPype/pull/4085) +- Maya: aov filtering [\#4083](https://github.com/pypeclub/OpenPype/pull/4083) +- Flame/Flare: Loading to multiple batches [\#4080](https://github.com/pypeclub/OpenPype/pull/4080) +- hiero: creator from settings with set maximum [\#4077](https://github.com/pypeclub/OpenPype/pull/4077) +- Nuke: resolve hashes in file name only for frame token [\#4074](https://github.com/pypeclub/OpenPype/pull/4074) +- Publisher: Fix cache of asset docs [\#4070](https://github.com/pypeclub/OpenPype/pull/4070) +- Webpublisher: cleanup wp extract thumbnail [\#4067](https://github.com/pypeclub/OpenPype/pull/4067) +- Settings UI: Locked setting can't bypass lock [\#4066](https://github.com/pypeclub/OpenPype/pull/4066) +- Loader: Fix comparison of repre name [\#4053](https://github.com/pypeclub/OpenPype/pull/4053) +- Deadline: Extract environment subprocess failure [\#4050](https://github.com/pypeclub/OpenPype/pull/4050) + +**🔀 Refactored code** + +- General: Collect entities plugin minor changes [\#4089](https://github.com/pypeclub/OpenPype/pull/4089) +- General: Direct interfaces import [\#4065](https://github.com/pypeclub/OpenPype/pull/4065) + +**Merged pull requests:** + +- Bump loader-utils from 1.4.1 to 1.4.2 in /website [\#4100](https://github.com/pypeclub/OpenPype/pull/4100) +- Online family for Tray Publisher [\#4093](https://github.com/pypeclub/OpenPype/pull/4093) +- Bump loader-utils from 1.4.0 to 1.4.1 in /website [\#4081](https://github.com/pypeclub/OpenPype/pull/4081) +- remove underscore from subset name [\#4059](https://github.com/pypeclub/OpenPype/pull/4059) +- Alembic Loader as Arnold Standin [\#4047](https://github.com/pypeclub/OpenPype/pull/4047) + ## [3.14.6](https://github.com/pypeclub/OpenPype/tree/3.14.6) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.5...3.14.6) From 2594bc2a0efa19331b7dbccb2624be41acf1032a Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 23 Nov 2022 10:45:17 +0000 Subject: [PATCH 67/75] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index a4af8b7a99..a00c7de704 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.7-nightly.7" +__version__ = "3.14.7-nightly.8" From 8b1b09b33825dc9ff320b6c6e49597dedcf58f7f Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 23 Nov 2022 10:58:00 +0000 Subject: [PATCH 68/75] [Automated] Release --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index a00c7de704..ffabcf8025 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.7-nightly.8" +__version__ = "3.14.7" From 5779687a2b4467195a20bd9242d2fa782f7b27cd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 23 Nov 2022 11:59:53 +0100 Subject: [PATCH 69/75] Removed unused argument --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 78e1371eee..40193bac71 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -192,7 +192,7 @@ def get_openpype_executable(): return exe_list, dir_list -def get_openpype_versions(exe_list, dir_list): +def get_openpype_versions(dir_list): print(">>> Getting OpenPype executable ...") openpype_versions = [] From c3b7e3269544d3471c02e193acd4054b5a08eb08 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Nov 2022 16:30:49 +0100 Subject: [PATCH 70/75] skip turning on/off of autosync --- .../publish/integrate_hierarchy_ftrack.py | 43 +------------------ 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index fa7a89050c..6bae922d94 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -8,9 +8,6 @@ import pyblish.api from openpype.client import get_asset_by_id from openpype.lib import filter_profiles - -# Copy of constant `openpype_modules.ftrack.lib.avalon_sync.CUST_ATTR_AUTO_SYNC` -CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" CUST_ATTR_GROUP = "openpype" @@ -97,18 +94,9 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): self.task_types = self.get_all_task_types(project) self.task_statuses = self.get_task_statuses(project) - # disable termporarily ftrack project's autosyncing - if auto_sync_state: - self.auto_sync_off(project) + # import ftrack hierarchy + self.import_to_ftrack(project_name, hierarchy_context) - try: - # import ftrack hierarchy - self.import_to_ftrack(project_name, hierarchy_context) - except Exception: - raise - finally: - if auto_sync_state: - self.auto_sync_on(project) def import_to_ftrack(self, project_name, input_data, parent=None): # Prequery hiearchical custom attributes @@ -381,33 +369,6 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): return entity - def auto_sync_off(self, project): - project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = False - - self.log.info("Ftrack autosync swithed off") - - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - def auto_sync_on(self, project): - - project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = True - - self.log.info("Ftrack autosync swithed on") - - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - def _get_active_assets(self, context): """ Returns only asset dictionary. Usually the last part of deep dictionary which From 635c662a8c357c5170aadfb9197081a16c27c3b2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Nov 2022 16:32:04 +0100 Subject: [PATCH 71/75] raise known publish error if project in ftrack was not found --- .../publish/integrate_hierarchy_ftrack.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index 6bae922d94..8b0e4ab62d 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -7,6 +7,7 @@ import pyblish.api from openpype.client import get_asset_by_id from openpype.lib import filter_profiles +from openpype.pipeline import KnownPublishError CUST_ATTR_GROUP = "openpype" @@ -16,7 +17,6 @@ CUST_ATTR_GROUP = "openpype" def get_pype_attr(session, split_hierarchical=True): custom_attributes = [] hier_custom_attributes = [] - # TODO remove deprecated "avalon" group from query cust_attrs_query = ( "select id, entity_type, object_type_id, is_hierarchical, default" " from CustomAttributeConfiguration" @@ -76,19 +76,25 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): create_task_status_profiles = [] def process(self, context): - self.context = context - if "hierarchyContext" not in self.context.data: + if "hierarchyContext" not in context.data: return hierarchy_context = self._get_active_assets(context) self.log.debug("__ hierarchy_context: {}".format(hierarchy_context)) - session = self.context.data["ftrackSession"] - project_name = self.context.data["projectEntity"]["name"] - query = 'Project where full_name is "{}"'.format(project_name) - project = session.query(query).one() - auto_sync_state = project["custom_attributes"][CUST_ATTR_AUTO_SYNC] + session = context.data["ftrackSession"] + project_name = context.data["projectName"] + project = session.query( + 'select id, full_name from Project where full_name is "{}"'.format( + project_name + ) + ).first() + if not project: + raise KnownPublishError( + "Project \"{}\" was not found on ftrack.".format(project_name) + ) + self.context = context self.session = session self.ft_project = project self.task_types = self.get_all_task_types(project) From 5a0cc527325642c9871323a6aba8c263be72d194 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Nov 2022 16:34:02 +0100 Subject: [PATCH 72/75] implemented helper methods to query information we need from ftrack --- .../publish/integrate_hierarchy_ftrack.py | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index 8b0e4ab62d..02946f813f 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -103,6 +103,129 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): # import ftrack hierarchy self.import_to_ftrack(project_name, hierarchy_context) + def query_ftrack_entitites(self, session, ft_project): + project_id = ft_project["id"] + entities = session.query(( + "select id, name, parent_id" + " from TypedContext where project_id is \"{}\"" + ).format(project_id)).all() + + entities_by_id = {} + entities_by_parent_id = collections.defaultdict(list) + for entity in entities: + entities_by_id[entity["id"]] = entity + parent_id = entity["parent_id"] + entities_by_parent_id[parent_id].append(entity) + + ftrack_hierarchy = [] + ftrack_id_queue = collections.deque() + ftrack_id_queue.append((project_id, ftrack_hierarchy)) + while ftrack_id_queue: + item = ftrack_id_queue.popleft() + ftrack_id, parent_list = item + if ftrack_id == project_id: + entity = ft_project + name = entity["full_name"] + else: + entity = entities_by_id[ftrack_id] + name = entity["name"] + + children = [] + parent_list.append({ + "name": name, + "low_name": name.lower(), + "entity": entity, + "children": children, + }) + for child in entities_by_parent_id[ftrack_id]: + ftrack_id_queue.append((child["id"], children)) + return ftrack_hierarchy + + def find_matching_ftrack_entities( + self, hierarchy_context, ftrack_hierarchy + ): + walk_queue = collections.deque() + for entity_name, entity_data in hierarchy_context.items(): + walk_queue.append( + (entity_name, entity_data, ftrack_hierarchy) + ) + + matching_ftrack_entities = [] + while walk_queue: + item = walk_queue.popleft() + entity_name, entity_data, ft_children = item + matching_ft_child = None + for ft_child in ft_children: + if ft_child["low_name"] == entity_name.lower(): + matching_ft_child = ft_child + break + + if matching_ft_child is None: + continue + + entity = matching_ft_child["entity"] + entity_data["ft_entity"] = entity + matching_ftrack_entities.append(entity) + + hierarchy_children = entity_data.get("childs") + if not hierarchy_children: + continue + + for child_name, child_data in hierarchy_children.items(): + walk_queue.append( + (child_name, child_data, matching_ft_child["children"]) + ) + return matching_ftrack_entities + + def query_custom_attribute_values(self, session, entities, hier_attrs): + attr_ids = { + attr["id"] + for attr in hier_attrs + } + entity_ids = { + entity["id"] + for entity in entities + } + output = { + entity_id: {} + for entity_id in entity_ids + } + if not attr_ids or not entity_ids: + return {} + + joined_attr_ids = ",".join( + ['"{}"'.format(attr_id) for attr_id in attr_ids] + ) + + # Query values in chunks + chunk_size = int(5000 / len(attr_ids)) + # Make sure entity_ids is `list` for chunk selection + entity_ids = list(entity_ids) + results = [] + for idx in range(0, len(entity_ids), chunk_size): + joined_entity_ids = ",".join([ + '"{}"'.format(entity_id) + for entity_id in entity_ids[idx:idx + chunk_size] + ]) + results.extend( + session.query( + ( + "select value, entity_id, configuration_id" + " from CustomAttributeValue" + " where entity_id in ({}) and configuration_id in ({})" + ).format( + joined_entity_ids, + joined_attr_ids + ) + ).all() + ) + + for result in results: + attr_id = result["configuration_id"] + entity_id = result["entity_id"] + output[entity_id][attr_id] = result["value"] + + return output def import_to_ftrack(self, project_name, input_data, parent=None): # Prequery hiearchical custom attributes From a78ef54e56e7a0a0300fdc140ec40fb1be4111e9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Nov 2022 16:35:02 +0100 Subject: [PATCH 73/75] query user at the start of import method instead of requerying it again --- .../publish/integrate_hierarchy_ftrack.py | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index 02946f813f..5d30b9bf7b 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -234,6 +234,16 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): attr["key"]: attr for attr in hier_custom_attributes } + # Query user entity (for comments) + user = self.session.query( + "User where username is \"{}\"".format(self.session.api_user) + ).first() + if not user: + self.log.warning( + "Was not able to query current User {}".format( + self.session.api_user + ) + ) # Get ftrack api module (as they are different per python version) ftrack_api = self.context.data["ftrackPythonModule"] @@ -364,25 +374,18 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): six.reraise(tp, value, tb) # Create notes. - user = self.session.query( - "User where username is \"{}\"".format(self.session.api_user) - ).first() - if user: - for comment in entity_data.get("comments", []): + entity_comments = entity_data.get("comments") + if user and entity_comments: + for comment in entity_comments: entity.create_note(comment, user) - else: - self.log.warning( - "Was not able to query current User {}".format( - self.session.api_user - ) - ) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) + + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + self.session._configure_locations() + six.reraise(tp, value, tb) # Import children. if 'childs' in entity_data: From 36afd8aa7c3a9a88001c20f6c0ae8c616a2bf51a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Nov 2022 16:36:21 +0100 Subject: [PATCH 74/75] import to ftrack is not recursion based but queue based method --- .../publish/integrate_hierarchy_ftrack.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index 5d30b9bf7b..12e89a1884 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -227,7 +227,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): return output - def import_to_ftrack(self, project_name, input_data, parent=None): + def import_to_ftrack(self, project_name, hierarchy_context): # Prequery hiearchical custom attributes hier_custom_attributes = get_pype_attr(self.session)[1] hier_attr_by_key = { @@ -247,8 +247,17 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): # Get ftrack api module (as they are different per python version) ftrack_api = self.context.data["ftrackPythonModule"] - for entity_name in input_data: - entity_data = input_data[entity_name] + # Use queue of hierarchy items to process + import_queue = collections.deque() + for entity_name, entity_data in hierarchy_context.items(): + import_queue.append( + (entity_name, entity_data, None) + ) + + while import_queue: + item = import_queue.popleft() + entity_name, entity_data, parent = item + entity_type = entity_data['entity_type'] self.log.debug(entity_data) self.log.debug(entity_type) @@ -388,9 +397,14 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): six.reraise(tp, value, tb) # Import children. - if 'childs' in entity_data: - self.import_to_ftrack( - project_name, entity_data['childs'], entity) + children = entity_data.get("childs") + if not children: + continue + + for entity_name, entity_data in children.items(): + import_queue.append( + (entity_name, entity_data, entity) + ) def create_links(self, project_name, entity_data, entity): # Clear existing links. From 5de422dea2c294bcc1ff097c272180b272e89e8a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Nov 2022 16:38:04 +0100 Subject: [PATCH 75/75] change how custom attributes are filled on entities and how entities are created --- .../publish/integrate_hierarchy_ftrack.py | 156 +++++++++--------- 1 file changed, 82 insertions(+), 74 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index 12e89a1884..046dfd9ad8 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -229,10 +229,10 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): def import_to_ftrack(self, project_name, hierarchy_context): # Prequery hiearchical custom attributes - hier_custom_attributes = get_pype_attr(self.session)[1] + hier_attrs = get_pype_attr(self.session)[1] hier_attr_by_key = { attr["key"]: attr - for attr in hier_custom_attributes + for attr in hier_attrs } # Query user entity (for comments) user = self.session.query( @@ -244,6 +244,19 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): self.session.api_user ) ) + + # Query ftrack hierarchy with parenting + ftrack_hierarchy = self.query_ftrack_entitites( + self.session, self.ft_project) + + # Fill ftrack entities to hierarchy context + # - there is no need to query entities again + matching_entities = self.find_matching_ftrack_entities( + hierarchy_context, ftrack_hierarchy) + # Query custom attribute values of each entity + custom_attr_values_by_id = self.query_custom_attribute_values( + self.session, matching_entities, hier_attrs) + # Get ftrack api module (as they are different per python version) ftrack_api = self.context.data["ftrackPythonModule"] @@ -260,75 +273,87 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): entity_type = entity_data['entity_type'] self.log.debug(entity_data) - self.log.debug(entity_type) - if entity_type.lower() == 'project': - entity = self.ft_project - - elif self.ft_project is None or parent is None: + entity = entity_data.get("ft_entity") + if entity is None and entity_type.lower() == "project": raise AssertionError( "Collected items are not in right order!" ) - # try to find if entity already exists - else: - query = ( - 'TypedContext where name is "{0}" and ' - 'project_id is "{1}"' - ).format(entity_name, self.ft_project["id"]) - try: - entity = self.session.query(query).one() - except Exception: - entity = None - # Create entity if not exists if entity is None: - entity = self.create_entity( - name=entity_name, - type=entity_type, - parent=parent - ) + entity = self.session.create(entity_type, { + "name": entity_name, + "parent": parent + }) + entity_data["ft_entity"] = entity + # self.log.info('entity: {}'.format(dict(entity))) # CUSTOM ATTRIBUTES - custom_attributes = entity_data.get('custom_attributes', []) - instances = [ - instance - for instance in self.context - if instance.data.get("asset") == entity["name"] - ] + custom_attributes = entity_data.get('custom_attributes', {}) + instances = [] + for instance in self.context: + instance_asset_name = instance.data.get("asset") + if ( + instance_asset_name + and instance_asset_name.lower() == entity["name"].lower() + ): + instances.append(instance) for instance in instances: instance.data["ftrackEntity"] = entity - for key in custom_attributes: + for key, cust_attr_value in custom_attributes.items(): + if cust_attr_value is None: + continue + hier_attr = hier_attr_by_key.get(key) # Use simple method if key is not hierarchical if not hier_attr: - assert (key in entity['custom_attributes']), ( - 'Missing custom attribute key: `{0}` in attrs: ' - '`{1}`'.format(key, entity['custom_attributes'].keys()) + if key not in entity["custom_attributes"]: + raise KnownPublishError(( + "Missing custom attribute in ftrack with name '{}'" + ).format(key)) + + entity['custom_attributes'][key] = cust_attr_value + continue + + attr_id = hier_attr["id"] + entity_values = custom_attr_values_by_id.get(entity["id"], {}) + # New value is defined by having id in values + # - it can be set to 'None' (ftrack allows that using API) + is_new_value = attr_id not in entity_values + attr_value = entity_values.get(attr_id) + + # Use ftrack operations method to set hiearchical + # attribute value. + # - this is because there may be non hiearchical custom + # attributes with different properties + entity_key = collections.OrderedDict(( + ("configuration_id", hier_attr["id"]), + ("entity_id", entity["id"]) + )) + op = None + if is_new_value: + op = ftrack_api.operation.CreateEntityOperation( + "CustomAttributeValue", + entity_key, + {"value": cust_attr_value} ) - entity['custom_attributes'][key] = custom_attributes[key] - - else: - # Use ftrack operations method to set hiearchical - # attribute value. - # - this is because there may be non hiearchical custom - # attributes with different properties - entity_key = collections.OrderedDict() - entity_key["configuration_id"] = hier_attr["id"] - entity_key["entity_id"] = entity["id"] - self.session.recorded_operations.push( - ftrack_api.operation.UpdateEntityOperation( - "ContextCustomAttributeValue", - entity_key, - "value", - ftrack_api.symbol.NOT_SET, - custom_attributes[key] - ) + elif attr_value != cust_attr_value: + op = ftrack_api.operation.UpdateEntityOperation( + "CustomAttributeValue", + entity_key, + "value", + attr_value, + cust_attr_value ) + if op is not None: + self.session.recorded_operations.push(op) + + if self.session.recorded_operations: try: self.session.commit() except Exception: @@ -342,7 +367,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): for instance in instances: task_name = instance.data.get("task") if task_name: - instances_by_task_name[task_name].append(instance) + instances_by_task_name[task_name.lower()].append(instance) tasks = entity_data.get('tasks', []) existing_tasks = [] @@ -500,21 +525,6 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): return task - def create_entity(self, name, type, parent): - entity = self.session.create(type, { - 'name': name, - 'parent': parent - }) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - return entity - def _get_active_assets(self, context): """ Returns only asset dictionary. Usually the last part of deep dictionary which @@ -536,19 +546,17 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): hierarchy_context = context.data["hierarchyContext"] - active_assets = [] + active_assets = set() # filter only the active publishing insatnces for instance in context: if instance.data.get("publish") is False: continue - if not instance.data.get("asset"): - continue - - active_assets.append(instance.data["asset"]) + asset_name = instance.data.get("asset") + if asset_name: + active_assets.add(asset_name) # remove duplicity in list - active_assets = list(set(active_assets)) - self.log.debug("__ active_assets: {}".format(active_assets)) + self.log.debug("__ active_assets: {}".format(list(active_assets))) return get_pure_hierarchy_data(hierarchy_context)