Merge remote-tracking branch 'pypeclub/release/3.15.x' into feature/OP-3845_nuke-convert-to-new-publisher

This commit is contained in:
Jakub Jezek 2022-12-01 16:10:20 +01:00
commit a51eaa9f86
No known key found for this signature in database
GPG key ID: 730D7C02726179A7
41 changed files with 1325 additions and 879 deletions

View file

@ -1,8 +1,66 @@
# 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**
- 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)
### 📖 Documentation

View file

@ -1,5 +1,99 @@
# 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)
### 📖 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)

View file

@ -57,11 +57,9 @@ 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
"""
staging = False
path = None
# this should match any string complying with https://semver.org/
_VERSION_REGEX = re.compile(r"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>[a-zA-Z\d\-.]*))?(?:\+(?P<buildmetadata>[a-zA-Z\d\-.]*))?") # noqa: E501
@ -83,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"):
@ -113,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}>"
@ -150,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.
@ -218,21 +161,8 @@ 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
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)
if v.build and "staging" in v.build:
openpype_version.staging = True
return openpype_version
def __hash__(self):
return hash(self.path) if self.path else hash(str(self))
@ -382,80 +312,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():
@ -476,14 +354,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(
@ -562,7 +433,6 @@ class OpenPypeVersion(semver.VersionInfo):
@staticmethod
def get_latest_version(
staging: bool = False,
local: bool = None,
remote: bool = None
) -> Union[OpenPypeVersion, None]:
@ -571,7 +441,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
@ -580,7 +449,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.
@ -599,22 +467,9 @@ 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]
@ -705,7 +560,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:
@ -1133,14 +988,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.
@ -1153,9 +1006,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:
@ -1167,37 +1018,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
@ -1217,7 +1056,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.
@ -1231,8 +1069,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.
@ -1280,7 +1116,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
@ -1645,15 +1481,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
@ -1671,8 +1503,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
@ -1695,11 +1526,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)

View file

@ -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 ""

View file

@ -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.
@ -423,20 +422,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)

View file

@ -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;

View file

@ -395,41 +395,84 @@ 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
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{
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);
// 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;
}
}
render_item.render = true; // always set render queue to render
var item = render_item.outputModule(1);
// 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");
}
var file_url = item.file.toString();
return JSON.stringify({
"file_name": file_url,
"width": render_item.comp.width,
"height": render_item.comp.height
})
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:
@ -457,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
@ -480,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)
*/
@ -512,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]);
@ -524,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;
@ -534,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,
@ -545,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);
@ -620,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:
@ -635,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
@ -660,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);
@ -689,30 +732,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(){

View file

@ -418,18 +418,18 @@ 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
(list) of (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:
return records.pop()
return records
def get_audio_url(self, item_id):
""" Get audio layer absolute url for comp
@ -522,7 +522,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 +531,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):

View file

@ -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):
@ -44,46 +47,71 @@ 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)
def create(self, subset_name, data, pre_create_data):
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
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 len(items) > 1:
if not comps:
raise CreatorError(
"Please select only single composition at time."
)
if not items:
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 comp in comps:
if pre_create_data.get("use_composition_name"):
composition_name = comp.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
subset_name = re.sub(r"\{composition\}", '', subset_name,
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"] = [comp.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)
stub.rename_item(comp.id, subset_name)
def get_default_variants(self):
return self._default_variants
@ -94,6 +122,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 +132,18 @@ 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 = {}
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"):

View file

@ -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):
@ -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,9 +83,10 @@ 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")
render_item = render_q[0]
subset_name = inst.data["subset"]
instance = AERenderInstance(
@ -103,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,
@ -115,16 +115,16 @@ 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(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"):
@ -163,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):

View file

@ -21,41 +21,55 @@ 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
render_q = stub.get_render_info()
stub.render(staging_dir)
if not render_q:
# pull file name collected value from Render Queue Output module
if not instance.data["file_names"]:
raise ValueError("No file extension set in Render Queue")
_, 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)
comp_id = instance.data['comp_id']
stub.render(staging_dir, comp_id)
resulting_files = files
if len(files) == 1:
resulting_files = files[0]
representations = []
for file_name in instance.data["file_names"]:
_, ext = os.path.splitext(os.path.basename(file_name))
ext = ext[1:]
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"]
first_file_path = None
files = []
for found_file_name in os.listdir(staging_dir):
if not found_file_name.endswith(ext):
continue
instance.data["representations"] = [repre_data]
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
# 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
}
first_repre = not representations
if instance.data["review"] and first_repre:
repre_data["tags"] = ["review"]
representations.append(repre_data)
instance.data["representations"] = representations
ffmpeg_path = get_ffmpeg_tool_path("ffmpeg")
# Generate thumbnail.

View file

@ -536,6 +536,11 @@ class RenderProductsArnold(ARenderProducts):
products = []
aov_name = self._get_attr(aov, "name")
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,
@ -589,6 +594,7 @@ class RenderProductsArnold(ARenderProducts):
ext=ext,
aov=aov_name,
driver=ai_driver,
multipart=multipart,
camera=camera)
products.append(product)
@ -1016,7 +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"))
multipart = False
force_layer = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) # noqa
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
@ -1044,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.
@ -1075,7 +1084,7 @@ class RenderProductsRedshift(ARenderProducts):
productName=aov_light_group_name,
aov=aov_name,
ext=ext,
multipart=aov_multipart,
multipart=multipart,
camera=camera)
products.append(product)
@ -1089,7 +1098,7 @@ class RenderProductsRedshift(ARenderProducts):
product = RenderProduct(productName=aov_name,
aov=aov_name,
ext=ext,
multipart=aov_multipart,
multipart=multipart,
camera=camera)
products.append(product)
@ -1100,7 +1109,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,

View file

@ -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"

View file

@ -10,7 +10,7 @@ TRAYPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
class TrayPublishAddon(OpenPypeModule, IHostAddon, ITrayAction):
label = "New Publish (beta)"
label = "Publisher"
name = "traypublisher"
host_name = "traypublisher"
@ -19,20 +19,9 @@ class TrayPublishAddon(OpenPypeModule, IHostAddon, ITrayAction):
self.publish_paths = [
os.path.join(TRAYPUBLISH_ROOT_DIR, "plugins", "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(TrayPublishAddon, 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()

View file

@ -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)

View file

@ -57,15 +57,66 @@ 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.
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.
"""
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()
# ----------------------------------------
@ -131,13 +182,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
)
@ -146,9 +195,9 @@ def get_latest_version(staging=None, 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
@ -158,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

View file

@ -494,12 +494,13 @@ 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)
preview = match_aov_pattern(app, aov_patterns, render_file_name)
# toggle preview on if multipart is on
if instance_data.get("multipartExr"):
preview = True
self.log.debug("preview:{}".format(preview))
new_instance = deepcopy(instance_data)
new_instance["subset"] = subset_name
new_instance["subsetGroup"] = group_name
@ -542,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.debug("instances:{}".format(instances))
return instances
def _get_representations(self, instance, exp_files):

View file

@ -14,6 +14,137 @@ from Deadline.Scripting import (
ProcessUtils,
)
VERSION_REGEX = re.compile(
r"(?P<major>0|[1-9]\d*)"
r"\.(?P<minor>0|[1-9]\d*)"
r"\.(?P<patch>0|[1-9]\d*)"
r"(?:-(?P<prerelease>[a-zA-Z\d\-.]*))?"
r"(?:\+(?P<buildmetadata>[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.
@ -21,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")
@ -46,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():
@ -59,6 +192,91 @@ def get_openpype_executable():
return exe_list, dir_list
def get_openpype_versions(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.
@ -68,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))
@ -172,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")
@ -208,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)

View file

@ -7,10 +7,8 @@ import pyblish.api
from openpype.client import get_asset_by_id
from openpype.lib import filter_profiles
from openpype.pipeline import KnownPublishError
# Copy of constant `openpype_modules.ftrack.lib.avalon_sync.CUST_ATTR_AUTO_SYNC`
CUST_ATTR_AUTO_SYNC = "avalon_auto_sync"
CUST_ATTR_GROUP = "openpype"
@ -19,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"
@ -79,120 +76,284 @@ 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)
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 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()
def import_to_ftrack(self, project_name, input_data, parent=None):
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, 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(
"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
)
)
# 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"]
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)
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:
@ -206,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 = []
@ -247,30 +408,28 @@ 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:
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.
@ -366,48 +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 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
@ -429,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)

View file

@ -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)

View file

@ -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
@ -142,13 +148,17 @@ 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"]
repre_thumbnail_path = (
repre.get("published_path") or
os.path.join(repre["stagingDir"], repre["files"])
)
if os.path.exists(repre_thumbnail_path):
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"""
@ -178,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

View file

@ -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"]

View file

@ -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):

View file

@ -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",

View file

@ -288,6 +288,17 @@
"task_types": [],
"tasks": [],
"template_name": "maya2unreal"
},
{
"families": [
"online"
],
"hosts": [
"traypublisher"
],
"task_types": [],
"tasks": [],
"template_name": "online"
}
]
},
@ -404,15 +415,13 @@
"template": "{family}{Task}"
},
{
"families": [
"renderLocal"
],
"families": ["render"],
"hosts": [
"aftereffects"
],
"task_types": [],
"tasks": [],
"template": "render{Task}{Variant}"
"template": "{family}{Task}{Composition}{Variant}"
},
{
"families": [
@ -484,19 +493,7 @@
]
},
"publish": {
"template_name_profiles": [
{
"families": [
"online"
],
"hosts": [
"traypublisher"
],
"task_types": [],
"task_names": [],
"template_name": "online"
}
],
"template_name_profiles": [],
"hero_template_name_profiles": []
}
},

View file

@ -195,7 +195,7 @@
"enabled": true
},
"standalonepublish_tool": {
"enabled": true
"enabled": false
},
"project_manager": {
"enabled": true

View file

@ -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",
)

View file

@ -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)

View file

@ -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"
},

View file

@ -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,19 +588,9 @@ 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"
_production_versions_key = "production_versions"
_staging_versions_key = "staging_versions"
def __init__(self):
# Get mongo connection
@ -605,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)
@ -638,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.
@ -654,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
@ -698,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
@ -725,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
@ -767,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(
{
@ -997,10 +1031,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 +1043,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 +1064,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,
@ -1298,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(

View file

@ -1040,6 +1040,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.

View file

@ -92,11 +92,6 @@ class ExperimentalTools:
hosts_filter=["blender", "maya", "nuke", "celaction", "flame",
"fusion", "harmony", "hiero", "resolve",
"tvpaint", "unreal"]
),
ExperimentalTool(
"traypublisher",
"New Standalone Publisher",
"Standalone publisher using new publisher. Requires restart"
)
]

View file

@ -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,
@ -21,8 +21,9 @@ 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,
is_running_staging,
is_staging_enabled,
)
from openpype.modules import TrayModulesManager
from openpype.settings import (
@ -202,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.
@ -462,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:
@ -562,9 +629,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)
@ -573,31 +638,33 @@ 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
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
# Pop OPENPYPE_VERSION
if reset_version:
# Add staging flag if was running from staging
if is_running_staging():
args.append("--use-staging")
kwargs["env"].pop("OPENPYPE_VERSION", None)
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)
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, env=envs)
self.exit()
def exit(self):

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.14.7-nightly.6"
__version__ = "3.14.7"

117
start.py
View file

@ -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
@ -516,8 +518,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:
@ -544,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")
@ -570,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:
@ -682,8 +678,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 +693,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 +706,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 +719,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 +791,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 +815,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 +891,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 +907,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 +915,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 +930,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)
@ -997,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:
@ -1005,7 +969,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 +1022,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 +1035,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 +1051,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 +1135,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 +1157,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 "<N/A>"))
inf.append((" - database", Logger.log_database_name))
inf.append((" - collection", Logger.log_collection_name))
inf.append((" - user", mongo_components["username"] or "<N/A>"))
inf.extend([
("Logging to MongoDB", mongo_components["host"]),
(" - port", mongo_components["port"] or "<N/A>"),
(" - database", Logger.log_database_name),
(" - collection", Logger.log_collection_name),
(" - user", mongo_components["username"] or "<N/A>")
])
if mongo_components["auth_db"]:
inf.append((" - auth source", mongo_components["auth_db"]))

View file

@ -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"

View file

@ -54,14 +54,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.

View file

@ -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

View file

@ -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:

View file

@ -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,9 @@ 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 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`.