mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge branch 'develop' into bugfix/ocio-v2-aces1.3-display-resolving-error
This commit is contained in:
commit
bd48482746
13 changed files with 949 additions and 161 deletions
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,7 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to AYON Tray
|
||||
options:
|
||||
- 1.6.0
|
||||
- 1.5.3
|
||||
- 1.5.2
|
||||
- 1.5.1
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class OCIOEnvHook(PreLaunchHook):
|
|||
"fusion",
|
||||
"blender",
|
||||
"aftereffects",
|
||||
"3dsmax",
|
||||
"max",
|
||||
"houdini",
|
||||
"maya",
|
||||
"nuke",
|
||||
|
|
|
|||
630
client/ayon_core/plugins/load/create_hero_version.py
Normal file
630
client/ayon_core/plugins/load/create_hero_version.py
Normal file
|
|
@ -0,0 +1,630 @@
|
|||
"""Plugin to create hero version from selected context."""
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import copy
|
||||
import shutil
|
||||
import errno
|
||||
import itertools
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Any, Optional
|
||||
|
||||
from speedcopy import copyfile
|
||||
import clique
|
||||
import ayon_api
|
||||
from ayon_api.operations import OperationsSession, new_version_entity
|
||||
from ayon_api.utils import create_entity_id
|
||||
from qtpy import QtWidgets, QtCore
|
||||
from ayon_core import style
|
||||
from ayon_core.pipeline import load, Anatomy
|
||||
from ayon_core.lib import create_hard_link, source_hash, StringTemplate
|
||||
from ayon_core.lib.file_transaction import wait_for_future_errors
|
||||
from ayon_core.pipeline.publish import get_publish_template_name
|
||||
from ayon_core.pipeline.template_data import get_template_data
|
||||
|
||||
|
||||
def prepare_changes(old_entity: dict, new_entity: dict) -> dict:
|
||||
"""Prepare changes dict for update entity operation.
|
||||
|
||||
Args:
|
||||
old_entity (dict): Existing entity data from database.
|
||||
new_entity (dict): New entity data to compare against old.
|
||||
|
||||
Returns:
|
||||
dict: Changes to apply to old entity to make it like new entity.
|
||||
|
||||
"""
|
||||
changes = {}
|
||||
for key in set(new_entity.keys()):
|
||||
if key == "attrib":
|
||||
continue
|
||||
if key in new_entity and new_entity[key] != old_entity.get(key):
|
||||
changes[key] = new_entity[key]
|
||||
attrib_changes = {}
|
||||
if "attrib" in new_entity:
|
||||
for key, value in new_entity["attrib"].items():
|
||||
if value != old_entity["attrib"].get(key):
|
||||
attrib_changes[key] = value
|
||||
if attrib_changes:
|
||||
changes["attrib"] = attrib_changes
|
||||
return changes
|
||||
|
||||
|
||||
class CreateHeroVersion(load.ProductLoaderPlugin):
|
||||
"""Create hero version from selected context."""
|
||||
|
||||
is_multiple_contexts_compatible = False
|
||||
representations = {"*"}
|
||||
product_types = {"*"}
|
||||
label = "Create Hero Version"
|
||||
order = 36
|
||||
icon = "star"
|
||||
color = "#ffd700"
|
||||
|
||||
ignored_representation_names: list[str] = []
|
||||
db_representation_context_keys = [
|
||||
"project", "folder", "asset", "hierarchy", "task", "product",
|
||||
"subset", "family", "representation", "username", "user", "output"
|
||||
]
|
||||
use_hardlinks = False
|
||||
|
||||
@staticmethod
|
||||
def message(text: str) -> None:
|
||||
"""Show message box with text."""
|
||||
msgBox = QtWidgets.QMessageBox()
|
||||
msgBox.setText(text)
|
||||
msgBox.setStyleSheet(style.load_stylesheet())
|
||||
msgBox.setWindowFlags(
|
||||
msgBox.windowFlags() | QtCore.Qt.WindowType.FramelessWindowHint
|
||||
)
|
||||
msgBox.exec_()
|
||||
|
||||
def load(self, context, name=None, namespace=None, options=None) -> None:
|
||||
"""Load hero version from context (dict as in context.py)."""
|
||||
success = True
|
||||
errors = []
|
||||
|
||||
# Extract project, product, version, folder from context
|
||||
project = context.get("project")
|
||||
product = context.get("product")
|
||||
version = context.get("version")
|
||||
folder = context.get("folder")
|
||||
task_entity = ayon_api.get_task_by_id(
|
||||
task_id=version.get("taskId"), project_name=project["name"]
|
||||
)
|
||||
|
||||
anatomy = Anatomy(project["name"])
|
||||
|
||||
version_id = version["id"]
|
||||
project_name = project["name"]
|
||||
repres = list(
|
||||
ayon_api.get_representations(
|
||||
project_name, version_ids={version_id}
|
||||
)
|
||||
)
|
||||
anatomy_data = get_template_data(
|
||||
project_entity=project,
|
||||
folder_entity=folder,
|
||||
task_entity=task_entity,
|
||||
)
|
||||
anatomy_data["product"] = {
|
||||
"name": product["name"],
|
||||
"type": product["productType"],
|
||||
}
|
||||
anatomy_data["version"] = version["version"]
|
||||
published_representations = {}
|
||||
for repre in repres:
|
||||
repre_anatomy = copy.deepcopy(anatomy_data)
|
||||
if "ext" not in repre_anatomy:
|
||||
repre_anatomy["ext"] = repre.get("context", {}).get("ext", "")
|
||||
published_representations[repre["id"]] = {
|
||||
"representation": repre,
|
||||
"published_files": [f["path"] for f in repre.get("files", [])],
|
||||
"anatomy_data": repre_anatomy
|
||||
}
|
||||
# get the publish directory
|
||||
publish_template_key = get_publish_template_name(
|
||||
project_name,
|
||||
context.get("hostName"),
|
||||
product["productType"],
|
||||
task_name=anatomy_data.get("task", {}).get("name"),
|
||||
task_type=anatomy_data.get("task", {}).get("type"),
|
||||
project_settings=context.get("project_settings", {}),
|
||||
logger=self.log
|
||||
)
|
||||
published_template_obj = anatomy.get_template_item(
|
||||
"publish", publish_template_key, "directory"
|
||||
)
|
||||
published_dir = os.path.normpath(
|
||||
published_template_obj.format_strict(anatomy_data)
|
||||
)
|
||||
instance_data = {
|
||||
"productName": product["name"],
|
||||
"productType": product["productType"],
|
||||
"anatomyData": anatomy_data,
|
||||
"publishDir": published_dir,
|
||||
"published_representations": published_representations,
|
||||
"versionEntity": version,
|
||||
}
|
||||
|
||||
try:
|
||||
self.create_hero_version(instance_data, anatomy, context)
|
||||
except Exception as exc:
|
||||
success = False
|
||||
errors.append(str(exc))
|
||||
if success:
|
||||
self.message("Hero version created successfully.")
|
||||
else:
|
||||
self.message(
|
||||
f"Failed to create hero version:\n{chr(10).join(errors)}")
|
||||
|
||||
def create_hero_version(
|
||||
self,
|
||||
instance_data: dict[str, Any],
|
||||
anatomy: Anatomy,
|
||||
context: dict[str, Any]) -> None:
|
||||
"""Create hero version from instance data.
|
||||
|
||||
Args:
|
||||
instance_data (dict): Instance data with keys:
|
||||
- productName (str): Name of the product.
|
||||
- productType (str): Type of the product.
|
||||
- anatomyData (dict): Anatomy data for templates.
|
||||
- publishDir (str): Directory where the product is published.
|
||||
- published_representations (dict): Published representations.
|
||||
- versionEntity (dict, optional): Source version entity.
|
||||
anatomy (Anatomy): Anatomy object for the project.
|
||||
context (dict): Context data with keys:
|
||||
- hostName (str): Name of the host application.
|
||||
- project_settings (dict): Project settings.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If any required data is missing or an error occurs
|
||||
during the hero version creation process.
|
||||
|
||||
"""
|
||||
published_repres = instance_data.get("published_representations")
|
||||
if not published_repres:
|
||||
raise RuntimeError("No published representations found.")
|
||||
|
||||
project_name = anatomy.project_name
|
||||
template_key = get_publish_template_name(
|
||||
project_name,
|
||||
context.get("hostName"),
|
||||
instance_data.get("productType"),
|
||||
instance_data.get("anatomyData", {}).get("task", {}).get("name"),
|
||||
instance_data.get("anatomyData", {}).get("task", {}).get("type"),
|
||||
project_settings=context.get("project_settings", {}),
|
||||
hero=True,
|
||||
)
|
||||
hero_template = anatomy.get_template_item(
|
||||
"hero", template_key, "path", default=None
|
||||
)
|
||||
if hero_template is None:
|
||||
raise RuntimeError("Project anatomy does not have hero "
|
||||
f"template key: {template_key}")
|
||||
|
||||
self.log.info(f"Hero template: {hero_template.template}")
|
||||
|
||||
hero_publish_dir = self.get_publish_dir(
|
||||
instance_data, anatomy, template_key
|
||||
)
|
||||
|
||||
self.log.info(f"Hero publish dir: {hero_publish_dir}")
|
||||
|
||||
src_version_entity = instance_data.get("versionEntity")
|
||||
filtered_repre_ids = []
|
||||
for repre_id, repre_info in published_repres.items():
|
||||
repre = repre_info["representation"]
|
||||
if repre["name"].lower() in self.ignored_representation_names:
|
||||
filtered_repre_ids.append(repre_id)
|
||||
for repre_id in filtered_repre_ids:
|
||||
published_repres.pop(repre_id, None)
|
||||
if not published_repres:
|
||||
raise RuntimeError(
|
||||
"All published representations were filtered by name."
|
||||
)
|
||||
|
||||
if src_version_entity is None:
|
||||
src_version_entity = self.version_from_representations(
|
||||
project_name, published_repres)
|
||||
if not src_version_entity:
|
||||
raise RuntimeError("Can't find origin version in database.")
|
||||
if src_version_entity["version"] == 0:
|
||||
raise RuntimeError("Version 0 cannot have hero version.")
|
||||
|
||||
all_copied_files = []
|
||||
transfers = instance_data.get("transfers", [])
|
||||
for _src, dst in transfers:
|
||||
dst = os.path.normpath(dst)
|
||||
if dst not in all_copied_files:
|
||||
all_copied_files.append(dst)
|
||||
hardlinks = instance_data.get("hardlinks", [])
|
||||
for _src, dst in hardlinks:
|
||||
dst = os.path.normpath(dst)
|
||||
if dst not in all_copied_files:
|
||||
all_copied_files.append(dst)
|
||||
|
||||
all_repre_file_paths = []
|
||||
for repre_info in published_repres.values():
|
||||
published_files = repre_info.get("published_files") or []
|
||||
for file_path in published_files:
|
||||
file_path = os.path.normpath(file_path)
|
||||
if file_path not in all_repre_file_paths:
|
||||
all_repre_file_paths.append(file_path)
|
||||
|
||||
publish_dir = instance_data.get("publishDir", "")
|
||||
if not publish_dir:
|
||||
raise RuntimeError(
|
||||
"publishDir is empty in instance_data, cannot continue."
|
||||
)
|
||||
instance_publish_dir = os.path.normpath(publish_dir)
|
||||
other_file_paths_mapping = []
|
||||
for file_path in all_copied_files:
|
||||
if not file_path.startswith(instance_publish_dir):
|
||||
continue
|
||||
if file_path in all_repre_file_paths:
|
||||
continue
|
||||
dst_filepath = file_path.replace(
|
||||
instance_publish_dir, hero_publish_dir
|
||||
)
|
||||
other_file_paths_mapping.append((file_path, dst_filepath))
|
||||
|
||||
old_version, old_repres = self.current_hero_ents(
|
||||
project_name, src_version_entity
|
||||
)
|
||||
inactive_old_repres_by_name = {}
|
||||
old_repres_by_name = {}
|
||||
for repre in old_repres:
|
||||
low_name = repre["name"].lower()
|
||||
if repre["active"]:
|
||||
old_repres_by_name[low_name] = repre
|
||||
else:
|
||||
inactive_old_repres_by_name[low_name] = repre
|
||||
|
||||
op_session = OperationsSession()
|
||||
entity_id = old_version["id"] if old_version else None
|
||||
new_hero_version = new_version_entity(
|
||||
-src_version_entity["version"],
|
||||
src_version_entity["productId"],
|
||||
task_id=src_version_entity.get("taskId"),
|
||||
data=copy.deepcopy(src_version_entity["data"]),
|
||||
attribs=copy.deepcopy(src_version_entity["attrib"]),
|
||||
entity_id=entity_id,
|
||||
)
|
||||
if old_version:
|
||||
update_data = prepare_changes(old_version, new_hero_version)
|
||||
op_session.update_entity(
|
||||
project_name, "version", old_version["id"], update_data
|
||||
)
|
||||
else:
|
||||
op_session.create_entity(project_name, "version", new_hero_version)
|
||||
|
||||
# Store hero entity to instance_data
|
||||
instance_data["heroVersionEntity"] = new_hero_version
|
||||
|
||||
old_repres_to_replace = {}
|
||||
for repre_info in published_repres.values():
|
||||
repre = repre_info["representation"]
|
||||
repre_name_low = repre["name"].lower()
|
||||
if repre_name_low in old_repres_by_name:
|
||||
old_repres_to_replace[repre_name_low] = (
|
||||
old_repres_by_name.pop(repre_name_low)
|
||||
)
|
||||
old_repres_to_delete = old_repres_by_name or {}
|
||||
backup_hero_publish_dir = None
|
||||
if os.path.exists(hero_publish_dir):
|
||||
base_backup_dir = f"{hero_publish_dir}.BACKUP"
|
||||
max_idx = 10
|
||||
# Find the first available backup directory name
|
||||
for idx in range(max_idx + 1):
|
||||
if idx == 0:
|
||||
candidate_backup_dir = base_backup_dir
|
||||
else:
|
||||
candidate_backup_dir = f"{base_backup_dir}{idx}"
|
||||
if not os.path.exists(candidate_backup_dir):
|
||||
backup_hero_publish_dir = candidate_backup_dir
|
||||
break
|
||||
else:
|
||||
raise AssertionError(
|
||||
f"Backup folders are fully occupied to max index {max_idx}"
|
||||
)
|
||||
|
||||
try:
|
||||
os.rename(hero_publish_dir, backup_hero_publish_dir)
|
||||
except PermissionError as e:
|
||||
raise AssertionError(
|
||||
"Could not create hero version because it is "
|
||||
"not possible to replace current hero files."
|
||||
) from e
|
||||
|
||||
try:
|
||||
src_to_dst_file_paths = []
|
||||
repre_integrate_data = []
|
||||
path_template_obj = anatomy.get_template_item(
|
||||
"hero", template_key, "path")
|
||||
anatomy_root = {"root": anatomy.roots}
|
||||
for repre_info in published_repres.values():
|
||||
published_files = repre_info["published_files"]
|
||||
if len(published_files) == 0:
|
||||
continue
|
||||
anatomy_data = copy.deepcopy(repre_info["anatomy_data"])
|
||||
anatomy_data.pop("version", None)
|
||||
template_filled = path_template_obj.format_strict(anatomy_data)
|
||||
repre_context = template_filled.used_values
|
||||
for key in self.db_representation_context_keys:
|
||||
value = anatomy_data.get(key)
|
||||
if value is not None:
|
||||
repre_context[key] = value
|
||||
repre_entity = copy.deepcopy(repre_info["representation"])
|
||||
repre_entity.pop("id", None)
|
||||
repre_entity["versionId"] = new_hero_version["id"]
|
||||
repre_entity["context"] = repre_context
|
||||
repre_entity["attrib"] = {
|
||||
"path": str(template_filled),
|
||||
"template": hero_template.template
|
||||
}
|
||||
dst_paths = []
|
||||
|
||||
if len(published_files) == 1:
|
||||
dst_paths.append(str(template_filled))
|
||||
mapped_published_file = StringTemplate(
|
||||
published_files[0]).format_strict(
|
||||
anatomy_root
|
||||
)
|
||||
src_to_dst_file_paths.append(
|
||||
(mapped_published_file, template_filled)
|
||||
)
|
||||
self.log.info(
|
||||
f"Single published file: {mapped_published_file} -> "
|
||||
f"{template_filled}"
|
||||
)
|
||||
else:
|
||||
collections, remainders = clique.assemble(published_files)
|
||||
if remainders or not collections or len(collections) > 1:
|
||||
raise RuntimeError(
|
||||
(
|
||||
"Integrity error. Files of published "
|
||||
"representation is combination of frame "
|
||||
"collections and single files."
|
||||
)
|
||||
)
|
||||
src_col = collections[0]
|
||||
frame_splitter = "_-_FRAME_SPLIT_-_"
|
||||
anatomy_data["frame"] = frame_splitter
|
||||
_template_filled = path_template_obj.format_strict(
|
||||
anatomy_data
|
||||
)
|
||||
head, tail = _template_filled.split(frame_splitter)
|
||||
padding = anatomy.templates_obj.frame_padding
|
||||
dst_col = clique.Collection(
|
||||
head=head, padding=padding, tail=tail
|
||||
)
|
||||
dst_col.indexes.clear()
|
||||
dst_col.indexes.update(src_col.indexes)
|
||||
for src_file, dst_file in zip(src_col, dst_col):
|
||||
src_file = StringTemplate(src_file).format_strict(
|
||||
anatomy_root
|
||||
)
|
||||
src_to_dst_file_paths.append((src_file, dst_file))
|
||||
dst_paths.append(dst_file)
|
||||
self.log.info(
|
||||
f"Collection published file: {src_file} "
|
||||
f"-> {dst_file}"
|
||||
)
|
||||
repre_integrate_data.append((repre_entity, dst_paths))
|
||||
|
||||
# Copy files
|
||||
with ThreadPoolExecutor(max_workers=8) as executor:
|
||||
futures = [
|
||||
executor.submit(self.copy_file, src_path, dst_path)
|
||||
for src_path, dst_path in itertools.chain(
|
||||
src_to_dst_file_paths, other_file_paths_mapping)
|
||||
]
|
||||
wait_for_future_errors(executor, futures)
|
||||
|
||||
# Update/create representations
|
||||
for repre_entity, dst_paths in repre_integrate_data:
|
||||
repre_files = self.get_files_info(dst_paths, anatomy)
|
||||
repre_entity["files"] = repre_files
|
||||
repre_name_low = repre_entity["name"].lower()
|
||||
if repre_name_low in old_repres_to_replace:
|
||||
old_repre = old_repres_to_replace.pop(repre_name_low)
|
||||
repre_entity["id"] = old_repre["id"]
|
||||
update_data = prepare_changes(old_repre, repre_entity)
|
||||
op_session.update_entity(
|
||||
project_name,
|
||||
"representation",
|
||||
old_repre["id"],
|
||||
update_data
|
||||
)
|
||||
elif repre_name_low in inactive_old_repres_by_name:
|
||||
inactive_repre = inactive_old_repres_by_name.pop(
|
||||
repre_name_low
|
||||
)
|
||||
repre_entity["id"] = inactive_repre["id"]
|
||||
update_data = prepare_changes(inactive_repre, repre_entity)
|
||||
op_session.update_entity(
|
||||
project_name,
|
||||
"representation",
|
||||
inactive_repre["id"],
|
||||
update_data
|
||||
)
|
||||
else:
|
||||
op_session.create_entity(
|
||||
project_name,
|
||||
"representation",
|
||||
repre_entity
|
||||
)
|
||||
|
||||
for repre in old_repres_to_delete.values():
|
||||
op_session.update_entity(
|
||||
project_name,
|
||||
"representation",
|
||||
repre["id"],
|
||||
{"active": False}
|
||||
)
|
||||
|
||||
op_session.commit()
|
||||
|
||||
if backup_hero_publish_dir is not None and os.path.exists(
|
||||
backup_hero_publish_dir
|
||||
):
|
||||
shutil.rmtree(backup_hero_publish_dir)
|
||||
|
||||
except Exception:
|
||||
if backup_hero_publish_dir is not None and os.path.exists(
|
||||
backup_hero_publish_dir):
|
||||
if os.path.exists(hero_publish_dir):
|
||||
shutil.rmtree(hero_publish_dir)
|
||||
os.rename(backup_hero_publish_dir, hero_publish_dir)
|
||||
raise
|
||||
|
||||
def get_files_info(
|
||||
self, filepaths: list[str], anatomy: Anatomy) -> list[dict]:
|
||||
"""Get list of file info dictionaries for given file paths.
|
||||
|
||||
Args:
|
||||
filepaths (list[str]): List of absolute file paths.
|
||||
anatomy (Anatomy): Anatomy object for the project.
|
||||
|
||||
Returns:
|
||||
list[dict]: List of file info dictionaries.
|
||||
|
||||
"""
|
||||
file_infos = []
|
||||
for filepath in filepaths:
|
||||
file_info = self.prepare_file_info(filepath, anatomy)
|
||||
file_infos.append(file_info)
|
||||
return file_infos
|
||||
|
||||
def prepare_file_info(self, path: str, anatomy: Anatomy) -> dict:
|
||||
"""Prepare file info dictionary for given path.
|
||||
|
||||
Args:
|
||||
path (str): Absolute file path.
|
||||
anatomy (Anatomy): Anatomy object for the project.
|
||||
|
||||
Returns:
|
||||
dict: File info dictionary with keys:
|
||||
- id (str): Unique identifier for the file.
|
||||
- name (str): Base name of the file.
|
||||
- path (str): Rootless file path.
|
||||
- size (int): Size of the file in bytes.
|
||||
- hash (str): Hash of the file content.
|
||||
- hash_type (str): Type of the hash used.
|
||||
|
||||
"""
|
||||
return {
|
||||
"id": create_entity_id(),
|
||||
"name": os.path.basename(path),
|
||||
"path": self.get_rootless_path(anatomy, path),
|
||||
"size": os.path.getsize(path),
|
||||
"hash": source_hash(path),
|
||||
"hash_type": "op3",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_publish_dir(
|
||||
instance_data: dict,
|
||||
anatomy: Anatomy,
|
||||
template_key: str) -> str:
|
||||
"""Get publish directory from instance data and anatomy.
|
||||
|
||||
Args:
|
||||
instance_data (dict): Instance data with "anatomyData" key.
|
||||
anatomy (Anatomy): Anatomy object for the project.
|
||||
template_key (str): Template key for the hero template.
|
||||
|
||||
Returns:
|
||||
str: Normalized publish directory path.
|
||||
|
||||
"""
|
||||
template_data = copy.deepcopy(instance_data.get("anatomyData", {}))
|
||||
if "originalBasename" in instance_data:
|
||||
template_data["originalBasename"] = (
|
||||
instance_data["originalBasename"]
|
||||
)
|
||||
template_obj = anatomy.get_template_item(
|
||||
"hero", template_key, "directory"
|
||||
)
|
||||
return os.path.normpath(template_obj.format_strict(template_data))
|
||||
|
||||
@staticmethod
|
||||
def get_rootless_path(anatomy: Anatomy, path: str) -> str:
|
||||
"""Get rootless path from absolute path.
|
||||
|
||||
Args:
|
||||
anatomy (Anatomy): Anatomy object for the project.
|
||||
path (str): Absolute file path.
|
||||
|
||||
Returns:
|
||||
str: Rootless file path if root found, else original path.
|
||||
|
||||
"""
|
||||
success, rootless_path = anatomy.find_root_template_from_path(path)
|
||||
if success:
|
||||
path = rootless_path
|
||||
return path
|
||||
|
||||
def copy_file(self, src_path: str, dst_path: str) -> None:
|
||||
"""Copy file from src to dst with creating directories.
|
||||
|
||||
Args:
|
||||
src_path (str): Source file path.
|
||||
dst_path (str): Destination file path.
|
||||
|
||||
Raises:
|
||||
OSError: If copying or linking fails.
|
||||
|
||||
"""
|
||||
dirname = os.path.dirname(dst_path)
|
||||
try:
|
||||
os.makedirs(dirname)
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.EEXIST:
|
||||
raise
|
||||
if self.use_hardlinks:
|
||||
try:
|
||||
create_hard_link(src_path, dst_path)
|
||||
return
|
||||
except OSError as exc:
|
||||
if exc.errno not in [errno.EXDEV, errno.EINVAL]:
|
||||
raise
|
||||
copyfile(src_path, dst_path)
|
||||
|
||||
@staticmethod
|
||||
def version_from_representations(
|
||||
project_name: str, repres: dict) -> Optional[dict[str, Any]]:
|
||||
"""Find version from representations.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of the project.
|
||||
repres (dict): Dictionary of representations info.
|
||||
|
||||
Returns:
|
||||
Optional[dict]: Version entity if found, else None.
|
||||
|
||||
"""
|
||||
for repre_info in repres.values():
|
||||
version = ayon_api.get_version_by_id(
|
||||
project_name, repre_info["representation"]["versionId"]
|
||||
)
|
||||
if version:
|
||||
return version
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def current_hero_ents(
|
||||
project_name: str,
|
||||
version: dict[str, Any]) -> tuple[Any, list[dict[str, Any]]]:
|
||||
hero_version = ayon_api.get_hero_version_by_product_id(
|
||||
project_name, version["productId"]
|
||||
)
|
||||
if not hero_version:
|
||||
return None, []
|
||||
hero_repres = list(
|
||||
ayon_api.get_representations(
|
||||
project_name, version_ids={hero_version["id"]}
|
||||
)
|
||||
)
|
||||
return hero_version, hero_repres
|
||||
|
|
@ -6,15 +6,15 @@ from ayon_core.pipeline import load
|
|||
from ayon_core.pipeline.load import LoadError
|
||||
|
||||
|
||||
class PushToLibraryProject(load.ProductLoaderPlugin):
|
||||
"""Export selected versions to folder structure from Template"""
|
||||
class PushToProject(load.ProductLoaderPlugin):
|
||||
"""Export selected versions to different project"""
|
||||
|
||||
is_multiple_contexts_compatible = True
|
||||
|
||||
representations = {"*"}
|
||||
product_types = {"*"}
|
||||
|
||||
label = "Push to Library project"
|
||||
label = "Push to project"
|
||||
order = 35
|
||||
icon = "send"
|
||||
color = "#d8d8d8"
|
||||
|
|
@ -28,10 +28,12 @@ class PushToLibraryProject(load.ProductLoaderPlugin):
|
|||
if not filtered_contexts:
|
||||
raise LoadError("Nothing to push for your selection")
|
||||
|
||||
if len(filtered_contexts) > 1:
|
||||
raise LoadError("Please select only one item")
|
||||
|
||||
context = tuple(filtered_contexts)[0]
|
||||
folder_ids = set(
|
||||
context["folder"]["id"]
|
||||
for context in filtered_contexts
|
||||
)
|
||||
if len(folder_ids) > 1:
|
||||
raise LoadError("Please select products from single folder")
|
||||
|
||||
push_tool_script_path = os.path.join(
|
||||
AYON_CORE_ROOT,
|
||||
|
|
@ -39,14 +41,16 @@ class PushToLibraryProject(load.ProductLoaderPlugin):
|
|||
"push_to_project",
|
||||
"main.py"
|
||||
)
|
||||
project_name = filtered_contexts[0]["project"]["name"]
|
||||
|
||||
project_name = context["project"]["name"]
|
||||
version_id = context["version"]["id"]
|
||||
version_ids = {
|
||||
context["version"]["id"]
|
||||
for context in filtered_contexts
|
||||
}
|
||||
|
||||
args = get_ayon_launcher_args(
|
||||
"run",
|
||||
push_tool_script_path,
|
||||
"--project", project_name,
|
||||
"--version", version_id
|
||||
"--versions", ",".join(version_ids)
|
||||
)
|
||||
run_detached_process(args)
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import threading
|
||||
from typing import Dict
|
||||
|
||||
import ayon_api
|
||||
|
||||
|
|
@ -13,10 +14,11 @@ from .models import (
|
|||
UserPublishValuesModel,
|
||||
IntegrateModel,
|
||||
)
|
||||
from .models.integrate import ProjectPushItemProcess
|
||||
|
||||
|
||||
class PushToContextController:
|
||||
def __init__(self, project_name=None, version_id=None):
|
||||
def __init__(self, project_name=None, version_ids=None):
|
||||
self._event_system = self._create_event_system()
|
||||
|
||||
self._projects_model = ProjectsModel(self)
|
||||
|
|
@ -27,18 +29,20 @@ class PushToContextController:
|
|||
self._user_values = UserPublishValuesModel(self)
|
||||
|
||||
self._src_project_name = None
|
||||
self._src_version_id = None
|
||||
self._src_version_ids = []
|
||||
self._src_folder_entity = None
|
||||
self._src_folder_task_entities = {}
|
||||
self._src_product_entity = None
|
||||
self._src_version_entity = None
|
||||
self._src_version_entities = []
|
||||
self._src_product_entities = {}
|
||||
self._src_label = None
|
||||
|
||||
self._submission_enabled = False
|
||||
self._process_thread = None
|
||||
self._process_item_id = None
|
||||
|
||||
self.set_source(project_name, version_id)
|
||||
self._use_original_name = False
|
||||
|
||||
self.set_source(project_name, version_ids)
|
||||
|
||||
# Events system
|
||||
def emit_event(self, topic, data=None, source=None):
|
||||
|
|
@ -51,38 +55,47 @@ class PushToContextController:
|
|||
def register_event_callback(self, topic, callback):
|
||||
self._event_system.add_callback(topic, callback)
|
||||
|
||||
def set_source(self, project_name, version_id):
|
||||
def set_source(self, project_name, version_ids):
|
||||
"""Set source project and version.
|
||||
|
||||
There is currently assumption that tool is working on products of same
|
||||
folder.
|
||||
|
||||
Args:
|
||||
project_name (Union[str, None]): Source project name.
|
||||
version_id (Union[str, None]): Source version id.
|
||||
version_ids (Optional[list[str]]): Version ids.
|
||||
"""
|
||||
|
||||
if not project_name or not version_ids:
|
||||
return
|
||||
if (
|
||||
project_name == self._src_project_name
|
||||
and version_id == self._src_version_id
|
||||
and version_ids == self._src_version_ids
|
||||
):
|
||||
return
|
||||
|
||||
self._src_project_name = project_name
|
||||
self._src_version_id = version_id
|
||||
self._src_version_ids = version_ids
|
||||
self._src_label = None
|
||||
folder_entity = None
|
||||
task_entities = {}
|
||||
product_entity = None
|
||||
version_entity = None
|
||||
if project_name and version_id:
|
||||
version_entity = ayon_api.get_version_by_id(
|
||||
project_name, version_id
|
||||
product_entities = []
|
||||
version_entities = []
|
||||
if project_name and self._src_version_ids:
|
||||
version_entities = list(ayon_api.get_versions(
|
||||
project_name, version_ids=self._src_version_ids))
|
||||
|
||||
if version_entities:
|
||||
product_ids = [
|
||||
version_entity["productId"]
|
||||
for version_entity in version_entities
|
||||
]
|
||||
product_entities = list(ayon_api.get_products(
|
||||
project_name, product_ids=product_ids)
|
||||
)
|
||||
|
||||
if version_entity:
|
||||
product_entity = ayon_api.get_product_by_id(
|
||||
project_name, version_entity["productId"]
|
||||
)
|
||||
|
||||
if product_entity:
|
||||
if product_entities:
|
||||
# all products for same folder
|
||||
product_entity = product_entities[0]
|
||||
folder_entity = ayon_api.get_folder_by_id(
|
||||
project_name, product_entity["folderId"]
|
||||
)
|
||||
|
|
@ -97,15 +110,18 @@ class PushToContextController:
|
|||
|
||||
self._src_folder_entity = folder_entity
|
||||
self._src_folder_task_entities = task_entities
|
||||
self._src_product_entity = product_entity
|
||||
self._src_version_entity = version_entity
|
||||
self._src_version_entities = version_entities
|
||||
self._src_product_entities = {
|
||||
product["id"]: product
|
||||
for product in product_entities
|
||||
}
|
||||
if folder_entity:
|
||||
self._user_values.set_new_folder_name(folder_entity["name"])
|
||||
variant = self._get_src_variant()
|
||||
if variant:
|
||||
self._user_values.set_variant(variant)
|
||||
|
||||
comment = version_entity["attrib"].get("comment")
|
||||
comment = version_entities[0]["attrib"].get("comment")
|
||||
if comment:
|
||||
self._user_values.set_comment(comment)
|
||||
|
||||
|
|
@ -113,7 +129,7 @@ class PushToContextController:
|
|||
"source.changed",
|
||||
{
|
||||
"project_name": project_name,
|
||||
"version_id": version_id
|
||||
"version_ids": self._src_version_ids
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -142,6 +158,14 @@ class PushToContextController:
|
|||
def get_user_values(self):
|
||||
return self._user_values.get_data()
|
||||
|
||||
def original_names_required(self):
|
||||
"""Checks if original product names must be used.
|
||||
|
||||
Currently simple check if multiple versions, but if multiple products
|
||||
with different product_type were used, it wouldn't be necessary.
|
||||
"""
|
||||
return len(self._src_version_entities) > 1
|
||||
|
||||
def set_user_value_folder_name(self, folder_name):
|
||||
self._user_values.set_new_folder_name(folder_name)
|
||||
self._invalidate()
|
||||
|
|
@ -165,8 +189,9 @@ class PushToContextController:
|
|||
def set_selected_task(self, task_id, task_name):
|
||||
self._selection_model.set_selected_task(task_id, task_name)
|
||||
|
||||
def get_process_item_status(self, item_id):
|
||||
return self._integrate_model.get_item_status(item_id)
|
||||
def get_process_items(self) -> Dict[str, ProjectPushItemProcess]:
|
||||
"""Returns dict of all ProjectPushItemProcess items """
|
||||
return self._integrate_model.get_items()
|
||||
|
||||
# Processing methods
|
||||
def submit(self, wait=True):
|
||||
|
|
@ -176,29 +201,33 @@ class PushToContextController:
|
|||
if self._process_thread is not None:
|
||||
return
|
||||
|
||||
item_id = self._integrate_model.create_process_item(
|
||||
self._src_project_name,
|
||||
self._src_version_id,
|
||||
self._selection_model.get_selected_project_name(),
|
||||
self._selection_model.get_selected_folder_id(),
|
||||
self._selection_model.get_selected_task_name(),
|
||||
self._user_values.variant,
|
||||
comment=self._user_values.comment,
|
||||
new_folder_name=self._user_values.new_folder_name,
|
||||
dst_version=1
|
||||
)
|
||||
item_ids = []
|
||||
for src_version_entity in self._src_version_entities:
|
||||
item_id = self._integrate_model.create_process_item(
|
||||
self._src_project_name,
|
||||
src_version_entity["id"],
|
||||
self._selection_model.get_selected_project_name(),
|
||||
self._selection_model.get_selected_folder_id(),
|
||||
self._selection_model.get_selected_task_name(),
|
||||
self._user_values.variant,
|
||||
comment=self._user_values.comment,
|
||||
new_folder_name=self._user_values.new_folder_name,
|
||||
dst_version=1,
|
||||
use_original_name=self._use_original_name,
|
||||
)
|
||||
item_ids.append(item_id)
|
||||
|
||||
self._process_item_id = item_id
|
||||
self._process_item_ids = item_ids
|
||||
self._emit_event("submit.started")
|
||||
if wait:
|
||||
self._submit_callback()
|
||||
self._process_item_id = None
|
||||
self._process_item_ids = []
|
||||
return item_id
|
||||
|
||||
thread = threading.Thread(target=self._submit_callback)
|
||||
self._process_thread = thread
|
||||
thread.start()
|
||||
return item_id
|
||||
return item_ids
|
||||
|
||||
def wait_for_process_thread(self):
|
||||
if self._process_thread is None:
|
||||
|
|
@ -207,7 +236,7 @@ class PushToContextController:
|
|||
self._process_thread = None
|
||||
|
||||
def _prepare_source_label(self):
|
||||
if not self._src_project_name or not self._src_version_id:
|
||||
if not self._src_project_name or not self._src_version_ids:
|
||||
return "Source is not defined"
|
||||
|
||||
folder_entity = self._src_folder_entity
|
||||
|
|
@ -215,14 +244,21 @@ class PushToContextController:
|
|||
return "Source is invalid"
|
||||
|
||||
folder_path = folder_entity["path"]
|
||||
product_entity = self._src_product_entity
|
||||
version_entity = self._src_version_entity
|
||||
return "Source: {}{}/{}/v{:0>3}".format(
|
||||
self._src_project_name,
|
||||
folder_path,
|
||||
product_entity["name"],
|
||||
version_entity["version"]
|
||||
)
|
||||
src_labels = []
|
||||
for version_entity in self._src_version_entities:
|
||||
product_entity = self._src_product_entities.get(
|
||||
version_entity["productId"]
|
||||
)
|
||||
src_labels.append(
|
||||
"Source: {}{}/{}/v{:0>3}".format(
|
||||
self._src_project_name,
|
||||
folder_path,
|
||||
product_entity["name"],
|
||||
version_entity["version"],
|
||||
)
|
||||
)
|
||||
|
||||
return "\n".join(src_labels)
|
||||
|
||||
def _get_task_info_from_repre_entities(
|
||||
self, task_entities, repre_entities
|
||||
|
|
@ -256,7 +292,8 @@ class PushToContextController:
|
|||
|
||||
def _get_src_variant(self):
|
||||
project_name = self._src_project_name
|
||||
version_entity = self._src_version_entity
|
||||
# parse variant only from first version
|
||||
version_entity = self._src_version_entities[0]
|
||||
task_entities = self._src_folder_task_entities
|
||||
repre_entities = ayon_api.get_representations(
|
||||
project_name, version_ids={version_entity["id"]}
|
||||
|
|
@ -264,9 +301,12 @@ class PushToContextController:
|
|||
task_name, task_type = self._get_task_info_from_repre_entities(
|
||||
task_entities, repre_entities
|
||||
)
|
||||
product_entity = self._src_product_entities.get(
|
||||
version_entity["productId"]
|
||||
)
|
||||
|
||||
project_settings = get_project_settings(project_name)
|
||||
product_type = self._src_product_entity["productType"]
|
||||
product_type = product_entity["productType"]
|
||||
template = get_product_name_template(
|
||||
self._src_project_name,
|
||||
product_type,
|
||||
|
|
@ -300,7 +340,7 @@ class PushToContextController:
|
|||
print("Failed format", exc)
|
||||
return ""
|
||||
|
||||
product_name = self._src_product_entity["name"]
|
||||
product_name = product_entity["name"]
|
||||
if (
|
||||
(product_s and not product_name.startswith(product_s))
|
||||
or (product_e and not product_name.endswith(product_e))
|
||||
|
|
@ -314,9 +354,6 @@ class PushToContextController:
|
|||
return product_name
|
||||
|
||||
def _check_submit_validations(self):
|
||||
if not self._user_values.is_valid:
|
||||
return False
|
||||
|
||||
if not self._selection_model.get_selected_project_name():
|
||||
return False
|
||||
|
||||
|
|
@ -325,6 +362,13 @@ class PushToContextController:
|
|||
and not self._selection_model.get_selected_folder_id()
|
||||
):
|
||||
return False
|
||||
|
||||
if self._use_original_name:
|
||||
return True
|
||||
|
||||
if not self._user_values.is_valid:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _invalidate(self):
|
||||
|
|
@ -338,13 +382,14 @@ class PushToContextController:
|
|||
)
|
||||
|
||||
def _submit_callback(self):
|
||||
process_item_id = self._process_item_id
|
||||
if process_item_id is None:
|
||||
return
|
||||
self._integrate_model.integrate_item(process_item_id)
|
||||
process_item_ids = self._process_item_ids
|
||||
for process_item_id in process_item_ids:
|
||||
self._integrate_model.integrate_item(process_item_id)
|
||||
|
||||
self._emit_event("submit.finished", {})
|
||||
if process_item_id == self._process_item_id:
|
||||
self._process_item_id = None
|
||||
|
||||
if process_item_ids is self._process_item_ids:
|
||||
self._process_item_ids = []
|
||||
|
||||
def _emit_event(self, topic, data=None):
|
||||
if data is None:
|
||||
|
|
|
|||
|
|
@ -4,28 +4,28 @@ from ayon_core.tools.utils import get_ayon_qt_app
|
|||
from ayon_core.tools.push_to_project.ui import PushToContextSelectWindow
|
||||
|
||||
|
||||
def main_show(project_name, version_id):
|
||||
def main_show(project_name, version_ids):
|
||||
app = get_ayon_qt_app()
|
||||
|
||||
window = PushToContextSelectWindow()
|
||||
window.show()
|
||||
window.set_source(project_name, version_id)
|
||||
window.set_source(project_name, version_ids)
|
||||
|
||||
app.exec_()
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--project", help="Source project name")
|
||||
@click.option("--version", help="Source version id")
|
||||
def main(project, version):
|
||||
@click.option("--versions", help="Source version ids")
|
||||
def main(project, versions):
|
||||
"""Run PushToProject tool to integrate version in different project.
|
||||
|
||||
Args:
|
||||
project (str): Source project name.
|
||||
version (str): Version id.
|
||||
versions (str): comma separated versions for same context
|
||||
"""
|
||||
|
||||
main_show(project, version)
|
||||
main_show(project, versions.split(","))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import itertools
|
|||
import sys
|
||||
import traceback
|
||||
import uuid
|
||||
from typing import Optional, Dict
|
||||
|
||||
import ayon_api
|
||||
from ayon_api.utils import create_entity_id
|
||||
|
|
@ -21,6 +22,7 @@ from ayon_core.lib import (
|
|||
source_hash,
|
||||
)
|
||||
from ayon_core.lib.file_transaction import FileTransaction
|
||||
from ayon_core.pipeline.thumbnails import get_thumbnail_path
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.pipeline import Anatomy
|
||||
from ayon_core.pipeline.version_start import get_versioning_start
|
||||
|
|
@ -88,6 +90,7 @@ class ProjectPushItem:
|
|||
new_folder_name,
|
||||
dst_version,
|
||||
item_id=None,
|
||||
use_original_name=False
|
||||
):
|
||||
if not item_id:
|
||||
item_id = uuid.uuid4().hex
|
||||
|
|
@ -102,6 +105,7 @@ class ProjectPushItem:
|
|||
self.comment = comment or ""
|
||||
self.item_id = item_id
|
||||
self._repr_value = None
|
||||
self.use_original_name = use_original_name
|
||||
|
||||
@property
|
||||
def _repr(self):
|
||||
|
|
@ -113,7 +117,8 @@ class ProjectPushItem:
|
|||
str(self.dst_folder_id),
|
||||
str(self.new_folder_name),
|
||||
str(self.dst_task_name),
|
||||
str(self.dst_version)
|
||||
str(self.dst_version),
|
||||
self.use_original_name
|
||||
])
|
||||
return self._repr_value
|
||||
|
||||
|
|
@ -132,6 +137,7 @@ class ProjectPushItem:
|
|||
"comment": self.comment,
|
||||
"new_folder_name": self.new_folder_name,
|
||||
"item_id": self.item_id,
|
||||
"use_original_name": self.use_original_name
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
@ -311,7 +317,7 @@ class ProjectPushRepreItem:
|
|||
if self._src_files is not None:
|
||||
return self._src_files, self._resource_files
|
||||
|
||||
repre_context = self._repre_entity["context"]
|
||||
repre_context = self.repre_entity["context"]
|
||||
if "frame" in repre_context or "udim" in repre_context:
|
||||
src_files, resource_files = self._get_source_files_with_frames()
|
||||
else:
|
||||
|
|
@ -328,7 +334,7 @@ class ProjectPushRepreItem:
|
|||
udim_placeholder = "__udim__"
|
||||
src_files = []
|
||||
resource_files = []
|
||||
template = self._repre_entity["attrib"]["template"]
|
||||
template = self.repre_entity["attrib"]["template"]
|
||||
# Remove padding from 'udim' and 'frame' formatting keys
|
||||
# - "{frame:0>4}" -> "{frame}"
|
||||
for key in ("udim", "frame"):
|
||||
|
|
@ -336,7 +342,7 @@ class ProjectPushRepreItem:
|
|||
replacement = "{{{}}}".format(key)
|
||||
template = re.sub(sub_part, replacement, template)
|
||||
|
||||
repre_context = self._repre_entity["context"]
|
||||
repre_context = self.repre_entity["context"]
|
||||
fill_repre_context = copy.deepcopy(repre_context)
|
||||
if "frame" in fill_repre_context:
|
||||
fill_repre_context["frame"] = frame_placeholder
|
||||
|
|
@ -357,7 +363,7 @@ class ProjectPushRepreItem:
|
|||
.replace(udim_placeholder, "(?P<udim>[0-9]+)")
|
||||
)
|
||||
src_basename_regex = re.compile("^{}$".format(src_basename))
|
||||
for file_info in self._repre_entity["files"]:
|
||||
for file_info in self.repre_entity["files"]:
|
||||
filepath_template = self._clean_path(file_info["path"])
|
||||
filepath = self._clean_path(
|
||||
filepath_template.format(root=self._roots)
|
||||
|
|
@ -371,7 +377,6 @@ class ProjectPushRepreItem:
|
|||
resource_files.append(ResourceFile(filepath, relative_path))
|
||||
continue
|
||||
|
||||
filepath = os.path.join(src_dirpath, basename)
|
||||
frame = None
|
||||
udim = None
|
||||
for item in src_basename_regex.finditer(basename):
|
||||
|
|
@ -389,8 +394,8 @@ class ProjectPushRepreItem:
|
|||
def _get_source_files(self):
|
||||
src_files = []
|
||||
resource_files = []
|
||||
template = self._repre_entity["attrib"]["template"]
|
||||
repre_context = self._repre_entity["context"]
|
||||
template = self.repre_entity["attrib"]["template"]
|
||||
repre_context = self.repre_entity["context"]
|
||||
fill_repre_context = copy.deepcopy(repre_context)
|
||||
fill_roots = fill_repre_context["root"]
|
||||
for root_name in tuple(fill_roots.keys()):
|
||||
|
|
@ -399,7 +404,7 @@ class ProjectPushRepreItem:
|
|||
fill_repre_context)
|
||||
repre_path = self._clean_path(repre_path)
|
||||
src_dirpath = os.path.dirname(repre_path)
|
||||
for file_info in self._repre_entity["files"]:
|
||||
for file_info in self.repre_entity["files"]:
|
||||
filepath_template = self._clean_path(file_info["path"])
|
||||
filepath = self._clean_path(
|
||||
filepath_template.format(root=self._roots))
|
||||
|
|
@ -492,8 +497,11 @@ class ProjectPushItemProcess:
|
|||
|
||||
except Exception as exc:
|
||||
_exc, _value, _tb = sys.exc_info()
|
||||
product_name = self._src_product_entity["name"]
|
||||
self._status.set_failed(
|
||||
"Unhandled error happened: {}".format(str(exc)),
|
||||
"Unhandled error happened for `{}`: {}".format(
|
||||
product_name, str(exc)
|
||||
),
|
||||
(_exc, _value, _tb)
|
||||
)
|
||||
|
||||
|
|
@ -816,31 +824,34 @@ class ProjectPushItemProcess:
|
|||
self._template_name = template_name
|
||||
|
||||
def _determine_product_name(self):
|
||||
product_type = self._product_type
|
||||
task_info = self._task_info
|
||||
task_name = task_type = None
|
||||
if task_info:
|
||||
task_name = task_info["name"]
|
||||
task_type = task_info["taskType"]
|
||||
if self._item.use_original_name:
|
||||
product_name = self._src_product_entity["name"]
|
||||
else:
|
||||
product_type = self._product_type
|
||||
task_info = self._task_info
|
||||
task_name = task_type = None
|
||||
if task_info:
|
||||
task_name = task_info["name"]
|
||||
task_type = task_info["taskType"]
|
||||
|
||||
try:
|
||||
product_name = get_product_name(
|
||||
self._item.dst_project_name,
|
||||
task_name,
|
||||
task_type,
|
||||
self.host_name,
|
||||
product_type,
|
||||
self._item.variant,
|
||||
project_settings=self._project_settings
|
||||
)
|
||||
except TaskNotSetError:
|
||||
self._status.set_failed(
|
||||
"Target product name template requires task name. To continue"
|
||||
" you have to select target task or change settings"
|
||||
" <b>ayon+settings://core/tools/creator/product_name_profiles"
|
||||
f"?project={self._item.dst_project_name}</b>."
|
||||
)
|
||||
raise PushToProjectError(self._status.fail_reason)
|
||||
try:
|
||||
product_name = get_product_name(
|
||||
self._item.dst_project_name,
|
||||
task_name,
|
||||
task_type,
|
||||
self.host_name,
|
||||
product_type,
|
||||
self._item.variant,
|
||||
project_settings=self._project_settings
|
||||
)
|
||||
except TaskNotSetError:
|
||||
self._status.set_failed(
|
||||
"Target product name template requires task name. To "
|
||||
"continue you have to select target task or change settings " # noqa: E501
|
||||
" <b>ayon+settings://core/tools/creator/product_name_profiles" # noqa: E501
|
||||
f"?project={self._item.dst_project_name}</b>."
|
||||
)
|
||||
raise PushToProjectError(self._status.fail_reason)
|
||||
|
||||
self._log_info(
|
||||
f"Push will be integrating to product with name '{product_name}'"
|
||||
|
|
@ -917,14 +928,19 @@ class ProjectPushItemProcess:
|
|||
task_name=self._task_info["name"],
|
||||
task_type=self._task_info["taskType"],
|
||||
product_type=product_type,
|
||||
product_name=product_entity["name"]
|
||||
product_name=product_entity["name"],
|
||||
)
|
||||
|
||||
existing_version_entity = ayon_api.get_version_by_name(
|
||||
project_name, version, product_id
|
||||
)
|
||||
thumbnail_id = self._copy_version_thumbnail()
|
||||
|
||||
# Update existing version
|
||||
if existing_version_entity:
|
||||
updata_data = {"attrib": dst_attrib}
|
||||
if thumbnail_id:
|
||||
updata_data["thumbnailId"] = thumbnail_id
|
||||
self._operations.update_entity(
|
||||
project_name,
|
||||
"version",
|
||||
|
|
@ -939,6 +955,7 @@ class ProjectPushItemProcess:
|
|||
version,
|
||||
product_id,
|
||||
attribs=dst_attrib,
|
||||
thumbnail_id=thumbnail_id,
|
||||
)
|
||||
self._operations.create_entity(
|
||||
project_name, "version", version_entity
|
||||
|
|
@ -1005,10 +1022,18 @@ class ProjectPushItemProcess:
|
|||
self, anatomy, template_name, formatting_data, file_template
|
||||
):
|
||||
processed_repre_items = []
|
||||
repre_context = None
|
||||
for repre_item in self._src_repre_items:
|
||||
repre_entity = repre_item.repre_entity
|
||||
repre_name = repre_entity["name"]
|
||||
repre_format_data = copy.deepcopy(formatting_data)
|
||||
|
||||
if not repre_context:
|
||||
repre_context = self._update_repre_context(
|
||||
copy.deepcopy(repre_entity),
|
||||
formatting_data
|
||||
)
|
||||
|
||||
repre_format_data["representation"] = repre_name
|
||||
for src_file in repre_item.src_files:
|
||||
ext = os.path.splitext(src_file.path)[-1]
|
||||
|
|
@ -1024,7 +1049,6 @@ class ProjectPushItemProcess:
|
|||
"publish", template_name, "directory"
|
||||
)
|
||||
folder_path = template_obj.format_strict(formatting_data)
|
||||
repre_context = folder_path.used_values
|
||||
folder_path_rootless = folder_path.rootless
|
||||
repre_filepaths = []
|
||||
published_path = None
|
||||
|
|
@ -1047,7 +1071,6 @@ class ProjectPushItemProcess:
|
|||
)
|
||||
if published_path is None or frame == repre_item.frame:
|
||||
published_path = dst_filepath
|
||||
repre_context.update(filename.used_values)
|
||||
|
||||
repre_filepaths.append((dst_filepath, dst_rootless_path))
|
||||
self._file_transaction.add(src_file.path, dst_filepath)
|
||||
|
|
@ -1134,7 +1157,7 @@ class ProjectPushItemProcess:
|
|||
self._item.dst_project_name,
|
||||
"representation",
|
||||
entity_id,
|
||||
changes
|
||||
changes,
|
||||
)
|
||||
|
||||
existing_repre_names = set(existing_repres_by_low_name.keys())
|
||||
|
|
@ -1147,6 +1170,45 @@ class ProjectPushItemProcess:
|
|||
{"active": False}
|
||||
)
|
||||
|
||||
def _copy_version_thumbnail(self) -> Optional[str]:
|
||||
thumbnail_id = self._src_version_entity["thumbnailId"]
|
||||
if not thumbnail_id:
|
||||
return None
|
||||
path = get_thumbnail_path(
|
||||
self._item.src_project_name,
|
||||
"version",
|
||||
self._src_version_entity["id"],
|
||||
thumbnail_id
|
||||
)
|
||||
if not path:
|
||||
return None
|
||||
return ayon_api.create_thumbnail(
|
||||
self._item.dst_project_name,
|
||||
path
|
||||
)
|
||||
|
||||
def _update_repre_context(self, repre_entity, formatting_data):
|
||||
"""Replace old context value with new ones.
|
||||
|
||||
Folder might change, project definitely changes etc.
|
||||
"""
|
||||
repre_context = repre_entity["context"]
|
||||
for context_key, context_value in repre_context.items():
|
||||
if context_value and isinstance(context_value, dict):
|
||||
for context_sub_key in context_value.keys():
|
||||
value_to_update = formatting_data.get(context_key, {}).get(
|
||||
context_sub_key)
|
||||
if value_to_update:
|
||||
repre_context[context_key][
|
||||
context_sub_key] = value_to_update
|
||||
else:
|
||||
value_to_update = formatting_data.get(context_key)
|
||||
if value_to_update:
|
||||
repre_context[context_key] = value_to_update
|
||||
if "task" not in formatting_data:
|
||||
repre_context.pop("task")
|
||||
return repre_context
|
||||
|
||||
|
||||
class IntegrateModel:
|
||||
def __init__(self, controller):
|
||||
|
|
@ -1170,6 +1232,7 @@ class IntegrateModel:
|
|||
comment,
|
||||
new_folder_name,
|
||||
dst_version,
|
||||
use_original_name
|
||||
):
|
||||
"""Create new item for integration.
|
||||
|
||||
|
|
@ -1183,6 +1246,7 @@ class IntegrateModel:
|
|||
comment (Union[str, None]): Comment.
|
||||
new_folder_name (Union[str, None]): New folder name.
|
||||
dst_version (int): Destination version number.
|
||||
use_original_name (bool): If original product names should be used
|
||||
|
||||
Returns:
|
||||
str: Item id. The id can be used to trigger integration or get
|
||||
|
|
@ -1198,7 +1262,8 @@ class IntegrateModel:
|
|||
variant,
|
||||
comment=comment,
|
||||
new_folder_name=new_folder_name,
|
||||
dst_version=dst_version
|
||||
dst_version=dst_version,
|
||||
use_original_name=use_original_name
|
||||
)
|
||||
process_item = ProjectPushItemProcess(self, item)
|
||||
self._process_items[item.item_id] = process_item
|
||||
|
|
@ -1216,17 +1281,6 @@ class IntegrateModel:
|
|||
return
|
||||
item.integrate()
|
||||
|
||||
def get_item_status(self, item_id):
|
||||
"""Status of an item.
|
||||
|
||||
Args:
|
||||
item_id (str): Item id for which status should be returned.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Status data.
|
||||
"""
|
||||
|
||||
item = self._process_items.get(item_id)
|
||||
if item is not None:
|
||||
return item.get_status_data()
|
||||
return None
|
||||
def get_items(self) -> Dict[str, ProjectPushItemProcess]:
|
||||
"""Returns dict of all ProjectPushItemProcess items """
|
||||
return self._process_items
|
||||
|
|
|
|||
|
|
@ -85,6 +85,13 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
|
||||
header_widget = QtWidgets.QWidget(main_context_widget)
|
||||
|
||||
library_only_label = QtWidgets.QLabel(
|
||||
"Show only libraries",
|
||||
header_widget
|
||||
)
|
||||
library_only_checkbox = NiceCheckbox(
|
||||
True, parent=header_widget)
|
||||
|
||||
header_label = QtWidgets.QLabel(
|
||||
controller.get_source_label(),
|
||||
header_widget
|
||||
|
|
@ -92,7 +99,9 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
|
||||
header_layout = QtWidgets.QHBoxLayout(header_widget)
|
||||
header_layout.setContentsMargins(0, 0, 0, 0)
|
||||
header_layout.addWidget(header_label)
|
||||
header_layout.addWidget(header_label, 1)
|
||||
header_layout.addWidget(library_only_label, 0)
|
||||
header_layout.addWidget(library_only_checkbox, 0)
|
||||
|
||||
main_splitter = QtWidgets.QSplitter(
|
||||
QtCore.Qt.Horizontal, main_context_widget
|
||||
|
|
@ -124,6 +133,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
inputs_widget = QtWidgets.QWidget(main_splitter)
|
||||
|
||||
new_folder_checkbox = NiceCheckbox(True, parent=inputs_widget)
|
||||
original_names_checkbox = NiceCheckbox(False, parent=inputs_widget)
|
||||
|
||||
folder_name_input = PlaceholderLineEdit(inputs_widget)
|
||||
folder_name_input.setPlaceholderText("< Name of new folder >")
|
||||
|
|
@ -142,6 +152,8 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
inputs_layout.addRow("Create new folder", new_folder_checkbox)
|
||||
inputs_layout.addRow("New folder name", folder_name_input)
|
||||
inputs_layout.addRow("Variant", variant_input)
|
||||
inputs_layout.addRow(
|
||||
"Use original product names", original_names_checkbox)
|
||||
inputs_layout.addRow("Comment", comment_input)
|
||||
|
||||
main_splitter.addWidget(context_widget)
|
||||
|
|
@ -196,6 +208,10 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
show_detail_btn.setToolTip(
|
||||
"Show error detail dialog to copy full error."
|
||||
)
|
||||
original_names_checkbox.setToolTip(
|
||||
"Required for multi copy, doesn't allow changes "
|
||||
"variant values."
|
||||
)
|
||||
|
||||
overlay_close_btn = QtWidgets.QPushButton(
|
||||
"Close", overlay_btns_widget
|
||||
|
|
@ -240,6 +256,9 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
folder_name_input.textChanged.connect(self._on_new_folder_change)
|
||||
variant_input.textChanged.connect(self._on_variant_change)
|
||||
comment_input.textChanged.connect(self._on_comment_change)
|
||||
library_only_checkbox.stateChanged.connect(self._on_library_only_change)
|
||||
original_names_checkbox.stateChanged.connect(
|
||||
self._on_original_names_change)
|
||||
|
||||
publish_btn.clicked.connect(self._on_select_click)
|
||||
cancel_btn.clicked.connect(self._on_close_click)
|
||||
|
|
@ -288,6 +307,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
self._new_folder_checkbox = new_folder_checkbox
|
||||
self._folder_name_input = folder_name_input
|
||||
self._comment_input = comment_input
|
||||
self._use_original_names_checkbox = original_names_checkbox
|
||||
|
||||
self._publish_btn = publish_btn
|
||||
|
||||
|
|
@ -316,7 +336,6 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
self._main_thread_timer = main_thread_timer
|
||||
self._main_thread_timer_can_stop = True
|
||||
self._last_submit_message = None
|
||||
self._process_item_id = None
|
||||
|
||||
self._variant_is_valid = None
|
||||
self._folder_is_valid = None
|
||||
|
|
@ -327,17 +346,17 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
overlay_try_btn.setVisible(False)
|
||||
|
||||
# Support of public api function of controller
|
||||
def set_source(self, project_name, version_id):
|
||||
def set_source(self, project_name, version_ids):
|
||||
"""Set source project and version.
|
||||
|
||||
Call the method on controller.
|
||||
|
||||
Args:
|
||||
project_name (Union[str, None]): Name of project.
|
||||
version_id (Union[str, None]): Version id.
|
||||
version_ids (Union[str, None]): comma separated Version ids.
|
||||
"""
|
||||
|
||||
self._controller.set_source(project_name, version_id)
|
||||
self._controller.set_source(project_name, version_ids)
|
||||
|
||||
def showEvent(self, event):
|
||||
super(PushToContextSelectWindow, self).showEvent(event)
|
||||
|
|
@ -352,10 +371,12 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
self._folder_name_input.setText(new_folder_name or "")
|
||||
self._variant_input.setText(variant or "")
|
||||
self._invalidate_variant(user_values["is_variant_valid"])
|
||||
self._invalidate_use_original_names(
|
||||
self._use_original_names_checkbox.isChecked())
|
||||
self._invalidate_new_folder_name(
|
||||
new_folder_name, user_values["is_new_folder_name_valid"]
|
||||
)
|
||||
|
||||
self._controller._invalidate()
|
||||
self._projects_combobox.refresh()
|
||||
|
||||
def _on_first_show(self):
|
||||
|
|
@ -394,6 +415,15 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
self._comment_input_text = text
|
||||
self._user_input_changed_timer.start()
|
||||
|
||||
def _on_library_only_change(self, state: int) -> None:
|
||||
"""Change toggle state, reset filter, recalculate dropdown"""
|
||||
state = bool(state)
|
||||
self._projects_combobox.set_standard_filter_enabled(state)
|
||||
|
||||
def _on_original_names_change(self, state: int) -> None:
|
||||
use_original_name = bool(state)
|
||||
self._invalidate_use_original_names(use_original_name)
|
||||
|
||||
def _on_user_input_timer(self):
|
||||
folder_name_enabled = self._new_folder_name_enabled
|
||||
folder_name = self._new_folder_name_input_text
|
||||
|
|
@ -456,17 +486,27 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
state = ""
|
||||
if folder_name is not None:
|
||||
state = "valid" if is_valid else "invalid"
|
||||
set_style_property(
|
||||
self._folder_name_input, "state", state
|
||||
)
|
||||
set_style_property(self._folder_name_input, "state", state)
|
||||
|
||||
def _invalidate_variant(self, is_valid):
|
||||
if self._variant_is_valid is is_valid:
|
||||
return
|
||||
self._variant_is_valid = is_valid
|
||||
state = "valid" if is_valid else "invalid"
|
||||
set_style_property(self._variant_input, "state", state)
|
||||
|
||||
def _invalidate_use_original_names(self, use_original_names):
|
||||
"""Checks if original names must be used.
|
||||
|
||||
Invalidates Variant if necessary
|
||||
"""
|
||||
if self._controller.original_names_required():
|
||||
use_original_names = True
|
||||
|
||||
self._variant_input.setEnabled(not use_original_names)
|
||||
self._invalidate_variant(not use_original_names)
|
||||
|
||||
self._controller._use_original_name = use_original_names
|
||||
self._use_original_names_checkbox.setChecked(use_original_names)
|
||||
|
||||
def _on_submission_change(self, event):
|
||||
self._publish_btn.setEnabled(event["enabled"])
|
||||
|
||||
|
|
@ -495,31 +535,43 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
self._overlay_label.setText(self._last_submit_message)
|
||||
self._last_submit_message = None
|
||||
|
||||
process_status = self._controller.get_process_item_status(
|
||||
self._process_item_id
|
||||
)
|
||||
push_failed = process_status["failed"]
|
||||
fail_traceback = process_status["full_traceback"]
|
||||
failed_pushes = []
|
||||
fail_tracebacks = []
|
||||
for process_item in self._controller.get_process_items().values():
|
||||
process_status = process_item.get_status_data()
|
||||
if process_status["failed"]:
|
||||
failed_pushes.append(process_status)
|
||||
# push_failed = process_status["failed"]
|
||||
# fail_traceback = process_status["full_traceback"]
|
||||
if self._main_thread_timer_can_stop:
|
||||
self._main_thread_timer.stop()
|
||||
self._overlay_close_btn.setVisible(True)
|
||||
if push_failed:
|
||||
if failed_pushes:
|
||||
self._overlay_try_btn.setVisible(True)
|
||||
if fail_traceback:
|
||||
fail_tracebacks = [
|
||||
process_status["full_traceback"]
|
||||
for process_status in failed_pushes
|
||||
if process_status["full_traceback"]
|
||||
]
|
||||
if fail_tracebacks:
|
||||
self._show_detail_btn.setVisible(True)
|
||||
|
||||
if push_failed:
|
||||
reason = process_status["fail_reason"]
|
||||
if fail_traceback:
|
||||
if failed_pushes:
|
||||
reasons = [
|
||||
process_status["fail_reason"]
|
||||
for process_status in failed_pushes
|
||||
]
|
||||
if fail_tracebacks:
|
||||
reason = "\n".join(reasons)
|
||||
message = (
|
||||
"Unhandled error happened."
|
||||
" Check error detail for more information."
|
||||
)
|
||||
self._error_detail_dialog.set_detail(
|
||||
reason, fail_traceback
|
||||
reason, "\n".join(fail_tracebacks)
|
||||
)
|
||||
else:
|
||||
message = f"Push Failed:\n{reason}"
|
||||
message = f"Push Failed:\n{reasons}"
|
||||
|
||||
self._overlay_label.setText(message)
|
||||
set_style_property(self._overlay_close_btn, "state", "error")
|
||||
|
|
@ -534,7 +586,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
self._main_thread_timer_can_stop = False
|
||||
self._main_thread_timer.start()
|
||||
self._main_layout.setCurrentWidget(self._overlay_widget)
|
||||
self._overlay_label.setText("Submittion started")
|
||||
self._overlay_label.setText("Submission started")
|
||||
|
||||
def _on_controller_submit_end(self):
|
||||
self._main_thread_timer_can_stop = True
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
|
|||
status_text_rect.setLeft(icon_rect.right() + 2)
|
||||
|
||||
if status_text_rect.width() <= 0:
|
||||
painter.restore()
|
||||
return
|
||||
|
||||
if status_text_rect.width() < metrics.width(status_name):
|
||||
|
|
@ -144,6 +145,7 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
|
|||
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
|
||||
status_name
|
||||
)
|
||||
painter.restore()
|
||||
|
||||
def set_current_index(self, index):
|
||||
model = self._combo_view.model()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'core' version."""
|
||||
__version__ = "1.5.3+dev"
|
||||
__version__ = "1.6.0+dev"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ qtawesome = "0.7.3"
|
|||
[ayon.runtimeDependencies]
|
||||
aiohttp-middlewares = "^2.0.0"
|
||||
Click = "^8"
|
||||
OpenTimelineIO = "0.16.0"
|
||||
OpenTimelineIO = "0.17.0"
|
||||
opencolorio = "^2.3.2,<2.4.0"
|
||||
Pillow = "9.5.0"
|
||||
websocket-client = ">=0.40.0,<2"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name = "core"
|
||||
title = "Core"
|
||||
version = "1.5.3+dev"
|
||||
version = "1.6.0+dev"
|
||||
|
||||
client_dir = "ayon_core"
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
[tool.poetry]
|
||||
name = "ayon-core"
|
||||
version = "1.5.3+dev"
|
||||
version = "1.6.0+dev"
|
||||
description = ""
|
||||
authors = ["Ynput Team <team@ynput.io>"]
|
||||
readme = "README.md"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue