mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
Merge branch 'develop' into enhancement/OP-6317_Nuke-publish-existing-frames-on-farm
This commit is contained in:
commit
a8db933143
98 changed files with 4333 additions and 559 deletions
10
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
10
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,11 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to OpenPype Tray
|
||||
options:
|
||||
- 3.16.4
|
||||
- 3.16.4-nightly.3
|
||||
- 3.16.4-nightly.2
|
||||
- 3.16.4-nightly.1
|
||||
- 3.16.3
|
||||
- 3.16.3-nightly.5
|
||||
- 3.16.3-nightly.4
|
||||
- 3.16.3-nightly.3
|
||||
|
|
@ -130,11 +135,6 @@ body:
|
|||
- 3.14.8-nightly.2
|
||||
- 3.14.8-nightly.1
|
||||
- 3.14.7
|
||||
- 3.14.7-nightly.8
|
||||
- 3.14.7-nightly.7
|
||||
- 3.14.7-nightly.6
|
||||
- 3.14.7-nightly.5
|
||||
- 3.14.7-nightly.4
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
1160
CHANGELOG.md
1160
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
|
@ -6,6 +6,9 @@ from .mongo import (
|
|||
OpenPypeMongoConnection,
|
||||
get_project_database,
|
||||
get_project_connection,
|
||||
load_json_file,
|
||||
replace_project_documents,
|
||||
store_project_documents,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -17,4 +20,7 @@ __all__ = (
|
|||
"OpenPypeMongoConnection",
|
||||
"get_project_database",
|
||||
"get_project_connection",
|
||||
"load_json_file",
|
||||
"replace_project_documents",
|
||||
"store_project_documents",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1074,7 +1074,7 @@ def convert_update_folder_to_v4(project_name, asset_id, update_data, con):
|
|||
parent_id = None
|
||||
tasks = None
|
||||
new_data = {}
|
||||
attribs = {}
|
||||
attribs = full_update_data.pop("attrib", {})
|
||||
if "type" in update_data:
|
||||
new_update_data["active"] = update_data["type"] == "asset"
|
||||
|
||||
|
|
@ -1113,6 +1113,9 @@ def convert_update_folder_to_v4(project_name, asset_id, update_data, con):
|
|||
print("Folder has new data: {}".format(new_data))
|
||||
new_update_data["data"] = new_data
|
||||
|
||||
if attribs:
|
||||
new_update_data["attrib"] = attribs
|
||||
|
||||
if has_task_changes:
|
||||
raise ValueError("Task changes of folder are not implemented")
|
||||
|
||||
|
|
@ -1126,7 +1129,7 @@ def convert_update_subset_to_v4(project_name, subset_id, update_data, con):
|
|||
full_update_data = _from_flat_dict(update_data)
|
||||
data = full_update_data.get("data")
|
||||
new_data = {}
|
||||
attribs = {}
|
||||
attribs = full_update_data.pop("attrib", {})
|
||||
if data:
|
||||
if "family" in data:
|
||||
family = data.pop("family")
|
||||
|
|
@ -1148,9 +1151,6 @@ def convert_update_subset_to_v4(project_name, subset_id, update_data, con):
|
|||
elif value is not REMOVED_VALUE:
|
||||
new_data[key] = value
|
||||
|
||||
if attribs:
|
||||
new_update_data["attribs"] = attribs
|
||||
|
||||
if "name" in update_data:
|
||||
new_update_data["name"] = update_data["name"]
|
||||
|
||||
|
|
@ -1165,6 +1165,9 @@ def convert_update_subset_to_v4(project_name, subset_id, update_data, con):
|
|||
new_update_data["folderId"] = update_data["parent"]
|
||||
|
||||
flat_data = _to_flat_dict(new_update_data)
|
||||
if attribs:
|
||||
flat_data["attrib"] = attribs
|
||||
|
||||
if new_data:
|
||||
print("Subset has new data: {}".format(new_data))
|
||||
flat_data["data"] = new_data
|
||||
|
|
@ -1179,7 +1182,7 @@ def convert_update_version_to_v4(project_name, version_id, update_data, con):
|
|||
full_update_data = _from_flat_dict(update_data)
|
||||
data = full_update_data.get("data")
|
||||
new_data = {}
|
||||
attribs = {}
|
||||
attribs = full_update_data.pop("attrib", {})
|
||||
if data:
|
||||
if "author" in data:
|
||||
new_update_data["author"] = data.pop("author")
|
||||
|
|
@ -1196,9 +1199,6 @@ def convert_update_version_to_v4(project_name, version_id, update_data, con):
|
|||
elif value is not REMOVED_VALUE:
|
||||
new_data[key] = value
|
||||
|
||||
if attribs:
|
||||
new_update_data["attribs"] = attribs
|
||||
|
||||
if "name" in update_data:
|
||||
new_update_data["version"] = update_data["name"]
|
||||
|
||||
|
|
@ -1213,6 +1213,9 @@ def convert_update_version_to_v4(project_name, version_id, update_data, con):
|
|||
new_update_data["productId"] = update_data["parent"]
|
||||
|
||||
flat_data = _to_flat_dict(new_update_data)
|
||||
if attribs:
|
||||
flat_data["attrib"] = attribs
|
||||
|
||||
if new_data:
|
||||
print("Version has new data: {}".format(new_data))
|
||||
flat_data["data"] = new_data
|
||||
|
|
@ -1252,7 +1255,7 @@ def convert_update_representation_to_v4(
|
|||
data = full_update_data.get("data")
|
||||
|
||||
new_data = {}
|
||||
attribs = {}
|
||||
attribs = full_update_data.pop("attrib", {})
|
||||
if data:
|
||||
for key, value in data.items():
|
||||
if key in folder_attributes:
|
||||
|
|
@ -1309,6 +1312,9 @@ def convert_update_representation_to_v4(
|
|||
new_update_data["files"] = new_files
|
||||
|
||||
flat_data = _to_flat_dict(new_update_data)
|
||||
if attribs:
|
||||
flat_data["attrib"] = attribs
|
||||
|
||||
if new_data:
|
||||
print("Representation has new data: {}".format(new_data))
|
||||
flat_data["data"] = new_data
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
"""Cache of thumbnails downloaded from AYON server.
|
||||
|
||||
Thumbnails are cached to appdirs to predefined directory.
|
||||
|
||||
This should be moved to thumbnails logic in pipeline but because it would
|
||||
overflow OpenPype logic it's here for now.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import collections
|
||||
|
|
@ -10,7 +18,7 @@ FileInfo = collections.namedtuple(
|
|||
)
|
||||
|
||||
|
||||
class ThumbnailCache:
|
||||
class AYONThumbnailCache:
|
||||
"""Cache of thumbnails on local storage.
|
||||
|
||||
Thumbnails are cached to appdirs to predefined directory. Each project has
|
||||
|
|
@ -32,13 +40,14 @@ class ThumbnailCache:
|
|||
|
||||
# Lifetime of thumbnails (in seconds)
|
||||
# - default 3 days
|
||||
days_alive = 3 * 24 * 60 * 60
|
||||
days_alive = 3
|
||||
# Max size of thumbnail directory (in bytes)
|
||||
# - default 2 Gb
|
||||
max_filesize = 2 * 1024 * 1024 * 1024
|
||||
|
||||
def __init__(self, cleanup=True):
|
||||
self._thumbnails_dir = None
|
||||
self._days_alive_secs = self.days_alive * 24 * 60 * 60
|
||||
if cleanup:
|
||||
self.cleanup()
|
||||
|
||||
|
|
@ -50,7 +59,8 @@ class ThumbnailCache:
|
|||
"""
|
||||
|
||||
if self._thumbnails_dir is None:
|
||||
directory = appdirs.user_data_dir("ayon", "ynput")
|
||||
# TODO use generic function
|
||||
directory = appdirs.user_data_dir("AYON", "Ynput")
|
||||
self._thumbnails_dir = os.path.join(directory, "thumbnails")
|
||||
return self._thumbnails_dir
|
||||
|
||||
|
|
@ -121,7 +131,7 @@ class ThumbnailCache:
|
|||
for filename in filenames:
|
||||
path = os.path.join(root, filename)
|
||||
modification_time = os.path.getmtime(path)
|
||||
if current_time - modification_time > self.days_alive:
|
||||
if current_time - modification_time > self._days_alive_secs:
|
||||
os.remove(path)
|
||||
|
||||
def _max_size_cleanup(self, thumbnails_dir):
|
||||
|
|
@ -28,7 +28,6 @@ class RenderCreator(Creator):
|
|||
create_allow_context_change = True
|
||||
|
||||
# Settings
|
||||
default_variants = []
|
||||
mark_for_review = True
|
||||
|
||||
def create(self, subset_name_from_ui, data, pre_create_data):
|
||||
|
|
@ -171,6 +170,10 @@ class RenderCreator(Creator):
|
|||
)
|
||||
|
||||
self.mark_for_review = plugin_settings["mark_for_review"]
|
||||
self.default_variants = plugin_settings.get(
|
||||
"default_variants",
|
||||
plugin_settings.get("defaults") or []
|
||||
)
|
||||
|
||||
def get_detail_description(self):
|
||||
return """Creator for Render instances
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ from openpype.pipeline import (
|
|||
LegacyCreator,
|
||||
LoaderPlugin,
|
||||
get_representation_path,
|
||||
|
||||
legacy_io,
|
||||
)
|
||||
from openpype.pipeline.load import LoadError
|
||||
from openpype.client import get_asset_by_name
|
||||
from openpype.pipeline.create import get_subset_name
|
||||
|
||||
from . import lib
|
||||
from .lib import imprint, read
|
||||
|
|
@ -405,14 +405,21 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase):
|
|||
# No existing scene instance node for this layer. Note that
|
||||
# this instance will not have the `instance_node` data yet
|
||||
# until it's been saved/persisted at least once.
|
||||
# TODO: Correctly define the subset name using templates
|
||||
prefix = self.layer_instance_prefix or self.family
|
||||
subset_name = "{}{}".format(prefix, layer.name())
|
||||
project_name = self.create_context.get_current_project_name()
|
||||
|
||||
instance_data = {
|
||||
"asset": legacy_io.Session["AVALON_ASSET"],
|
||||
"task": legacy_io.Session["AVALON_TASK"],
|
||||
"asset": self.create_context.get_current_asset_name(),
|
||||
"task": self.create_context.get_current_task_name(),
|
||||
"variant": layer.name(),
|
||||
}
|
||||
asset_doc = get_asset_by_name(project_name,
|
||||
instance_data["asset"])
|
||||
subset_name = self.get_subset_name(
|
||||
layer.name(),
|
||||
instance_data["task"],
|
||||
asset_doc,
|
||||
project_name)
|
||||
|
||||
instance = CreatedInstance(
|
||||
family=self.family,
|
||||
subset_name=subset_name,
|
||||
|
|
@ -519,10 +526,75 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase):
|
|||
if node and cmds.objExists(node):
|
||||
cmds.delete(node)
|
||||
|
||||
def get_subset_name(
|
||||
self,
|
||||
variant,
|
||||
task_name,
|
||||
asset_doc,
|
||||
project_name,
|
||||
host_name=None,
|
||||
instance=None
|
||||
):
|
||||
# creator.family != 'render' as expected
|
||||
return get_subset_name(self.layer_instance_prefix,
|
||||
variant,
|
||||
task_name,
|
||||
asset_doc,
|
||||
project_name)
|
||||
|
||||
|
||||
class Loader(LoaderPlugin):
|
||||
hosts = ["maya"]
|
||||
|
||||
def get_custom_namespace_and_group(self, context, options, loader_key):
|
||||
"""Queries Settings to get custom template for namespace and group.
|
||||
|
||||
Group template might be empty >> this forces to not wrap imported items
|
||||
into separate group.
|
||||
|
||||
Args:
|
||||
context (dict)
|
||||
options (dict): artist modifiable options from dialog
|
||||
loader_key (str): key to get separate configuration from Settings
|
||||
('reference_loader'|'import_loader')
|
||||
"""
|
||||
options["attach_to_root"] = True
|
||||
|
||||
asset = context['asset']
|
||||
subset = context['subset']
|
||||
settings = get_project_settings(context['project']['name'])
|
||||
custom_naming = settings['maya']['load'][loader_key]
|
||||
|
||||
if not custom_naming['namespace']:
|
||||
raise LoadError("No namespace specified in "
|
||||
"Maya ReferenceLoader settings")
|
||||
elif not custom_naming['group_name']:
|
||||
self.log.debug("No custom group_name, no group will be created.")
|
||||
options["attach_to_root"] = False
|
||||
|
||||
formatting_data = {
|
||||
"asset_name": asset['name'],
|
||||
"asset_type": asset['type'],
|
||||
"folder": {
|
||||
"name": asset["name"],
|
||||
},
|
||||
"subset": subset['name'],
|
||||
"family": (
|
||||
subset['data'].get('family') or
|
||||
subset['data']['families'][0]
|
||||
)
|
||||
}
|
||||
|
||||
custom_namespace = custom_naming['namespace'].format(
|
||||
**formatting_data
|
||||
)
|
||||
|
||||
custom_group_name = custom_naming['group_name'].format(
|
||||
**formatting_data
|
||||
)
|
||||
|
||||
return custom_group_name, custom_namespace, options
|
||||
|
||||
|
||||
class ReferenceLoader(Loader):
|
||||
"""A basic ReferenceLoader for Maya
|
||||
|
|
@ -565,42 +637,13 @@ class ReferenceLoader(Loader):
|
|||
path = self.filepath_from_context(context)
|
||||
assert os.path.exists(path), "%s does not exist." % path
|
||||
|
||||
asset = context['asset']
|
||||
subset = context['subset']
|
||||
settings = get_project_settings(context['project']['name'])
|
||||
custom_naming = settings['maya']['load']['reference_loader']
|
||||
loaded_containers = []
|
||||
|
||||
if not custom_naming['namespace']:
|
||||
raise LoadError("No namespace specified in "
|
||||
"Maya ReferenceLoader settings")
|
||||
elif not custom_naming['group_name']:
|
||||
self.log.debug("No custom group_name, no group will be created.")
|
||||
options["attach_to_root"] = False
|
||||
|
||||
formatting_data = {
|
||||
"asset_name": asset['name'],
|
||||
"asset_type": asset['type'],
|
||||
"folder": {
|
||||
"name": asset["name"],
|
||||
},
|
||||
"subset": subset['name'],
|
||||
"family": (
|
||||
subset['data'].get('family') or
|
||||
subset['data']['families'][0]
|
||||
)
|
||||
}
|
||||
|
||||
custom_namespace = custom_naming['namespace'].format(
|
||||
**formatting_data
|
||||
)
|
||||
|
||||
custom_group_name = custom_naming['group_name'].format(
|
||||
**formatting_data
|
||||
)
|
||||
custom_group_name, custom_namespace, options = \
|
||||
self.get_custom_namespace_and_group(context, options,
|
||||
"reference_loader")
|
||||
|
||||
count = options.get("count") or 1
|
||||
|
||||
loaded_containers = []
|
||||
for c in range(0, count):
|
||||
namespace = lib.get_custom_namespace(custom_namespace)
|
||||
group_name = "{}:{}".format(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ from openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin
|
|||
from openpype.hosts.maya.api import plugin
|
||||
from openpype.hosts.maya.api.lib import read
|
||||
|
||||
from openpype.client import get_asset_by_name
|
||||
|
||||
from maya import cmds
|
||||
from maya.app.renderSetup.model import renderSetup
|
||||
|
||||
|
|
@ -135,6 +137,18 @@ class MayaLegacyConvertor(SubsetConvertorPlugin,
|
|||
# "rendering" family being converted to "renderlayer" family)
|
||||
original_data["family"] = creator.family
|
||||
|
||||
# recreate subset name as without it would be
|
||||
# `renderingMain` vs correct `renderMain`
|
||||
project_name = self.create_context.get_current_project_name()
|
||||
asset_doc = get_asset_by_name(project_name,
|
||||
original_data["asset"])
|
||||
subset_name = creator.get_subset_name(
|
||||
original_data["variant"],
|
||||
data["task"],
|
||||
asset_doc,
|
||||
project_name)
|
||||
original_data["subset"] = subset_name
|
||||
|
||||
# Convert to creator attributes when relevant
|
||||
creator_attributes = {}
|
||||
for key in list(original_data.keys()):
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
suffix="_abc"
|
||||
)
|
||||
|
||||
attach_to_root = options.get("attach_to_root", True)
|
||||
group_name = options["group_name"]
|
||||
|
||||
# no group shall be created
|
||||
if not attach_to_root:
|
||||
group_name = namespace
|
||||
|
||||
# hero_001 (abc)
|
||||
# asset_counter{optional}
|
||||
path = self.filepath_from_context(context)
|
||||
|
|
@ -41,8 +48,8 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
nodes = cmds.file(file_url,
|
||||
namespace=namespace,
|
||||
sharedReferenceFile=False,
|
||||
groupReference=True,
|
||||
groupName=options['group_name'],
|
||||
groupReference=attach_to_root,
|
||||
groupName=group_name,
|
||||
reference=True,
|
||||
returnNewNodes=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ import qargparse
|
|||
from openpype.pipeline import load
|
||||
from openpype.hosts.maya.api.lib import (
|
||||
maintained_selection,
|
||||
unique_namespace
|
||||
get_custom_namespace
|
||||
)
|
||||
import openpype.hosts.maya.api.plugin
|
||||
|
||||
|
||||
class SetFrameRangeLoader(load.LoaderPlugin):
|
||||
|
|
@ -83,7 +84,7 @@ class SetFrameRangeWithHandlesLoader(load.LoaderPlugin):
|
|||
animationEndTime=end)
|
||||
|
||||
|
||||
class ImportMayaLoader(load.LoaderPlugin):
|
||||
class ImportMayaLoader(openpype.hosts.maya.api.plugin.Loader):
|
||||
"""Import action for Maya (unmanaged)
|
||||
|
||||
Warning:
|
||||
|
|
@ -130,13 +131,14 @@ class ImportMayaLoader(load.LoaderPlugin):
|
|||
if choice is False:
|
||||
return
|
||||
|
||||
asset = context['asset']
|
||||
custom_group_name, custom_namespace, options = \
|
||||
self.get_custom_namespace_and_group(context, data,
|
||||
"import_loader")
|
||||
|
||||
namespace = namespace or unique_namespace(
|
||||
asset["name"] + "_",
|
||||
prefix="_" if asset["name"][0].isdigit() else "",
|
||||
suffix="_",
|
||||
)
|
||||
namespace = get_custom_namespace(custom_namespace)
|
||||
|
||||
if not options.get("attach_to_root", True):
|
||||
custom_group_name = namespace
|
||||
|
||||
path = self.filepath_from_context(context)
|
||||
with maintained_selection():
|
||||
|
|
@ -145,8 +147,9 @@ class ImportMayaLoader(load.LoaderPlugin):
|
|||
preserveReferences=True,
|
||||
namespace=namespace,
|
||||
returnNewNodes=True,
|
||||
groupReference=True,
|
||||
groupName="{}:{}".format(namespace, name))
|
||||
groupReference=options.get("attach_to_root",
|
||||
True),
|
||||
groupName=custom_group_name)
|
||||
|
||||
if data.get("clean_import", False):
|
||||
remove_attributes = ["cbId"]
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ from openpype.hosts.maya.api.lib import (
|
|||
maintained_selection,
|
||||
get_container_members,
|
||||
parent_nodes,
|
||||
create_rig_animation_instance,
|
||||
get_reference_node
|
||||
create_rig_animation_instance
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,15 @@ class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
def process_reference(
|
||||
self, context, name=None, namespace=None, options=None
|
||||
):
|
||||
group_name = options['group_name']
|
||||
path = self.filepath_from_context(context)
|
||||
|
||||
attach_to_root = options.get("attach_to_root", True)
|
||||
group_name = options["group_name"]
|
||||
|
||||
# no group shall be created
|
||||
if not attach_to_root:
|
||||
group_name = namespace
|
||||
|
||||
with lib.maintained_selection():
|
||||
file_url = self.prepare_root_value(
|
||||
path, context["project"]["name"]
|
||||
|
|
@ -30,7 +37,7 @@ class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
namespace=namespace,
|
||||
reference=True,
|
||||
returnNewNodes=True,
|
||||
groupReference=True,
|
||||
groupReference=attach_to_root,
|
||||
groupName=group_name
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ class CollectCurrentFile(pyblish.api.ContextPlugin):
|
|||
order = pyblish.api.CollectorOrder - 0.4
|
||||
label = "Maya Current File"
|
||||
hosts = ['maya']
|
||||
families = ["workfile"]
|
||||
|
||||
def process(self, context):
|
||||
"""Inject the current working file"""
|
||||
|
|
|
|||
|
|
@ -304,9 +304,9 @@ class CollectMayaRender(pyblish.api.InstancePlugin):
|
|||
|
||||
if self.sync_workfile_version:
|
||||
data["version"] = context.data["version"]
|
||||
for instance in context:
|
||||
if instance.data['family'] == "workfile":
|
||||
instance.data["version"] = context.data["version"]
|
||||
for _instance in context:
|
||||
if _instance.data['family'] == "workfile":
|
||||
_instance.data["version"] = context.data["version"]
|
||||
|
||||
# Define nice label
|
||||
label = "{0} ({1})".format(layer_name, instance.data["asset"])
|
||||
|
|
|
|||
|
|
@ -2076,9 +2076,16 @@ class WorkfileSettings(object):
|
|||
str(workfile_settings["OCIO_config"]))
|
||||
|
||||
else:
|
||||
# set values to root
|
||||
# OCIO config path is defined from prelaunch hook
|
||||
self._root_node["colorManagement"].setValue("OCIO")
|
||||
|
||||
# print previous settings in case some were found in workfile
|
||||
residual_path = self._root_node["customOCIOConfigPath"].value()
|
||||
if residual_path:
|
||||
log.info("Residual OCIO config path found: `{}`".format(
|
||||
residual_path
|
||||
))
|
||||
|
||||
# we dont need the key anymore
|
||||
workfile_settings.pop("customOCIOConfigPath", None)
|
||||
workfile_settings.pop("colorManagement", None)
|
||||
|
|
@ -2100,9 +2107,35 @@ class WorkfileSettings(object):
|
|||
|
||||
# set ocio config path
|
||||
if config_data:
|
||||
current_ocio_path = os.getenv("OCIO")
|
||||
if current_ocio_path != config_data["path"]:
|
||||
message = """
|
||||
log.info("OCIO config path found: `{}`".format(
|
||||
config_data["path"]))
|
||||
|
||||
# check if there's a mismatch between environment and settings
|
||||
correct_settings = self._is_settings_matching_environment(
|
||||
config_data)
|
||||
|
||||
# if there's no mismatch between environment and settings
|
||||
if correct_settings:
|
||||
self._set_ocio_config_path_to_workfile(config_data)
|
||||
|
||||
def _is_settings_matching_environment(self, config_data):
|
||||
""" Check if OCIO config path is different from environment
|
||||
|
||||
Args:
|
||||
config_data (dict): OCIO config data from settings
|
||||
|
||||
Returns:
|
||||
bool: True if settings are matching environment, False otherwise
|
||||
"""
|
||||
current_ocio_path = os.environ["OCIO"]
|
||||
settings_ocio_path = config_data["path"]
|
||||
|
||||
# normalize all paths to forward slashes
|
||||
current_ocio_path = current_ocio_path.replace("\\", "/")
|
||||
settings_ocio_path = settings_ocio_path.replace("\\", "/")
|
||||
|
||||
if current_ocio_path != settings_ocio_path:
|
||||
message = """
|
||||
It seems like there's a mismatch between the OCIO config path set in your Nuke
|
||||
settings and the actual path set in your OCIO environment.
|
||||
|
||||
|
|
@ -2120,12 +2153,118 @@ Please note the paths for your reference:
|
|||
|
||||
Reopening Nuke should synchronize these paths and resolve any discrepancies.
|
||||
"""
|
||||
nuke.message(
|
||||
message.format(
|
||||
env_path=current_ocio_path,
|
||||
settings_path=config_data["path"]
|
||||
)
|
||||
nuke.message(
|
||||
message.format(
|
||||
env_path=current_ocio_path,
|
||||
settings_path=settings_ocio_path
|
||||
)
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _set_ocio_config_path_to_workfile(self, config_data):
|
||||
""" Set OCIO config path to workfile
|
||||
|
||||
Path set into nuke workfile. It is trying to replace path with
|
||||
environment variable if possible. If not, it will set it as it is.
|
||||
It also saves the script to apply the change, but only if it's not
|
||||
empty Untitled script.
|
||||
|
||||
Args:
|
||||
config_data (dict): OCIO config data from settings
|
||||
|
||||
"""
|
||||
# replace path with env var if possible
|
||||
ocio_path = self._replace_ocio_path_with_env_var(config_data)
|
||||
|
||||
log.info("Setting OCIO config path to: `{}`".format(
|
||||
ocio_path))
|
||||
|
||||
self._root_node["customOCIOConfigPath"].setValue(
|
||||
ocio_path
|
||||
)
|
||||
self._root_node["OCIO_config"].setValue("custom")
|
||||
|
||||
# only save script if it's not empty
|
||||
if self._root_node["name"].value() != "":
|
||||
log.info("Saving script to apply OCIO config path change.")
|
||||
nuke.scriptSave()
|
||||
|
||||
def _get_included_vars(self, config_template):
|
||||
""" Get all environment variables included in template
|
||||
|
||||
Args:
|
||||
config_template (str): OCIO config template from settings
|
||||
|
||||
Returns:
|
||||
list: list of environment variables included in template
|
||||
"""
|
||||
# resolve all environments for whitelist variables
|
||||
included_vars = [
|
||||
"BUILTIN_OCIO_ROOT",
|
||||
]
|
||||
|
||||
# include all project root related env vars
|
||||
for env_var in os.environ:
|
||||
if env_var.startswith("OPENPYPE_PROJECT_ROOT_"):
|
||||
included_vars.append(env_var)
|
||||
|
||||
# use regex to find env var in template with format {ENV_VAR}
|
||||
# this way we make sure only template used env vars are included
|
||||
env_var_regex = r"\{([A-Z0-9_]+)\}"
|
||||
env_var = re.findall(env_var_regex, config_template)
|
||||
if env_var:
|
||||
included_vars.append(env_var[0])
|
||||
|
||||
return included_vars
|
||||
|
||||
def _replace_ocio_path_with_env_var(self, config_data):
|
||||
""" Replace OCIO config path with environment variable
|
||||
|
||||
Environment variable is added as TCL expression to path. TCL expression
|
||||
is also replacing backward slashes found in path for windows
|
||||
formatted values.
|
||||
|
||||
Args:
|
||||
config_data (str): OCIO config dict from settings
|
||||
|
||||
Returns:
|
||||
str: OCIO config path with environment variable TCL expression
|
||||
"""
|
||||
config_path = config_data["path"]
|
||||
config_template = config_data["template"]
|
||||
|
||||
included_vars = self._get_included_vars(config_template)
|
||||
|
||||
# make sure we return original path if no env var is included
|
||||
new_path = config_path
|
||||
|
||||
for env_var in included_vars:
|
||||
env_path = os.getenv(env_var)
|
||||
if not env_path:
|
||||
continue
|
||||
|
||||
# it has to be directory current process can see
|
||||
if not os.path.isdir(env_path):
|
||||
continue
|
||||
|
||||
# make sure paths are in same format
|
||||
env_path = env_path.replace("\\", "/")
|
||||
path = config_path.replace("\\", "/")
|
||||
|
||||
# check if env_path is in path and replace to first found positive
|
||||
if env_path in path:
|
||||
# with regsub we make sure path format of slashes is correct
|
||||
resub_expr = (
|
||||
"[regsub -all {{\\\\}} [getenv {}] \"/\"]").format(env_var)
|
||||
|
||||
new_path = path.replace(
|
||||
env_path, resub_expr
|
||||
)
|
||||
break
|
||||
|
||||
return new_path
|
||||
|
||||
def set_writes_colorspace(self):
|
||||
''' Adds correct colorspace to write node dict
|
||||
|
|
@ -2239,7 +2378,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies.
|
|||
knobs["to"]))
|
||||
|
||||
def set_colorspace(self):
|
||||
''' Setting colorpace following presets
|
||||
''' Setting colorspace following presets
|
||||
'''
|
||||
# get imageio
|
||||
nuke_colorspace = get_nuke_imageio_settings()
|
||||
|
|
@ -2247,17 +2386,16 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies.
|
|||
log.info("Setting colorspace to workfile...")
|
||||
try:
|
||||
self.set_root_colorspace(nuke_colorspace)
|
||||
except AttributeError:
|
||||
msg = "set_colorspace(): missing `workfile` settings in template"
|
||||
except AttributeError as _error:
|
||||
msg = "Set Colorspace to workfile error: {}".format(_error)
|
||||
nuke.message(msg)
|
||||
|
||||
log.info("Setting colorspace to viewers...")
|
||||
try:
|
||||
self.set_viewers_colorspace(nuke_colorspace["viewer"])
|
||||
except AttributeError:
|
||||
msg = "set_colorspace(): missing `viewer` settings in template"
|
||||
except AttributeError as _error:
|
||||
msg = "Set Colorspace to viewer error: {}".format(_error)
|
||||
nuke.message(msg)
|
||||
log.error(msg)
|
||||
|
||||
log.info("Setting colorspace to write nodes...")
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -114,6 +114,11 @@ class NukePlaceholderPlugin(PlaceholderPlugin):
|
|||
placeholder_data[key] = value
|
||||
return placeholder_data
|
||||
|
||||
def delete_placeholder(self, placeholder):
|
||||
"""Remove placeholder if building was successful"""
|
||||
placeholder_node = nuke.toNode(placeholder.scene_identifier)
|
||||
nuke.delete(placeholder_node)
|
||||
|
||||
|
||||
class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin):
|
||||
identifier = "nuke.load"
|
||||
|
|
@ -276,14 +281,6 @@ class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin):
|
|||
placeholder.data["nb_children"] += 1
|
||||
reset_selection()
|
||||
|
||||
# remove placeholders marked as delete
|
||||
if (
|
||||
placeholder.data.get("delete")
|
||||
and not placeholder.data.get("keep_placeholder")
|
||||
):
|
||||
self.log.debug("Deleting node: {}".format(placeholder_node.name()))
|
||||
nuke.delete(placeholder_node)
|
||||
|
||||
# go back to root group
|
||||
nuke.root().begin()
|
||||
|
||||
|
|
@ -690,14 +687,6 @@ class NukePlaceholderCreatePlugin(
|
|||
placeholder.data["nb_children"] += 1
|
||||
reset_selection()
|
||||
|
||||
# remove placeholders marked as delete
|
||||
if (
|
||||
placeholder.data.get("delete")
|
||||
and not placeholder.data.get("keep_placeholder")
|
||||
):
|
||||
self.log.debug("Deleting node: {}".format(placeholder_node.name()))
|
||||
nuke.delete(placeholder_node)
|
||||
|
||||
# go back to root group
|
||||
nuke.root().begin()
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,8 @@ class LoadImage(load.LoaderPlugin):
|
|||
|
||||
file = file.replace("\\", "/")
|
||||
|
||||
repr_cont = context["representation"]["context"]
|
||||
representation = context["representation"]
|
||||
repr_cont = representation["context"]
|
||||
frame = repr_cont.get("frame")
|
||||
if frame:
|
||||
padding = len(frame)
|
||||
|
|
@ -104,16 +105,7 @@ class LoadImage(load.LoaderPlugin):
|
|||
frame,
|
||||
format(frame_number, "0{}".format(padding)))
|
||||
|
||||
name_data = {
|
||||
"asset": repr_cont["asset"],
|
||||
"subset": repr_cont["subset"],
|
||||
"representation": context["representation"]["name"],
|
||||
"ext": repr_cont["representation"],
|
||||
"id": context["representation"]["_id"],
|
||||
"class_name": self.__class__.__name__
|
||||
}
|
||||
|
||||
read_name = self.node_name_template.format(**name_data)
|
||||
read_name = self._get_node_name(representation)
|
||||
|
||||
# Create the Loader with the filename path set
|
||||
with viewer_update_and_undo_stop():
|
||||
|
|
@ -212,6 +204,8 @@ class LoadImage(load.LoaderPlugin):
|
|||
last = first = int(frame_number)
|
||||
|
||||
# Set the global in to the start frame of the sequence
|
||||
read_name = self._get_node_name(representation)
|
||||
node["name"].setValue(read_name)
|
||||
node["file"].setValue(file)
|
||||
node["origfirst"].setValue(first)
|
||||
node["first"].setValue(first)
|
||||
|
|
@ -250,3 +244,17 @@ class LoadImage(load.LoaderPlugin):
|
|||
|
||||
with viewer_update_and_undo_stop():
|
||||
nuke.delete(node)
|
||||
|
||||
def _get_node_name(self, representation):
|
||||
|
||||
repre_cont = representation["context"]
|
||||
name_data = {
|
||||
"asset": repre_cont["asset"],
|
||||
"subset": repre_cont["subset"],
|
||||
"representation": representation["name"],
|
||||
"ext": repre_cont["representation"],
|
||||
"id": representation["_id"],
|
||||
"class_name": self.__class__.__name__
|
||||
}
|
||||
|
||||
return self.node_name_template.format(**name_data)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ class ExtractThumbnail(publish.Extractor):
|
|||
def render_thumbnail(self, instance, output_name=None, **kwargs):
|
||||
first_frame = instance.data["frameStartHandle"]
|
||||
last_frame = instance.data["frameEndHandle"]
|
||||
colorspace = instance.data["colorspace"]
|
||||
|
||||
# find frame range and define middle thumb frame
|
||||
mid_frame = int((last_frame - first_frame) / 2)
|
||||
|
|
@ -112,8 +113,8 @@ class ExtractThumbnail(publish.Extractor):
|
|||
if self.use_rendered and os.path.isfile(path_render):
|
||||
# check if file exist otherwise connect to write node
|
||||
rnode = nuke.createNode("Read")
|
||||
|
||||
rnode["file"].setValue(path_render)
|
||||
rnode["colorspace"].setValue(colorspace)
|
||||
|
||||
# turn it raw if none of baking is ON
|
||||
if all([
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ Provides:
|
|||
import pyblish.api
|
||||
|
||||
from openpype.client import get_last_version_by_subset_name
|
||||
from openpype.pipeline.version_start import get_versioning_start
|
||||
|
||||
|
||||
class CollectPublishedVersion(pyblish.api.ContextPlugin):
|
||||
|
|
@ -47,9 +48,17 @@ class CollectPublishedVersion(pyblish.api.ContextPlugin):
|
|||
version_doc = get_last_version_by_subset_name(project_name,
|
||||
workfile_subset_name,
|
||||
asset_id)
|
||||
version_int = 1
|
||||
|
||||
if version_doc:
|
||||
version_int += int(version_doc["name"])
|
||||
version_int = int(version_doc["name"]) + 1
|
||||
else:
|
||||
version_int = get_versioning_start(
|
||||
project_name,
|
||||
"photoshop",
|
||||
task_name=context.data["task"],
|
||||
task_type=context.data["taskType"],
|
||||
project_settings=context.data["project_settings"]
|
||||
)
|
||||
|
||||
self.log.debug(f"Setting {version_int} to context.")
|
||||
context.data["version"] = version_int
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ def get_layers_pre_post_behavior(layer_ids, communicator=None):
|
|||
|
||||
Pre and Post behaviors is enumerator of possible values:
|
||||
- "none"
|
||||
- "repeat" / "loop"
|
||||
- "repeat"
|
||||
- "pingpong"
|
||||
- "hold"
|
||||
|
||||
|
|
@ -242,7 +242,7 @@ def get_layers_pre_post_behavior(layer_ids, communicator=None):
|
|||
{
|
||||
0: {
|
||||
"pre": "none",
|
||||
"post": "loop"
|
||||
"post": "repeat"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -77,13 +77,15 @@ def _calculate_pre_behavior_copy(
|
|||
for frame_idx in range(range_start, layer_frame_start):
|
||||
output_idx_by_frame_idx[frame_idx] = first_exposure_frame
|
||||
|
||||
elif pre_beh in ("loop", "repeat"):
|
||||
elif pre_beh == "repeat":
|
||||
# Loop backwards from last frame of layer
|
||||
for frame_idx in reversed(range(range_start, layer_frame_start)):
|
||||
eq_frame_idx_offset = (
|
||||
(layer_frame_end - frame_idx) % frame_count
|
||||
)
|
||||
eq_frame_idx = layer_frame_end - eq_frame_idx_offset
|
||||
eq_frame_idx = layer_frame_start + (
|
||||
layer_frame_end - eq_frame_idx_offset
|
||||
)
|
||||
output_idx_by_frame_idx[frame_idx] = eq_frame_idx
|
||||
|
||||
elif pre_beh == "pingpong":
|
||||
|
|
@ -139,10 +141,10 @@ def _calculate_post_behavior_copy(
|
|||
for frame_idx in range(layer_frame_end + 1, range_end + 1):
|
||||
output_idx_by_frame_idx[frame_idx] = last_exposure_frame
|
||||
|
||||
elif post_beh in ("loop", "repeat"):
|
||||
elif post_beh == "repeat":
|
||||
# Loop backwards from last frame of layer
|
||||
for frame_idx in range(layer_frame_end + 1, range_end + 1):
|
||||
eq_frame_idx = frame_idx % frame_count
|
||||
eq_frame_idx = layer_frame_start + (frame_idx % frame_count)
|
||||
output_idx_by_frame_idx[frame_idx] = eq_frame_idx
|
||||
|
||||
elif post_beh == "pingpong":
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from openpype.hosts.tvpaint.api.lib import (
|
|||
from openpype.hosts.tvpaint.api.pipeline import (
|
||||
get_current_workfile_context,
|
||||
)
|
||||
from openpype.pipeline.version_start import get_versioning_start
|
||||
|
||||
|
||||
class LoadWorkfile(plugin.Loader):
|
||||
|
|
@ -95,7 +96,13 @@ class LoadWorkfile(plugin.Loader):
|
|||
)[1]
|
||||
|
||||
if version is None:
|
||||
version = 1
|
||||
version = get_versioning_start(
|
||||
project_name,
|
||||
"tvpaint",
|
||||
task_name=task_name,
|
||||
task_type=data["task"]["type"],
|
||||
family="workfile"
|
||||
)
|
||||
else:
|
||||
version += 1
|
||||
|
||||
|
|
|
|||
|
|
@ -76,11 +76,16 @@ class AnimationAlembicLoader(plugin.Loader):
|
|||
asset_name = "{}_{}".format(asset, name)
|
||||
else:
|
||||
asset_name = "{}".format(name)
|
||||
version = context.get('version').get('name')
|
||||
version = context.get('version')
|
||||
# Check if version is hero version and use different name
|
||||
if not version.get("name") and version.get('type') == "hero_version":
|
||||
name_version = f"{name}_hero"
|
||||
else:
|
||||
name_version = f"{name}_v{version.get('name'):03d}"
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{root}/{asset}/{name}_v{version:03d}", suffix="")
|
||||
f"{root}/{asset}/{name_version}", suffix="")
|
||||
|
||||
container_name += suffix
|
||||
|
||||
|
|
|
|||
|
|
@ -78,11 +78,16 @@ class SkeletalMeshAlembicLoader(plugin.Loader):
|
|||
asset_name = "{}_{}".format(asset, name)
|
||||
else:
|
||||
asset_name = "{}".format(name)
|
||||
version = context.get('version').get('name')
|
||||
version = context.get('version')
|
||||
# Check if version is hero version and use different name
|
||||
if not version.get("name") and version.get('type') == "hero_version":
|
||||
name_version = f"{name}_hero"
|
||||
else:
|
||||
name_version = f"{name}_v{version.get('name'):03d}"
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{root}/{asset}/{name}_v{version:03d}", suffix="")
|
||||
f"{root}/{asset}/{name_version}", suffix="")
|
||||
|
||||
container_name += suffix
|
||||
|
||||
|
|
|
|||
|
|
@ -52,11 +52,16 @@ class SkeletalMeshFBXLoader(plugin.Loader):
|
|||
asset_name = "{}_{}".format(asset, name)
|
||||
else:
|
||||
asset_name = "{}".format(name)
|
||||
version = context.get('version').get('name')
|
||||
version = context.get('version')
|
||||
# Check if version is hero version and use different name
|
||||
if not version.get("name") and version.get('type') == "hero_version":
|
||||
name_version = f"{name}_hero"
|
||||
else:
|
||||
name_version = f"{name}_v{version.get('name'):03d}"
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{root}/{asset}/{name}_v{version:03d}", suffix="")
|
||||
f"{root}/{asset}/{name_version}", suffix="")
|
||||
|
||||
container_name += suffix
|
||||
|
||||
|
|
|
|||
|
|
@ -79,11 +79,13 @@ class StaticMeshAlembicLoader(plugin.Loader):
|
|||
root = "/Game/Ayon/Assets"
|
||||
asset = context.get('asset').get('name')
|
||||
suffix = "_CON"
|
||||
if asset:
|
||||
asset_name = "{}_{}".format(asset, name)
|
||||
asset_name = f"{asset}_{name}" if asset else f"{name}"
|
||||
version = context.get('version')
|
||||
# Check if version is hero version and use different name
|
||||
if not version.get("name") and version.get('type') == "hero_version":
|
||||
name_version = f"{name}_hero"
|
||||
else:
|
||||
asset_name = "{}".format(name)
|
||||
version = context.get('version').get('name')
|
||||
name_version = f"{name}_v{version.get('name'):03d}"
|
||||
|
||||
default_conversion = False
|
||||
if options.get("default_conversion"):
|
||||
|
|
@ -91,7 +93,7 @@ class StaticMeshAlembicLoader(plugin.Loader):
|
|||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{root}/{asset}/{name}_v{version:03d}", suffix="")
|
||||
f"{root}/{asset}/{name_version}", suffix="")
|
||||
|
||||
container_name += suffix
|
||||
|
||||
|
|
|
|||
|
|
@ -78,10 +78,16 @@ class StaticMeshFBXLoader(plugin.Loader):
|
|||
asset_name = "{}_{}".format(asset, name)
|
||||
else:
|
||||
asset_name = "{}".format(name)
|
||||
version = context.get('version')
|
||||
# Check if version is hero version and use different name
|
||||
if not version.get("name") and version.get('type') == "hero_version":
|
||||
name_version = f"{name}_hero"
|
||||
else:
|
||||
name_version = f"{name}_v{version.get('name'):03d}"
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{root}/{asset}/{name}", suffix=""
|
||||
f"{root}/{asset}/{name_version}", suffix=""
|
||||
)
|
||||
|
||||
container_name += suffix
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from openpype.lib import (
|
|||
)
|
||||
from openpype.pipeline.create import get_subset_name
|
||||
from openpype_modules.webpublisher.lib import parse_json
|
||||
from openpype.pipeline.version_start import get_versioning_start
|
||||
|
||||
|
||||
class CollectPublishedFiles(pyblish.api.ContextPlugin):
|
||||
|
|
@ -103,7 +104,13 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
|
|||
project_settings=context.data["project_settings"]
|
||||
)
|
||||
version = self._get_next_version(
|
||||
project_name, asset_doc, subset_name
|
||||
project_name,
|
||||
asset_doc,
|
||||
task_name,
|
||||
task_type,
|
||||
family,
|
||||
subset_name,
|
||||
context
|
||||
)
|
||||
next_versions.append(version)
|
||||
|
||||
|
|
@ -141,8 +148,9 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
|
|||
try:
|
||||
no_of_frames = self._get_number_of_frames(file_url)
|
||||
if no_of_frames:
|
||||
frame_end = int(frame_start) + \
|
||||
math.ceil(no_of_frames)
|
||||
frame_end = (
|
||||
int(frame_start) + math.ceil(no_of_frames)
|
||||
)
|
||||
frame_end = math.ceil(frame_end) - 1
|
||||
instance.data["frameEnd"] = frame_end
|
||||
self.log.debug("frameEnd:: {}".format(
|
||||
|
|
@ -270,7 +278,16 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
|
|||
config["families"],
|
||||
config["tags"])
|
||||
|
||||
def _get_next_version(self, project_name, asset_doc, subset_name):
|
||||
def _get_next_version(
|
||||
self,
|
||||
project_name,
|
||||
asset_doc,
|
||||
task_name,
|
||||
task_type,
|
||||
family,
|
||||
subset_name,
|
||||
context
|
||||
):
|
||||
"""Returns version number or 1 for 'asset' and 'subset'"""
|
||||
|
||||
version_doc = get_last_version_by_subset_name(
|
||||
|
|
@ -279,9 +296,19 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
|
|||
asset_doc["_id"],
|
||||
fields=["name"]
|
||||
)
|
||||
version = 1
|
||||
if version_doc:
|
||||
version += int(version_doc["name"])
|
||||
version = int(version_doc["name"]) + 1
|
||||
else:
|
||||
version = get_versioning_start(
|
||||
project_name,
|
||||
"webpublisher",
|
||||
task_name=task_name,
|
||||
task_type=task_type,
|
||||
family=family,
|
||||
subset=subset_name,
|
||||
project_settings=context.data["project_settings"]
|
||||
)
|
||||
|
||||
return version
|
||||
|
||||
def _get_number_of_frames(self, file_url):
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import os
|
||||
import json
|
||||
import re
|
||||
from copy import copy, deepcopy
|
||||
from copy import deepcopy
|
||||
import requests
|
||||
import clique
|
||||
|
||||
|
|
@ -16,6 +16,7 @@ from openpype.client import (
|
|||
from openpype.pipeline import publish, legacy_io
|
||||
from openpype.lib import EnumDef, is_running_from_build
|
||||
from openpype.tests.lib import is_in_tests
|
||||
from openpype.pipeline.version_start import get_versioning_start
|
||||
|
||||
from openpype.pipeline.farm.pyblish_functions import (
|
||||
create_skeleton_instance,
|
||||
|
|
@ -211,7 +212,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
|
|||
environment["OPENPYPE_PUBLISH_JOB"] = "1"
|
||||
environment["OPENPYPE_RENDER_JOB"] = "0"
|
||||
environment["OPENPYPE_REMOTE_PUBLISH"] = "0"
|
||||
deadline_plugin = "Openpype"
|
||||
deadline_plugin = "OpenPype"
|
||||
# Add OpenPype version if we are running from build.
|
||||
if is_running_from_build():
|
||||
self.environ_keys.append("OPENPYPE_VERSION")
|
||||
|
|
@ -568,7 +569,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
|
|||
if version:
|
||||
version = int(version["name"]) + 1
|
||||
else:
|
||||
version = 1
|
||||
version = get_versioning_start(
|
||||
project_name,
|
||||
template_data["app"],
|
||||
task_name=template_data["task"]["name"],
|
||||
task_type=template_data["task"]["type"],
|
||||
family="render",
|
||||
subset=subset,
|
||||
project_settings=context.data["project_settings"]
|
||||
)
|
||||
|
||||
host_name = context.data["hostName"]
|
||||
task_info = template_data.get("task") or {}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class AyonDeadlinePlugin(DeadlinePlugin):
|
|||
for publish process.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.InitializeProcessCallback += self.InitializeProcess
|
||||
self.RenderExecutableCallback += self.RenderExecutable
|
||||
self.RenderArgumentCallback += self.RenderArgument
|
||||
|
|
|
|||
|
|
@ -8,13 +8,14 @@ from Deadline.Scripting import *
|
|||
|
||||
def GetDeadlinePlugin():
|
||||
return HarmonyOpenPypePlugin()
|
||||
|
||||
|
||||
def CleanupDeadlinePlugin( deadlinePlugin ):
|
||||
deadlinePlugin.Cleanup()
|
||||
|
||||
|
||||
class HarmonyOpenPypePlugin( DeadlinePlugin ):
|
||||
|
||||
def __init__( self ):
|
||||
super().__init__()
|
||||
self.InitializeProcessCallback += self.InitializeProcess
|
||||
self.RenderExecutableCallback += self.RenderExecutable
|
||||
self.RenderArgumentCallback += self.RenderArgument
|
||||
|
|
@ -24,11 +25,11 @@ class HarmonyOpenPypePlugin( DeadlinePlugin ):
|
|||
print("Cleanup")
|
||||
for stdoutHandler in self.StdoutHandlers:
|
||||
del stdoutHandler.HandleCallback
|
||||
|
||||
|
||||
del self.InitializeProcessCallback
|
||||
del self.RenderExecutableCallback
|
||||
del self.RenderArgumentCallback
|
||||
|
||||
|
||||
def CheckExitCode( self, exitCode ):
|
||||
print("check code")
|
||||
if exitCode != 0:
|
||||
|
|
@ -36,20 +37,20 @@ class HarmonyOpenPypePlugin( DeadlinePlugin ):
|
|||
self.LogInfo( "Renderer reported an error with error code 100. This will be ignored, since the option to ignore it is specified in the Job Properties." )
|
||||
else:
|
||||
self.FailRender( "Renderer returned non-zero error code %d. Check the renderer's output." % exitCode )
|
||||
|
||||
|
||||
def InitializeProcess( self ):
|
||||
self.PluginType = PluginType.Simple
|
||||
self.StdoutHandling = True
|
||||
self.PopupHandling = True
|
||||
|
||||
|
||||
self.AddStdoutHandlerCallback( "Rendered frame ([0-9]+)" ).HandleCallback += self.HandleStdoutProgress
|
||||
|
||||
|
||||
def HandleStdoutProgress( self ):
|
||||
startFrame = self.GetStartFrame()
|
||||
endFrame = self.GetEndFrame()
|
||||
if( endFrame - startFrame + 1 != 0 ):
|
||||
self.SetProgress( 100 * ( int(self.GetRegexMatch(1)) - startFrame + 1 ) / ( endFrame - startFrame + 1 ) )
|
||||
|
||||
|
||||
def RenderExecutable( self ):
|
||||
version = int( self.GetPluginInfoEntry( "Version" ) )
|
||||
exe = ""
|
||||
|
|
@ -58,7 +59,7 @@ class HarmonyOpenPypePlugin( DeadlinePlugin ):
|
|||
if( exe == "" ):
|
||||
self.FailRender( "Harmony render executable was not found in the configured separated list \"" + exeList + "\". The path to the render executable can be configured from the Plugin Configuration in the Deadline Monitor." )
|
||||
return exe
|
||||
|
||||
|
||||
def RenderArgument( self ):
|
||||
renderArguments = "-batch"
|
||||
|
||||
|
|
@ -72,20 +73,20 @@ class HarmonyOpenPypePlugin( DeadlinePlugin ):
|
|||
resolutionX = self.GetIntegerPluginInfoEntryWithDefault( "ResolutionX", -1 )
|
||||
resolutionY = self.GetIntegerPluginInfoEntryWithDefault( "ResolutionY", -1 )
|
||||
fov = self.GetFloatPluginInfoEntryWithDefault( "FieldOfView", -1 )
|
||||
|
||||
|
||||
if resolutionX > 0 and resolutionY > 0 and fov > 0:
|
||||
renderArguments += " -res " + str( resolutionX ) + " " + str( resolutionY ) + " " + str( fov )
|
||||
|
||||
|
||||
camera = self.GetPluginInfoEntryWithDefault( "Camera", "" )
|
||||
|
||||
|
||||
if not camera == "":
|
||||
renderArguments += " -camera " + camera
|
||||
|
||||
|
||||
startFrame = str( self.GetStartFrame() )
|
||||
endFrame = str( self.GetEndFrame() )
|
||||
|
||||
|
||||
renderArguments += " -frames " + startFrame + " " + endFrame
|
||||
|
||||
|
||||
if not self.GetBooleanPluginInfoEntryWithDefault( "IsDatabase", False ):
|
||||
sceneFilename = self.GetPluginInfoEntryWithDefault( "SceneFile", self.GetDataFilename() )
|
||||
sceneFilename = RepositoryUtils.CheckPathMapping( sceneFilename )
|
||||
|
|
@ -99,12 +100,12 @@ class HarmonyOpenPypePlugin( DeadlinePlugin ):
|
|||
renderArguments += " -scene " + scene
|
||||
version = self.GetPluginInfoEntryWithDefault( "SceneVersion", "" )
|
||||
renderArguments += " -version " + version
|
||||
|
||||
|
||||
#tempSceneDirectory = self.CreateTempDirectory( "thread" + str(self.GetThreadNumber()) )
|
||||
#preRenderScript =
|
||||
#preRenderScript =
|
||||
rendernodeNum = 0
|
||||
scriptBuilder = StringBuilder()
|
||||
|
||||
|
||||
while True:
|
||||
nodeName = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "Node", "" )
|
||||
if nodeName == "":
|
||||
|
|
@ -115,35 +116,35 @@ class HarmonyOpenPypePlugin( DeadlinePlugin ):
|
|||
nodeLeadingZero = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "LeadingZero", "" )
|
||||
nodeFormat = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "Format", "" )
|
||||
nodeStartFrame = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "StartFrame", "" )
|
||||
|
||||
|
||||
if not nodePath == "":
|
||||
scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"drawingName\", 1, \"" + nodePath + "\" );")
|
||||
|
||||
|
||||
if not nodeLeadingZero == "":
|
||||
scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"leadingZeros\", 1, \"" + nodeLeadingZero + "\" );")
|
||||
|
||||
|
||||
if not nodeFormat == "":
|
||||
scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"drawingType\", 1, \"" + nodeFormat + "\" );")
|
||||
|
||||
|
||||
if not nodeStartFrame == "":
|
||||
scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"start\", 1, \"" + nodeStartFrame + "\" );")
|
||||
|
||||
|
||||
if nodeType == "Movie":
|
||||
nodePath = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "Path", "" )
|
||||
if not nodePath == "":
|
||||
scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"moviePath\", 1, \"" + nodePath + "\" );")
|
||||
|
||||
|
||||
rendernodeNum += 1
|
||||
|
||||
|
||||
tempDirectory = self.CreateTempDirectory( "thread" + str(self.GetThreadNumber()) )
|
||||
preRenderScriptName = Path.Combine( tempDirectory, "preRenderScript.txt" )
|
||||
|
||||
|
||||
File.WriteAllText( preRenderScriptName, scriptBuilder.ToString() )
|
||||
|
||||
|
||||
preRenderInlineScript = self.GetPluginInfoEntryWithDefault( "PreRenderInlineScript", "" )
|
||||
if preRenderInlineScript:
|
||||
renderArguments += " -preRenderInlineScript \"" + preRenderInlineScript +"\""
|
||||
|
||||
|
||||
renderArguments += " -preRenderScript \"" + preRenderScriptName +"\""
|
||||
|
||||
|
||||
return renderArguments
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin):
|
|||
for publish process.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.InitializeProcessCallback += self.InitializeProcess
|
||||
self.RenderExecutableCallback += self.RenderExecutable
|
||||
self.RenderArgumentCallback += self.RenderArgument
|
||||
|
|
@ -107,7 +108,7 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin):
|
|||
"Scanning for compatible requested "
|
||||
f"version {requested_version}"))
|
||||
dir_list = self.GetConfigEntry("OpenPypeInstallationDirs")
|
||||
|
||||
|
||||
# clean '\ ' for MacOS pasting
|
||||
if platform.system().lower() == "darwin":
|
||||
dir_list = dir_list.replace("\\ ", " ")
|
||||
|
|
|
|||
|
|
@ -249,6 +249,7 @@ class OpenPypeTileAssembler(DeadlinePlugin):
|
|||
|
||||
def __init__(self):
|
||||
"""Init."""
|
||||
super().__init__()
|
||||
self.InitializeProcessCallback += self.initialize_process
|
||||
self.RenderExecutableCallback += self.render_executable
|
||||
self.RenderArgumentCallback += self.render_argument
|
||||
|
|
|
|||
|
|
@ -11,10 +11,8 @@ Provides:
|
|||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import collections
|
||||
|
||||
import six
|
||||
import pyblish.api
|
||||
import clique
|
||||
|
||||
|
|
|
|||
|
|
@ -116,6 +116,18 @@ class CopyLastPublishedWorkfile(PreLaunchHook):
|
|||
"task": {"name": task_name, "type": task_type}
|
||||
}
|
||||
|
||||
# Add version filter
|
||||
workfile_version = self.launch_context.data.get("workfile_version", -1)
|
||||
if workfile_version > 0 and workfile_version not in {None, "last"}:
|
||||
context_filters["version"] = self.launch_context.data[
|
||||
"workfile_version"
|
||||
]
|
||||
|
||||
# Only one version will be matched
|
||||
version_index = 0
|
||||
else:
|
||||
version_index = workfile_version
|
||||
|
||||
workfile_representations = list(get_representations(
|
||||
project_name,
|
||||
context_filters=context_filters
|
||||
|
|
@ -133,9 +145,10 @@ class CopyLastPublishedWorkfile(PreLaunchHook):
|
|||
lambda r: r["context"].get("version") is not None,
|
||||
workfile_representations
|
||||
)
|
||||
workfile_representation = max(
|
||||
# Get workfile version
|
||||
workfile_representation = sorted(
|
||||
filtered_repres, key=lambda r: r["context"]["version"]
|
||||
)
|
||||
)[version_index]
|
||||
|
||||
# Copy file and substitute path
|
||||
last_published_workfile_path = download_last_published_workfile(
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ from .context_tools import (
|
|||
get_current_host_name,
|
||||
get_current_project_name,
|
||||
get_current_asset_name,
|
||||
get_current_task_name,
|
||||
get_current_task_name
|
||||
)
|
||||
install = install_host
|
||||
uninstall = uninstall_host
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from openpype.client import (
|
|||
from openpype.lib.events import emit_event
|
||||
from openpype.modules import load_modules, ModulesManager
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.tests.lib import is_in_tests
|
||||
|
||||
from .publish.lib import filter_pyblish_plugins
|
||||
from .anatomy import Anatomy
|
||||
|
|
@ -35,7 +36,7 @@ from . import (
|
|||
register_inventory_action_path,
|
||||
register_creator_plugin_path,
|
||||
deregister_loader_plugin_path,
|
||||
deregister_inventory_action_path,
|
||||
deregister_inventory_action_path
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -142,6 +143,10 @@ def install_host(host):
|
|||
else:
|
||||
pyblish.api.register_target("local")
|
||||
|
||||
if is_in_tests():
|
||||
print("Registering pyblish target: automated")
|
||||
pyblish.api.register_target("automated")
|
||||
|
||||
project_name = os.environ.get("AVALON_PROJECT")
|
||||
host_name = os.environ.get("AVALON_APP")
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from .constants import (
|
|||
SUBSET_NAME_ALLOWED_SYMBOLS,
|
||||
DEFAULT_SUBSET_TEMPLATE,
|
||||
PRE_CREATE_THUMBNAIL_KEY,
|
||||
DEFAULT_VARIANT_VALUE,
|
||||
)
|
||||
|
||||
from .utils import (
|
||||
|
|
@ -50,6 +51,7 @@ __all__ = (
|
|||
"SUBSET_NAME_ALLOWED_SYMBOLS",
|
||||
"DEFAULT_SUBSET_TEMPLATE",
|
||||
"PRE_CREATE_THUMBNAIL_KEY",
|
||||
"DEFAULT_VARIANT_VALUE",
|
||||
|
||||
"get_last_versions_for_instances",
|
||||
"get_next_versions_for_instances",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
SUBSET_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_."
|
||||
DEFAULT_SUBSET_TEMPLATE = "{family}{Variant}"
|
||||
PRE_CREATE_THUMBNAIL_KEY = "thumbnail_source"
|
||||
DEFAULT_VARIANT_VALUE = "Main"
|
||||
|
||||
|
||||
__all__ = (
|
||||
"SUBSET_NAME_ALLOWED_SYMBOLS",
|
||||
"DEFAULT_SUBSET_TEMPLATE",
|
||||
"PRE_CREATE_THUMBNAIL_KEY",
|
||||
"DEFAULT_VARIANT_VALUE",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import os
|
||||
import copy
|
||||
import collections
|
||||
|
||||
|
|
@ -20,6 +19,7 @@ from openpype.pipeline.plugin_discover import (
|
|||
deregister_plugin_path
|
||||
)
|
||||
|
||||
from .constants import DEFAULT_VARIANT_VALUE
|
||||
from .subset_name import get_subset_name
|
||||
from .utils import get_next_versions_for_instances
|
||||
from .legacy_create import LegacyCreator
|
||||
|
|
@ -517,7 +517,7 @@ class Creator(BaseCreator):
|
|||
default_variants = []
|
||||
|
||||
# Default variant used in 'get_default_variant'
|
||||
default_variant = None
|
||||
_default_variant = None
|
||||
|
||||
# Short description of family
|
||||
# - may not be used if `get_description` is overriden
|
||||
|
|
@ -543,6 +543,21 @@ class Creator(BaseCreator):
|
|||
# - similar to instance attribute definitions
|
||||
pre_create_attr_defs = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
cls = self.__class__
|
||||
|
||||
# Fix backwards compatibility for plugins which override
|
||||
# 'default_variant' attribute directly
|
||||
if not isinstance(cls.default_variant, property):
|
||||
# Move value from 'default_variant' to '_default_variant'
|
||||
self._default_variant = self.default_variant
|
||||
# Create property 'default_variant' on the class
|
||||
cls.default_variant = property(
|
||||
cls._get_default_variant_wrap,
|
||||
cls._set_default_variant_wrap
|
||||
)
|
||||
super(Creator, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def show_order(self):
|
||||
"""Order in which is creator shown in UI.
|
||||
|
|
@ -595,10 +610,10 @@ class Creator(BaseCreator):
|
|||
def get_default_variants(self):
|
||||
"""Default variant values for UI tooltips.
|
||||
|
||||
Replacement of `defatults` attribute. Using method gives ability to
|
||||
have some "logic" other than attribute values.
|
||||
Replacement of `default_variants` attribute. Using method gives
|
||||
ability to have some "logic" other than attribute values.
|
||||
|
||||
By default returns `default_variants` value.
|
||||
By default, returns `default_variants` value.
|
||||
|
||||
Returns:
|
||||
List[str]: Whisper variants for user input.
|
||||
|
|
@ -606,17 +621,63 @@ class Creator(BaseCreator):
|
|||
|
||||
return copy.deepcopy(self.default_variants)
|
||||
|
||||
def get_default_variant(self):
|
||||
def get_default_variant(self, only_explicit=False):
|
||||
"""Default variant value that will be used to prefill variant input.
|
||||
|
||||
This is for user input and value may not be content of result from
|
||||
`get_default_variants`.
|
||||
|
||||
Can return `None`. In that case first element from
|
||||
`get_default_variants` should be used.
|
||||
Note:
|
||||
This method does not allow to have empty string as
|
||||
default variant.
|
||||
|
||||
Args:
|
||||
only_explicit (Optional[bool]): If True, only explicit default
|
||||
variant from '_default_variant' will be returned.
|
||||
|
||||
Returns:
|
||||
str: Variant value.
|
||||
"""
|
||||
|
||||
return self.default_variant
|
||||
if only_explicit or self._default_variant:
|
||||
return self._default_variant
|
||||
|
||||
for variant in self.get_default_variants():
|
||||
return variant
|
||||
return DEFAULT_VARIANT_VALUE
|
||||
|
||||
def _get_default_variant_wrap(self):
|
||||
"""Default variant value that will be used to prefill variant input.
|
||||
|
||||
Wrapper for 'get_default_variant'.
|
||||
|
||||
Notes:
|
||||
This method is wrapper for 'get_default_variant'
|
||||
for 'default_variant' property, so creator can override
|
||||
the method.
|
||||
|
||||
Returns:
|
||||
str: Variant value.
|
||||
"""
|
||||
|
||||
return self.get_default_variant()
|
||||
|
||||
def _set_default_variant_wrap(self, variant):
|
||||
"""Set default variant value.
|
||||
|
||||
This method is needed for automated settings overrides which are
|
||||
changing attributes based on keys in settings.
|
||||
|
||||
Args:
|
||||
variant (str): New default variant value.
|
||||
"""
|
||||
|
||||
self._default_variant = variant
|
||||
|
||||
default_variant = property(
|
||||
_get_default_variant_wrap,
|
||||
_set_default_variant_wrap
|
||||
)
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
"""Plugin attribute definitions needed for creation.
|
||||
|
|
|
|||
|
|
@ -116,8 +116,8 @@ def get_time_data_from_instance_or_context(instance):
|
|||
instance.context.data.get("fps")),
|
||||
handle_start=(instance.data.get("handleStart") or
|
||||
instance.context.data.get("handleStart")), # noqa: E501
|
||||
handle_end=(instance.data.get("handleStart") or
|
||||
instance.context.data.get("handleStart"))
|
||||
handle_end=(instance.data.get("handleEnd") or
|
||||
instance.context.data.get("handleEnd"))
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -568,9 +568,15 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
|
|||
col = list(cols[0])
|
||||
|
||||
# create subset name `familyTaskSubset_AOV`
|
||||
group_name = 'render{}{}{}{}'.format(
|
||||
task[0].upper(), task[1:],
|
||||
subset[0].upper(), subset[1:])
|
||||
# TODO refactor/remove me
|
||||
family = skeleton["family"]
|
||||
if not subset.startswith(family):
|
||||
group_name = '{}{}{}{}{}'.format(
|
||||
family,
|
||||
task[0].upper(), task[1:],
|
||||
subset[0].upper(), subset[1:])
|
||||
else:
|
||||
group_name = subset
|
||||
|
||||
# if there are multiple cameras, we need to add camera name
|
||||
if isinstance(col, (list, tuple)):
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import copy
|
|||
import logging
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.lib import Logger
|
||||
from openpype.client import get_project
|
||||
from . import legacy_io
|
||||
from .anatomy import Anatomy
|
||||
|
|
@ -11,13 +12,13 @@ from .plugin_discover import (
|
|||
register_plugin,
|
||||
register_plugin_path,
|
||||
)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_thumbnail_binary(thumbnail_entity, thumbnail_type, dbcon=None):
|
||||
if not thumbnail_entity:
|
||||
return
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
resolvers = discover_thumbnail_resolvers()
|
||||
resolvers = sorted(resolvers, key=lambda cls: cls.priority)
|
||||
if dbcon is None:
|
||||
|
|
@ -133,6 +134,16 @@ class BinaryThumbnail(ThumbnailResolver):
|
|||
|
||||
|
||||
class ServerThumbnailResolver(ThumbnailResolver):
|
||||
_cache = None
|
||||
|
||||
@classmethod
|
||||
def _get_cache(cls):
|
||||
if cls._cache is None:
|
||||
from openpype.client.server.thumbnails import AYONThumbnailCache
|
||||
|
||||
cls._cache = AYONThumbnailCache()
|
||||
return cls._cache
|
||||
|
||||
def process(self, thumbnail_entity, thumbnail_type):
|
||||
if not AYON_SERVER_ENABLED:
|
||||
return None
|
||||
|
|
@ -142,20 +153,40 @@ class ServerThumbnailResolver(ThumbnailResolver):
|
|||
if not entity_type or not entity_id:
|
||||
return None
|
||||
|
||||
from openpype.client.server.server_api import get_server_api_connection
|
||||
import ayon_api
|
||||
|
||||
project_name = self.dbcon.active_project()
|
||||
thumbnail_id = thumbnail_entity["_id"]
|
||||
con = get_server_api_connection()
|
||||
filepath = con.get_thumbnail(
|
||||
project_name, entity_type, entity_id, thumbnail_id
|
||||
)
|
||||
content = None
|
||||
|
||||
cache = self._get_cache()
|
||||
filepath = cache.get_thumbnail_filepath(project_name, thumbnail_id)
|
||||
if filepath:
|
||||
with open(filepath, "rb") as stream:
|
||||
content = stream.read()
|
||||
return stream.read()
|
||||
|
||||
return content
|
||||
# This is new way how thumbnails can be received from server
|
||||
# - output is 'ThumbnailContent' object
|
||||
if hasattr(ayon_api, "get_thumbnail_by_id"):
|
||||
result = ayon_api.get_thumbnail_by_id(thumbnail_id)
|
||||
if result.is_valid:
|
||||
filepath = cache.store_thumbnail(
|
||||
project_name,
|
||||
thumbnail_id,
|
||||
result.content,
|
||||
result.content_type
|
||||
)
|
||||
else:
|
||||
# Backwards compatibility for ayon api where 'get_thumbnail_by_id'
|
||||
# is not implemented and output is filepath
|
||||
filepath = ayon_api.get_thumbnail(
|
||||
project_name, entity_type, entity_id, thumbnail_id
|
||||
)
|
||||
|
||||
if not filepath:
|
||||
return None
|
||||
|
||||
with open(filepath, "rb") as stream:
|
||||
return stream.read()
|
||||
|
||||
|
||||
# Thumbnail resolvers
|
||||
|
|
|
|||
37
openpype/pipeline/version_start.py
Normal file
37
openpype/pipeline/version_start.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
from openpype.lib.profiles_filtering import filter_profiles
|
||||
from openpype.settings import get_project_settings
|
||||
|
||||
|
||||
def get_versioning_start(
|
||||
project_name,
|
||||
host_name,
|
||||
task_name=None,
|
||||
task_type=None,
|
||||
family=None,
|
||||
subset=None,
|
||||
project_settings=None,
|
||||
):
|
||||
"""Get anatomy versioning start"""
|
||||
if not project_settings:
|
||||
project_settings = get_project_settings(project_name)
|
||||
|
||||
version_start = 1
|
||||
settings = project_settings["global"]
|
||||
profiles = settings.get("version_start_category", {}).get("profiles", [])
|
||||
|
||||
if not profiles:
|
||||
return version_start
|
||||
|
||||
filtering_criteria = {
|
||||
"host_names": host_name,
|
||||
"families": family,
|
||||
"task_names": task_name,
|
||||
"task_types": task_type,
|
||||
"subsets": subset
|
||||
}
|
||||
profile = filter_profiles(profiles, filtering_criteria)
|
||||
|
||||
if profile is None:
|
||||
return version_start
|
||||
|
||||
return profile["version_start"]
|
||||
|
|
@ -10,7 +10,7 @@ from openpype.lib import (
|
|||
Logger,
|
||||
StringTemplate,
|
||||
)
|
||||
from openpype.pipeline import Anatomy
|
||||
from openpype.pipeline import version_start, Anatomy
|
||||
from openpype.pipeline.template_data import get_template_data
|
||||
|
||||
|
||||
|
|
@ -316,7 +316,13 @@ def get_last_workfile(
|
|||
)
|
||||
if filename is None:
|
||||
data = copy.deepcopy(fill_data)
|
||||
data["version"] = 1
|
||||
data["version"] = version_start.get_versioning_start(
|
||||
data["project"]["name"],
|
||||
data["app"],
|
||||
task_name=data["task"]["name"],
|
||||
task_type=data["task"]["type"],
|
||||
family="workfile"
|
||||
)
|
||||
data.pop("comment", None)
|
||||
if not data.get("ext"):
|
||||
data["ext"] = extensions[0]
|
||||
|
|
|
|||
|
|
@ -1612,7 +1612,7 @@ class PlaceholderLoadMixin(object):
|
|||
|
||||
pass
|
||||
|
||||
def delete_placeholder(self, placeholder, failed):
|
||||
def delete_placeholder(self, placeholder):
|
||||
"""Called when all item population is done."""
|
||||
self.log.debug("Clean up of placeholder is not implemented.")
|
||||
|
||||
|
|
@ -1781,6 +1781,17 @@ class PlaceholderCreateMixin(object):
|
|||
|
||||
self.post_placeholder_process(placeholder, failed)
|
||||
|
||||
if failed:
|
||||
self.log.debug(
|
||||
"Placeholder cleanup skipped due to failed placeholder "
|
||||
"population."
|
||||
)
|
||||
return
|
||||
|
||||
if not placeholder.data.get("keep_placeholder", True):
|
||||
self.delete_placeholder(placeholder)
|
||||
|
||||
|
||||
def create_failed(self, placeholder, creator_data):
|
||||
if hasattr(placeholder, "create_failed"):
|
||||
placeholder.create_failed(creator_data)
|
||||
|
|
@ -1800,9 +1811,12 @@ class PlaceholderCreateMixin(object):
|
|||
representation.
|
||||
failed (bool): Loading of representation failed.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def delete_placeholder(self, placeholder):
|
||||
"""Called when all item population is done."""
|
||||
self.log.debug("Clean up of placeholder is not implemented.")
|
||||
|
||||
def _before_instance_create(self, placeholder):
|
||||
"""Can be overriden. Is called before instance is created."""
|
||||
|
||||
|
|
|
|||
125
openpype/plugins/actions/open_file_explorer.py
Normal file
125
openpype/plugins/actions/open_file_explorer.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
from string import Formatter
|
||||
from openpype.client import (
|
||||
get_project,
|
||||
get_asset_by_name,
|
||||
)
|
||||
from openpype.pipeline import (
|
||||
Anatomy,
|
||||
LauncherAction,
|
||||
)
|
||||
from openpype.pipeline.template_data import get_template_data
|
||||
|
||||
|
||||
class OpenTaskPath(LauncherAction):
|
||||
name = "open_task_path"
|
||||
label = "Explore here"
|
||||
icon = "folder-open"
|
||||
order = 500
|
||||
|
||||
def is_compatible(self, session):
|
||||
"""Return whether the action is compatible with the session"""
|
||||
return bool(session.get("AVALON_ASSET"))
|
||||
|
||||
def process(self, session, **kwargs):
|
||||
from qtpy import QtCore, QtWidgets
|
||||
|
||||
project_name = session["AVALON_PROJECT"]
|
||||
asset_name = session["AVALON_ASSET"]
|
||||
task_name = session.get("AVALON_TASK", None)
|
||||
|
||||
path = self._get_workdir(project_name, asset_name, task_name)
|
||||
if not path:
|
||||
return
|
||||
|
||||
app = QtWidgets.QApplication.instance()
|
||||
ctrl_pressed = QtCore.Qt.ControlModifier & app.keyboardModifiers()
|
||||
if ctrl_pressed:
|
||||
# Copy path to clipboard
|
||||
self.copy_path_to_clipboard(path)
|
||||
else:
|
||||
self.open_in_explorer(path)
|
||||
|
||||
def _find_first_filled_path(self, path):
|
||||
if not path:
|
||||
return ""
|
||||
|
||||
fields = set()
|
||||
for item in Formatter().parse(path):
|
||||
_, field_name, format_spec, conversion = item
|
||||
if not field_name:
|
||||
continue
|
||||
conversion = "!{}".format(conversion) if conversion else ""
|
||||
format_spec = ":{}".format(format_spec) if format_spec else ""
|
||||
orig_key = "{{{}{}{}}}".format(
|
||||
field_name, conversion, format_spec)
|
||||
fields.add(orig_key)
|
||||
|
||||
for field in fields:
|
||||
path = path.split(field, 1)[0]
|
||||
return path
|
||||
|
||||
def _get_workdir(self, project_name, asset_name, task_name):
|
||||
project = get_project(project_name)
|
||||
asset = get_asset_by_name(project_name, asset_name)
|
||||
|
||||
data = get_template_data(project, asset, task_name)
|
||||
|
||||
anatomy = Anatomy(project_name)
|
||||
workdir = anatomy.templates_obj["work"]["folder"].format(data)
|
||||
|
||||
# Remove any potential un-formatted parts of the path
|
||||
valid_workdir = self._find_first_filled_path(workdir)
|
||||
|
||||
# Path is not filled at all
|
||||
if not valid_workdir:
|
||||
raise AssertionError("Failed to calculate workdir.")
|
||||
|
||||
# Normalize
|
||||
valid_workdir = os.path.normpath(valid_workdir)
|
||||
if os.path.exists(valid_workdir):
|
||||
return valid_workdir
|
||||
|
||||
# If task was selected, try to find asset path only to asset
|
||||
if not task_name:
|
||||
raise AssertionError("Folder does not exist.")
|
||||
|
||||
data.pop("task", None)
|
||||
workdir = anatomy.templates_obj["work"]["folder"].format(data)
|
||||
valid_workdir = self._find_first_filled_path(workdir)
|
||||
if valid_workdir:
|
||||
# Normalize
|
||||
valid_workdir = os.path.normpath(valid_workdir)
|
||||
if os.path.exists(valid_workdir):
|
||||
return valid_workdir
|
||||
raise AssertionError("Folder does not exist.")
|
||||
|
||||
@staticmethod
|
||||
def open_in_explorer(path):
|
||||
platform_name = platform.system().lower()
|
||||
if platform_name == "windows":
|
||||
args = ["start", path]
|
||||
elif platform_name == "darwin":
|
||||
args = ["open", "-na", path]
|
||||
elif platform_name == "linux":
|
||||
args = ["xdg-open", path]
|
||||
else:
|
||||
raise RuntimeError(f"Unknown platform {platform.system()}")
|
||||
# Make sure path is converted correctly for 'os.system'
|
||||
os.system(subprocess.list2cmdline(args))
|
||||
|
||||
@staticmethod
|
||||
def copy_path_to_clipboard(path):
|
||||
from qtpy import QtWidgets
|
||||
|
||||
path = path.replace("\\", "/")
|
||||
print(f"Copied to clipboard: {path}")
|
||||
app = QtWidgets.QApplication.instance()
|
||||
assert app, "Must have running QApplication instance"
|
||||
|
||||
# Set to Clipboard
|
||||
clipboard = QtWidgets.QApplication.clipboard()
|
||||
clipboard.setText(os.path.normpath(path))
|
||||
|
|
@ -32,6 +32,7 @@ from openpype.client import (
|
|||
get_subsets,
|
||||
get_last_versions
|
||||
)
|
||||
from openpype.pipeline.version_start import get_versioning_start
|
||||
|
||||
|
||||
class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
||||
|
|
@ -187,25 +188,13 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
project_task_types = project_doc["config"]["tasks"]
|
||||
|
||||
for instance in context:
|
||||
if self.follow_workfile_version:
|
||||
version_number = context.data('version')
|
||||
else:
|
||||
version_number = instance.data.get("version")
|
||||
# If version is not specified for instance or context
|
||||
if version_number is None:
|
||||
# TODO we should be able to change default version by studio
|
||||
# preferences (like start with version number `0`)
|
||||
version_number = 1
|
||||
# use latest version (+1) if already any exist
|
||||
latest_version = instance.data["latestVersion"]
|
||||
if latest_version is not None:
|
||||
version_number += int(latest_version)
|
||||
|
||||
anatomy_updates = {
|
||||
"asset": instance.data["asset"],
|
||||
"folder": {
|
||||
"name": instance.data["asset"],
|
||||
},
|
||||
"family": instance.data["family"],
|
||||
"subset": instance.data["subset"],
|
||||
"version": version_number
|
||||
}
|
||||
|
||||
# Hierarchy
|
||||
|
|
@ -225,6 +214,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
anatomy_updates["parent"] = parent_name
|
||||
|
||||
# Task
|
||||
task_type = None
|
||||
task_name = instance.data.get("task")
|
||||
if task_name:
|
||||
asset_tasks = asset_doc["data"]["tasks"]
|
||||
|
|
@ -240,6 +230,30 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
"short": task_code
|
||||
}
|
||||
|
||||
# Define version
|
||||
if self.follow_workfile_version:
|
||||
version_number = context.data('version')
|
||||
else:
|
||||
version_number = instance.data.get("version")
|
||||
|
||||
# use latest version (+1) if already any exist
|
||||
if version_number is None:
|
||||
latest_version = instance.data["latestVersion"]
|
||||
if latest_version is not None:
|
||||
version_number = int(latest_version) + 1
|
||||
|
||||
# If version is not specified for instance or context
|
||||
if version_number is None:
|
||||
version_number = get_versioning_start(
|
||||
context.data["projectName"],
|
||||
instance.context.data["hostName"],
|
||||
task_name=task_name,
|
||||
task_type=task_type,
|
||||
family=instance.data["family"],
|
||||
subset=instance.data["subset"]
|
||||
)
|
||||
anatomy_updates["version"] = version_number
|
||||
|
||||
# Additional data
|
||||
resolution_width = instance.data.get("resolutionWidth")
|
||||
if resolution_width:
|
||||
|
|
|
|||
|
|
@ -53,8 +53,8 @@ class ExtractBurnin(publish.Extractor):
|
|||
"flame",
|
||||
"houdini",
|
||||
"max",
|
||||
"blender"
|
||||
# "resolve"
|
||||
"blender",
|
||||
"unreal"
|
||||
]
|
||||
|
||||
optional = True
|
||||
|
|
|
|||
|
|
@ -142,6 +142,12 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
|
|||
))
|
||||
return
|
||||
|
||||
if AYON_SERVER_ENABLED and src_version_entity["name"] == 0:
|
||||
self.log.debug(
|
||||
"Version 0 cannot have hero version. Skipping."
|
||||
)
|
||||
return
|
||||
|
||||
all_copied_files = []
|
||||
transfers = instance.data.get("transfers", list())
|
||||
for _src, dst in transfers:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from openpype.pipeline import (
|
|||
)
|
||||
|
||||
from openpype.pipeline.context_tools import get_workdir_from_session
|
||||
from openpype.pipeline.version_start import get_versioning_start
|
||||
|
||||
log = logging.getLogger("Update Slap Comp")
|
||||
|
||||
|
|
@ -26,9 +27,6 @@ log = logging.getLogger("Update Slap Comp")
|
|||
def _format_version_folder(folder):
|
||||
"""Format a version folder based on the filepath
|
||||
|
||||
Assumption here is made that, if the path does not exists the folder
|
||||
will be "v001"
|
||||
|
||||
Args:
|
||||
folder: file path to a folder
|
||||
|
||||
|
|
@ -36,9 +34,13 @@ def _format_version_folder(folder):
|
|||
str: new version folder name
|
||||
"""
|
||||
|
||||
new_version = 1
|
||||
new_version = get_versioning_start(
|
||||
get_current_project_name(),
|
||||
"fusion",
|
||||
family="workfile"
|
||||
)
|
||||
if os.path.isdir(folder):
|
||||
re_version = re.compile("v\d+$")
|
||||
re_version = re.compile(r"v\d+$")
|
||||
versions = [i for i in os.listdir(folder) if os.path.isdir(i)
|
||||
and re_version.match(i)]
|
||||
if versions:
|
||||
|
|
|
|||
|
|
@ -301,6 +301,10 @@ def convert_system_settings(ayon_settings, default_settings, addon_versions):
|
|||
if "core" in ayon_settings:
|
||||
_convert_general(ayon_settings, output, default_settings)
|
||||
|
||||
for key, value in ayon_settings.items():
|
||||
if key not in output:
|
||||
output[key] = value
|
||||
|
||||
for key, value in default_settings.items():
|
||||
if key not in output:
|
||||
output[key] = value
|
||||
|
|
@ -602,6 +606,13 @@ def _convert_maya_project_settings(ayon_settings, output):
|
|||
.replace("{product[name]}", "{subset}")
|
||||
)
|
||||
|
||||
if ayon_maya_load.get("import_loader"):
|
||||
import_loader = ayon_maya_load["import_loader"]
|
||||
import_loader["namespace"] = (
|
||||
import_loader["namespace"]
|
||||
.replace("{product[name]}", "{subset}")
|
||||
)
|
||||
|
||||
output["maya"] = ayon_maya
|
||||
|
||||
|
||||
|
|
@ -1265,6 +1276,10 @@ def convert_project_settings(ayon_settings, default_settings):
|
|||
|
||||
_convert_global_project_settings(ayon_settings, output, default_settings)
|
||||
|
||||
for key, value in ayon_settings.items():
|
||||
if key not in output:
|
||||
output[key] = value
|
||||
|
||||
for key, value in default_settings.items():
|
||||
if key not in output:
|
||||
output[key] = value
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
},
|
||||
"create": {
|
||||
"RenderCreator": {
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
],
|
||||
"mark_for_review": true
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
{
|
||||
"version_start_category": {
|
||||
"profiles": []
|
||||
},
|
||||
"imageio": {
|
||||
"activate_global_color_management": false,
|
||||
"ocio_config": {
|
||||
|
|
|
|||
|
|
@ -14,48 +14,70 @@
|
|||
"create": {
|
||||
"CreateArnoldAss": {
|
||||
"enabled": true,
|
||||
"default_variants": [],
|
||||
"default_variants": [
|
||||
"Main"
|
||||
],
|
||||
"ext": ".ass"
|
||||
},
|
||||
"CreateAlembicCamera": {
|
||||
"enabled": true,
|
||||
"defaults": []
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateCompositeSequence": {
|
||||
"enabled": true,
|
||||
"defaults": []
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreatePointCache": {
|
||||
"enabled": true,
|
||||
"defaults": []
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateRedshiftROP": {
|
||||
"enabled": true,
|
||||
"defaults": []
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateRemotePublish": {
|
||||
"enabled": true,
|
||||
"defaults": []
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateVDBCache": {
|
||||
"enabled": true,
|
||||
"defaults": []
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateUSD": {
|
||||
"enabled": false,
|
||||
"defaults": []
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateUSDModel": {
|
||||
"enabled": false,
|
||||
"defaults": []
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"USDCreateShadingWorkspace": {
|
||||
"enabled": false,
|
||||
"defaults": []
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateUSDRender": {
|
||||
"enabled": false,
|
||||
"defaults": []
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
}
|
||||
},
|
||||
"publish": {
|
||||
|
|
|
|||
|
|
@ -527,7 +527,7 @@
|
|||
},
|
||||
"CreateRender": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
|
|
@ -547,7 +547,9 @@
|
|||
},
|
||||
"CreateUnrealSkeletalMesh": {
|
||||
"enabled": true,
|
||||
"default_variants": [],
|
||||
"default_variants": [
|
||||
"Main"
|
||||
],
|
||||
"joint_hints": "jnt_org"
|
||||
},
|
||||
"CreateMultiverseLook": {
|
||||
|
|
@ -627,55 +629,55 @@
|
|||
},
|
||||
"CreateMultiverseUsd": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateMultiverseUsdComp": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateMultiverseUsdOver": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateAssembly": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateCamera": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateLayout": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateMayaScene": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateRenderSetup": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateRig": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main",
|
||||
"Sim",
|
||||
"Cloth"
|
||||
|
|
@ -683,20 +685,20 @@
|
|||
},
|
||||
"CreateSetDress": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main",
|
||||
"Anim"
|
||||
]
|
||||
},
|
||||
"CreateVRayScene": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateYetiRig": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
}
|
||||
|
|
@ -1463,6 +1465,10 @@
|
|||
"namespace": "{asset_name}_{subset}_##_",
|
||||
"group_name": "_GRP",
|
||||
"display_handle": true
|
||||
},
|
||||
"import_loader": {
|
||||
"namespace": "{asset_name}_{subset}_##_",
|
||||
"group_name": "_GRP"
|
||||
}
|
||||
},
|
||||
"workfile_build": {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
"children": [
|
||||
{
|
||||
"type": "list",
|
||||
"key": "defaults",
|
||||
"key": "default_variants",
|
||||
"label": "Default Variants",
|
||||
"object_type": "text",
|
||||
"docstring": "Fill default variant(s) (like 'Main' or 'Default') used in subset name creation."
|
||||
|
|
|
|||
|
|
@ -5,6 +5,61 @@
|
|||
"label": "Global",
|
||||
"is_file": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "version_start_category",
|
||||
"label": "Version Start",
|
||||
"collapsible": true,
|
||||
"collapsible_key": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "list",
|
||||
"collapsible": true,
|
||||
"key": "profiles",
|
||||
"label": "Profiles",
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"key": "host_names",
|
||||
"label": "Host names",
|
||||
"type": "hosts-enum",
|
||||
"multiselection": true
|
||||
},
|
||||
{
|
||||
"key": "task_types",
|
||||
"label": "Task types",
|
||||
"type": "task-types-enum"
|
||||
},
|
||||
{
|
||||
"key": "task_names",
|
||||
"label": "Task names",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"key": "families",
|
||||
"label": "Families",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"key": "subsets",
|
||||
"label": "Subset names",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"key": "version_start",
|
||||
"label": "Version Start",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "imageio",
|
||||
"type": "dict",
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
{
|
||||
"type": "list",
|
||||
"key": "default_variants",
|
||||
"label": "Default Subsets",
|
||||
"label": "Default Variants",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
|
|
@ -39,51 +39,51 @@
|
|||
]
|
||||
|
||||
},
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_create_plugin",
|
||||
"template_data": [
|
||||
{
|
||||
"key": "CreateAlembicCamera",
|
||||
"label": "Create Alembic Camera"
|
||||
},
|
||||
{
|
||||
"key": "CreateCompositeSequence",
|
||||
"label": "Create Composite (Image Sequence)"
|
||||
},
|
||||
{
|
||||
"key": "CreatePointCache",
|
||||
"label": "Create Point Cache"
|
||||
},
|
||||
{
|
||||
"key": "CreateRedshiftROP",
|
||||
"label": "Create Redshift ROP"
|
||||
},
|
||||
{
|
||||
"key": "CreateRemotePublish",
|
||||
"label": "Create Remote Publish"
|
||||
},
|
||||
{
|
||||
"key": "CreateVDBCache",
|
||||
"label": "Create VDB Cache"
|
||||
},
|
||||
{
|
||||
"key": "CreateUSD",
|
||||
"label": "Create USD"
|
||||
},
|
||||
{
|
||||
"key": "CreateUSDModel",
|
||||
"label": "Create USD Model"
|
||||
},
|
||||
{
|
||||
"key": "USDCreateShadingWorkspace",
|
||||
"label": "Create USD Shading Workspace"
|
||||
},
|
||||
{
|
||||
"key": "CreateUSDRender",
|
||||
"label": "Create USD Render"
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_create_plugin",
|
||||
"template_data": [
|
||||
{
|
||||
"key": "CreateAlembicCamera",
|
||||
"label": "Create Alembic Camera"
|
||||
},
|
||||
{
|
||||
"key": "CreateCompositeSequence",
|
||||
"label": "Create Composite (Image Sequence)"
|
||||
},
|
||||
{
|
||||
"key": "CreatePointCache",
|
||||
"label": "Create Point Cache"
|
||||
},
|
||||
{
|
||||
"key": "CreateRedshiftROP",
|
||||
"label": "Create Redshift ROP"
|
||||
},
|
||||
{
|
||||
"key": "CreateRemotePublish",
|
||||
"label": "Create Remote Publish"
|
||||
},
|
||||
{
|
||||
"key": "CreateVDBCache",
|
||||
"label": "Create VDB Cache"
|
||||
},
|
||||
{
|
||||
"key": "CreateUSD",
|
||||
"label": "Create USD"
|
||||
},
|
||||
{
|
||||
"key": "CreateUSDModel",
|
||||
"label": "Create USD Model"
|
||||
},
|
||||
{
|
||||
"key": "USDCreateShadingWorkspace",
|
||||
"label": "Create USD Shading Workspace"
|
||||
},
|
||||
{
|
||||
"key": "CreateUSDRender",
|
||||
"label": "Create USD Render"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,14 +29,20 @@
|
|||
{
|
||||
"type": "list",
|
||||
"key": "default_variants",
|
||||
"label": "Default Subsets",
|
||||
"label": "Default Variants",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_maya_create_render"
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_create_plugin",
|
||||
"template_data": [
|
||||
{
|
||||
"key": "CreateRender",
|
||||
"label": "Create Render"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
|
|
@ -53,7 +59,7 @@
|
|||
{
|
||||
"type": "list",
|
||||
"key": "default_variants",
|
||||
"label": "Default Subsets",
|
||||
"label": "Default Variants",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
|
|
@ -85,7 +91,7 @@
|
|||
{
|
||||
"type": "list",
|
||||
"key": "default_variants",
|
||||
"label": "Default Subsets",
|
||||
"label": "Default Variants",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
|
|
@ -148,7 +154,7 @@
|
|||
{
|
||||
"type": "list",
|
||||
"key": "default_variants",
|
||||
"label": "Default Subsets",
|
||||
"label": "Default Variants",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
|
|
@ -178,7 +184,7 @@
|
|||
{
|
||||
"type": "list",
|
||||
"key": "default_variants",
|
||||
"label": "Default Subsets",
|
||||
"label": "Default Variants",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
|
|
@ -213,7 +219,7 @@
|
|||
{
|
||||
"type": "list",
|
||||
"key": "default_variants",
|
||||
"label": "Default Subsets",
|
||||
"label": "Default Variants",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
|
|
@ -243,7 +249,7 @@
|
|||
{
|
||||
"type": "list",
|
||||
"key": "default_variants",
|
||||
"label": "Default Subsets",
|
||||
"label": "Default Variants",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
|
|
@ -263,7 +269,7 @@
|
|||
{
|
||||
"type": "list",
|
||||
"key": "default_variants",
|
||||
"label": "Default Subsets",
|
||||
"label": "Default Variants",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
|
|
@ -288,7 +294,7 @@
|
|||
{
|
||||
"type": "list",
|
||||
"key": "default_variants",
|
||||
"label": "Default Subsets",
|
||||
"label": "Default Variants",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
|
|
@ -390,7 +396,7 @@
|
|||
{
|
||||
"type": "list",
|
||||
"key": "default_variants",
|
||||
"label": "Default Subsets",
|
||||
"label": "Default Variants",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "CreateRender",
|
||||
"label": "Create Render",
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "defaults",
|
||||
"label": "Default Subsets",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -121,6 +121,28 @@
|
|||
"label": "Display Handle On Load References"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "import_loader",
|
||||
"label": "Import Loader",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Namespace",
|
||||
"key": "namespace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Group name",
|
||||
"key": "group_name"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Here's a link to the doc where you can find explanations about customing the naming of referenced assets: https://openpype.io/docs/admin_hosts_maya#load-plugins"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@
|
|||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "defaults",
|
||||
"label": "Default Subsets",
|
||||
"key": "default_variants",
|
||||
"label": "Default Variants",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from openpype import AYON_SERVER_ENABLED
|
|||
from openpype.pipeline.create import (
|
||||
SUBSET_NAME_ALLOWED_SYMBOLS,
|
||||
PRE_CREATE_THUMBNAIL_KEY,
|
||||
DEFAULT_VARIANT_VALUE,
|
||||
TaskNotSetError,
|
||||
)
|
||||
|
||||
|
|
@ -626,7 +627,7 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
|
||||
default_variants = creator_item.default_variants
|
||||
if not default_variants:
|
||||
default_variants = ["Main"]
|
||||
default_variants = [DEFAULT_VARIANT_VALUE]
|
||||
|
||||
default_variant = creator_item.default_variant
|
||||
if not default_variant:
|
||||
|
|
@ -642,7 +643,7 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
elif variant:
|
||||
self.variant_hints_menu.addAction(variant)
|
||||
|
||||
variant_text = default_variant or "Main"
|
||||
variant_text = default_variant or DEFAULT_VARIANT_VALUE
|
||||
# Make sure subset name is updated to new plugin
|
||||
if variant_text == self.variant_input.text():
|
||||
self._on_variant_change()
|
||||
|
|
|
|||
BIN
openpype/tools/publisher/widgets/images/browse.png
Normal file
BIN
openpype/tools/publisher/widgets/images/browse.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
openpype/tools/publisher/widgets/images/options.png
Normal file
BIN
openpype/tools/publisher/widgets/images/options.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
BIN
openpype/tools/publisher/widgets/images/paste.png
Normal file
BIN
openpype/tools/publisher/widgets/images/paste.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
BIN
openpype/tools/publisher/widgets/images/take_screenshot.png
Normal file
BIN
openpype/tools/publisher/widgets/images/take_screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
314
openpype/tools/publisher/widgets/screenshot_widget.py
Normal file
314
openpype/tools/publisher/widgets/screenshot_widget.py
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
import os
|
||||
import tempfile
|
||||
|
||||
from qtpy import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class ScreenMarquee(QtWidgets.QDialog):
|
||||
"""Dialog to interactively define screen area.
|
||||
|
||||
This allows to select a screen area through a marquee selection.
|
||||
|
||||
You can use any of its classmethods for easily saving an image,
|
||||
capturing to QClipboard or returning a QPixmap, respectively
|
||||
`capture_to_file`, `capture_to_clipboard` and `capture_to_pixmap`.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ScreenMarquee, self).__init__(parent=parent)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.FramelessWindowHint
|
||||
| QtCore.Qt.WindowStaysOnTopHint
|
||||
| QtCore.Qt.CustomizeWindowHint
|
||||
| QtCore.Qt.Tool)
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
self.setCursor(QtCore.Qt.CrossCursor)
|
||||
self.setMouseTracking(True)
|
||||
|
||||
fade_anim = QtCore.QVariantAnimation()
|
||||
fade_anim.setStartValue(0)
|
||||
fade_anim.setEndValue(50)
|
||||
fade_anim.setDuration(200)
|
||||
fade_anim.setEasingCurve(QtCore.QEasingCurve.OutCubic)
|
||||
fade_anim.start(QtCore.QAbstractAnimation.DeleteWhenStopped)
|
||||
|
||||
fade_anim.valueChanged.connect(self._on_fade_anim)
|
||||
|
||||
app = QtWidgets.QApplication.instance()
|
||||
if hasattr(app, "screenAdded"):
|
||||
app.screenAdded.connect(self._on_screen_added)
|
||||
app.screenRemoved.connect(self._fit_screen_geometry)
|
||||
elif hasattr(app, "desktop"):
|
||||
desktop = app.desktop()
|
||||
desktop.screenCountChanged.connect(self._fit_screen_geometry)
|
||||
|
||||
for screen in QtWidgets.QApplication.screens():
|
||||
screen.geometryChanged.connect(self._fit_screen_geometry)
|
||||
|
||||
self._opacity = fade_anim.currentValue()
|
||||
self._click_pos = None
|
||||
self._capture_rect = None
|
||||
|
||||
self._fade_anim = fade_anim
|
||||
|
||||
def get_captured_pixmap(self):
|
||||
if self._capture_rect is None:
|
||||
return QtGui.QPixmap()
|
||||
|
||||
return self.get_desktop_pixmap(self._capture_rect)
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""Paint event"""
|
||||
|
||||
# Convert click and current mouse positions to local space.
|
||||
mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos())
|
||||
click_pos = None
|
||||
if self._click_pos is not None:
|
||||
click_pos = self.mapFromGlobal(self._click_pos)
|
||||
|
||||
painter = QtGui.QPainter(self)
|
||||
|
||||
# Draw background. Aside from aesthetics, this makes the full
|
||||
# tool region accept mouse events.
|
||||
painter.setBrush(QtGui.QColor(0, 0, 0, self._opacity))
|
||||
painter.setPen(QtCore.Qt.NoPen)
|
||||
painter.drawRect(event.rect())
|
||||
|
||||
# Clear the capture area
|
||||
if click_pos is not None:
|
||||
capture_rect = QtCore.QRect(click_pos, mouse_pos)
|
||||
painter.setCompositionMode(
|
||||
QtGui.QPainter.CompositionMode_Clear)
|
||||
painter.drawRect(capture_rect)
|
||||
painter.setCompositionMode(
|
||||
QtGui.QPainter.CompositionMode_SourceOver)
|
||||
|
||||
pen_color = QtGui.QColor(255, 255, 255, 64)
|
||||
pen = QtGui.QPen(pen_color, 1, QtCore.Qt.DotLine)
|
||||
painter.setPen(pen)
|
||||
|
||||
# Draw cropping markers at click position
|
||||
rect = event.rect()
|
||||
if click_pos is not None:
|
||||
painter.drawLine(
|
||||
rect.left(), click_pos.y(),
|
||||
rect.right(), click_pos.y()
|
||||
)
|
||||
painter.drawLine(
|
||||
click_pos.x(), rect.top(),
|
||||
click_pos.x(), rect.bottom()
|
||||
)
|
||||
|
||||
# Draw cropping markers at current mouse position
|
||||
painter.drawLine(
|
||||
rect.left(), mouse_pos.y(),
|
||||
rect.right(), mouse_pos.y()
|
||||
)
|
||||
painter.drawLine(
|
||||
mouse_pos.x(), rect.top(),
|
||||
mouse_pos.x(), rect.bottom()
|
||||
)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""Mouse click event"""
|
||||
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
# Begin click drag operation
|
||||
self._click_pos = event.globalPos()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""Mouse release event"""
|
||||
if (
|
||||
self._click_pos is not None
|
||||
and event.button() == QtCore.Qt.LeftButton
|
||||
):
|
||||
# End click drag operation and commit the current capture rect
|
||||
self._capture_rect = QtCore.QRect(
|
||||
self._click_pos, event.globalPos()
|
||||
).normalized()
|
||||
self._click_pos = None
|
||||
self.close()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""Mouse move event"""
|
||||
self.repaint()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Mouse press event"""
|
||||
if event.key() == QtCore.Qt.Key_Escape:
|
||||
self._click_pos = None
|
||||
self._capture_rect = None
|
||||
self.close()
|
||||
return
|
||||
return super(ScreenMarquee, self).mousePressEvent(event)
|
||||
|
||||
def showEvent(self, event):
|
||||
self._fit_screen_geometry()
|
||||
self._fade_anim.start()
|
||||
|
||||
def _fit_screen_geometry(self):
|
||||
# Compute the union of all screen geometries, and resize to fit.
|
||||
workspace_rect = QtCore.QRect()
|
||||
for screen in QtWidgets.QApplication.screens():
|
||||
workspace_rect = workspace_rect.united(screen.geometry())
|
||||
self.setGeometry(workspace_rect)
|
||||
|
||||
def _on_fade_anim(self):
|
||||
"""Animation callback for opacity."""
|
||||
|
||||
self._opacity = self._fade_anim.currentValue()
|
||||
self.repaint()
|
||||
|
||||
def _on_screen_added(self):
|
||||
for screen in QtGui.QGuiApplication.screens():
|
||||
screen.geometryChanged.connect(self._fit_screen_geometry)
|
||||
|
||||
@classmethod
|
||||
def get_desktop_pixmap(cls, rect):
|
||||
"""Performs a screen capture on the specified rectangle.
|
||||
|
||||
Args:
|
||||
rect (QtCore.QRect): The rectangle to capture.
|
||||
|
||||
Returns:
|
||||
QtGui.QPixmap: Captured pixmap image
|
||||
"""
|
||||
|
||||
if rect.width() < 1 or rect.height() < 1:
|
||||
return QtGui.QPixmap()
|
||||
|
||||
screen_pixes = []
|
||||
for screen in QtWidgets.QApplication.screens():
|
||||
screen_geo = screen.geometry()
|
||||
if not screen_geo.intersects(rect):
|
||||
continue
|
||||
|
||||
screen_pix_rect = screen_geo.intersected(rect)
|
||||
screen_pix = screen.grabWindow(
|
||||
0,
|
||||
screen_pix_rect.x() - screen_geo.x(),
|
||||
screen_pix_rect.y() - screen_geo.y(),
|
||||
screen_pix_rect.width(), screen_pix_rect.height()
|
||||
)
|
||||
paste_point = QtCore.QPoint(
|
||||
screen_pix_rect.x() - rect.x(),
|
||||
screen_pix_rect.y() - rect.y()
|
||||
)
|
||||
screen_pixes.append((screen_pix, paste_point))
|
||||
|
||||
output_pix = QtGui.QPixmap(rect.width(), rect.height())
|
||||
output_pix.fill(QtCore.Qt.transparent)
|
||||
pix_painter = QtGui.QPainter()
|
||||
pix_painter.begin(output_pix)
|
||||
for item in screen_pixes:
|
||||
(screen_pix, offset) = item
|
||||
pix_painter.drawPixmap(offset, screen_pix)
|
||||
|
||||
pix_painter.end()
|
||||
|
||||
return output_pix
|
||||
|
||||
@classmethod
|
||||
def capture_to_pixmap(cls):
|
||||
"""Take screenshot with marquee into pixmap.
|
||||
|
||||
Note:
|
||||
The pixmap can be invalid (use 'isNull' to check).
|
||||
|
||||
Returns:
|
||||
QtGui.QPixmap: Captured pixmap image.
|
||||
"""
|
||||
|
||||
tool = cls()
|
||||
tool.exec_()
|
||||
return tool.get_captured_pixmap()
|
||||
|
||||
@classmethod
|
||||
def capture_to_file(cls, filepath=None):
|
||||
"""Take screenshot with marquee into file.
|
||||
|
||||
Args:
|
||||
filepath (Optional[str]): Path where screenshot will be saved.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Path to the saved screenshot, or None if user
|
||||
cancelled the operation.
|
||||
"""
|
||||
|
||||
pixmap = cls.capture_to_pixmap()
|
||||
if pixmap.isNull():
|
||||
return None
|
||||
|
||||
if filepath is None:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
prefix="screenshot_", suffix=".png", delete=False
|
||||
) as tmpfile:
|
||||
filepath = tmpfile.name
|
||||
|
||||
else:
|
||||
output_dir = os.path.dirname(filepath)
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
pixmap.save(filepath)
|
||||
return filepath
|
||||
|
||||
@classmethod
|
||||
def capture_to_clipboard(cls):
|
||||
"""Take screenshot with marquee into clipboard.
|
||||
|
||||
Notes:
|
||||
Screenshot is not in clipboard if user cancelled the operation.
|
||||
|
||||
Returns:
|
||||
bool: Screenshot was added to clipboard.
|
||||
"""
|
||||
|
||||
clipboard = QtWidgets.QApplication.clipboard()
|
||||
pixmap = cls.capture_to_pixmap()
|
||||
if pixmap.isNull():
|
||||
return False
|
||||
image = pixmap.toImage()
|
||||
clipboard.setImage(image, QtGui.QClipboard.Clipboard)
|
||||
return True
|
||||
|
||||
|
||||
def capture_to_pixmap():
|
||||
"""Take screenshot with marquee into pixmap.
|
||||
|
||||
Note:
|
||||
The pixmap can be invalid (use 'isNull' to check).
|
||||
|
||||
Returns:
|
||||
QtGui.QPixmap: Captured pixmap image.
|
||||
"""
|
||||
|
||||
return ScreenMarquee.capture_to_pixmap()
|
||||
|
||||
|
||||
def capture_to_file(filepath=None):
|
||||
"""Take screenshot with marquee into file.
|
||||
|
||||
Args:
|
||||
filepath (Optional[str]): Path where screenshot will be saved.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Path to the saved screenshot, or None if user
|
||||
cancelled the operation.
|
||||
"""
|
||||
|
||||
return ScreenMarquee.capture_to_file(filepath)
|
||||
|
||||
|
||||
def capture_to_clipboard():
|
||||
"""Take screenshot with marquee into clipboard.
|
||||
|
||||
Notes:
|
||||
Screenshot is not in clipboard if user cancelled the operation.
|
||||
|
||||
Returns:
|
||||
bool: Screenshot was added to clipboard.
|
||||
"""
|
||||
|
||||
return ScreenMarquee.capture_to_clipboard()
|
||||
|
|
@ -22,6 +22,7 @@ from openpype.tools.utils import (
|
|||
from openpype.tools.publisher.control import CardMessageTypes
|
||||
|
||||
from .icons import get_image
|
||||
from .screenshot_widget import capture_to_file
|
||||
|
||||
|
||||
class ThumbnailPainterWidget(QtWidgets.QWidget):
|
||||
|
|
@ -306,20 +307,43 @@ class ThumbnailWidget(QtWidgets.QWidget):
|
|||
|
||||
thumbnail_painter = ThumbnailPainterWidget(self)
|
||||
|
||||
icon_color = get_objected_colors("bg-view-selection").get_qcolor()
|
||||
icon_color.setAlpha(255)
|
||||
|
||||
buttons_widget = QtWidgets.QWidget(self)
|
||||
buttons_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
icon_color = get_objected_colors("bg-view-selection").get_qcolor()
|
||||
icon_color.setAlpha(255)
|
||||
clear_image = get_image("clear_thumbnail")
|
||||
clear_pix = paint_image_with_color(clear_image, icon_color)
|
||||
|
||||
clear_button = PixmapButton(clear_pix, buttons_widget)
|
||||
clear_button.setObjectName("ThumbnailPixmapHoverButton")
|
||||
clear_button.setToolTip("Clear thumbnail")
|
||||
|
||||
take_screenshot_image = get_image("take_screenshot")
|
||||
take_screenshot_pix = paint_image_with_color(
|
||||
take_screenshot_image, icon_color)
|
||||
take_screenshot_btn = PixmapButton(
|
||||
take_screenshot_pix, buttons_widget)
|
||||
take_screenshot_btn.setObjectName("ThumbnailPixmapHoverButton")
|
||||
take_screenshot_btn.setToolTip("Take screenshot")
|
||||
|
||||
paste_image = get_image("paste")
|
||||
paste_pix = paint_image_with_color(paste_image, icon_color)
|
||||
paste_btn = PixmapButton(paste_pix, buttons_widget)
|
||||
paste_btn.setObjectName("ThumbnailPixmapHoverButton")
|
||||
paste_btn.setToolTip("Paste from clipboard")
|
||||
|
||||
browse_image = get_image("browse")
|
||||
browse_pix = paint_image_with_color(browse_image, icon_color)
|
||||
browse_btn = PixmapButton(browse_pix, buttons_widget)
|
||||
browse_btn.setObjectName("ThumbnailPixmapHoverButton")
|
||||
browse_btn.setToolTip("Browse...")
|
||||
|
||||
buttons_layout = QtWidgets.QHBoxLayout(buttons_widget)
|
||||
buttons_layout.setContentsMargins(3, 3, 3, 3)
|
||||
buttons_layout.addStretch(1)
|
||||
buttons_layout.setContentsMargins(0, 0, 0, 0)
|
||||
buttons_layout.addWidget(take_screenshot_btn, 0)
|
||||
buttons_layout.addWidget(paste_btn, 0)
|
||||
buttons_layout.addWidget(browse_btn, 0)
|
||||
buttons_layout.addWidget(clear_button, 0)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
|
|
@ -327,6 +351,9 @@ class ThumbnailWidget(QtWidgets.QWidget):
|
|||
layout.addWidget(thumbnail_painter)
|
||||
|
||||
clear_button.clicked.connect(self._on_clear_clicked)
|
||||
take_screenshot_btn.clicked.connect(self._on_take_screenshot)
|
||||
paste_btn.clicked.connect(self._on_paste_from_clipboard)
|
||||
browse_btn.clicked.connect(self._on_browse_clicked)
|
||||
|
||||
self._controller = controller
|
||||
self._output_dir = controller.get_thumbnail_temp_dir_path()
|
||||
|
|
@ -338,9 +365,16 @@ class ThumbnailWidget(QtWidgets.QWidget):
|
|||
self._adapted_to_size = True
|
||||
self._last_width = None
|
||||
self._last_height = None
|
||||
self._hide_on_finish = False
|
||||
|
||||
self._buttons_widget = buttons_widget
|
||||
self._thumbnail_painter = thumbnail_painter
|
||||
self._clear_button = clear_button
|
||||
self._take_screenshot_btn = take_screenshot_btn
|
||||
self._paste_btn = paste_btn
|
||||
self._browse_btn = browse_btn
|
||||
|
||||
clear_button.setEnabled(False)
|
||||
|
||||
@property
|
||||
def width_ratio(self):
|
||||
|
|
@ -430,13 +464,75 @@ class ThumbnailWidget(QtWidgets.QWidget):
|
|||
|
||||
self._thumbnail_painter.clear_cache()
|
||||
|
||||
def _set_current_thumbails(self, thumbnail_paths):
|
||||
self._thumbnail_painter.set_current_thumbnails(thumbnail_paths)
|
||||
self._update_buttons_position()
|
||||
|
||||
def set_current_thumbnails(self, thumbnail_paths=None):
|
||||
self._thumbnail_painter.set_current_thumbnails(thumbnail_paths)
|
||||
self._update_buttons_position()
|
||||
self._clear_button.setEnabled(self._thumbnail_painter.has_pixes)
|
||||
|
||||
def _on_clear_clicked(self):
|
||||
self.set_current_thumbnails()
|
||||
self.thumbnail_cleared.emit()
|
||||
self._clear_button.setEnabled(False)
|
||||
|
||||
def _on_take_screenshot(self):
|
||||
window = self.window()
|
||||
state = window.windowState()
|
||||
window.setWindowState(QtCore.Qt.WindowMinimized)
|
||||
output_path = os.path.join(
|
||||
self._output_dir, uuid.uuid4().hex + ".png")
|
||||
if capture_to_file(output_path):
|
||||
self.thumbnail_created.emit(output_path)
|
||||
# restore original window state
|
||||
window.setWindowState(state)
|
||||
|
||||
def _on_paste_from_clipboard(self):
|
||||
"""Set thumbnail from a pixmap image in the system clipboard"""
|
||||
|
||||
clipboard = QtWidgets.QApplication.clipboard()
|
||||
pixmap = clipboard.pixmap()
|
||||
if pixmap.isNull():
|
||||
return
|
||||
|
||||
# Save as temporary file
|
||||
output_path = os.path.join(
|
||||
self._output_dir, uuid.uuid4().hex + ".png")
|
||||
|
||||
output_dir = os.path.dirname(output_path)
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
if pixmap.save(output_path):
|
||||
self.thumbnail_created.emit(output_path)
|
||||
|
||||
def _on_browse_clicked(self):
|
||||
ext_filter = "Source (*{0})".format(
|
||||
" *".join(self._review_extensions)
|
||||
)
|
||||
filepath, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
self, "Choose thumbnail", os.path.expanduser("~"), ext_filter
|
||||
)
|
||||
if not filepath:
|
||||
return
|
||||
valid_path = False
|
||||
ext = os.path.splitext(filepath)[-1].lower()
|
||||
if ext in self._review_extensions:
|
||||
valid_path = True
|
||||
|
||||
output = None
|
||||
if valid_path:
|
||||
output = export_thumbnail(filepath, self._output_dir)
|
||||
|
||||
if output:
|
||||
self.thumbnail_created.emit(output)
|
||||
else:
|
||||
self._controller.emit_card_message(
|
||||
"Couldn't convert the source for thumbnail",
|
||||
CardMessageTypes.error
|
||||
)
|
||||
|
||||
def _adapt_to_size(self):
|
||||
if not self._adapted_to_size:
|
||||
|
|
@ -452,13 +548,25 @@ class ThumbnailWidget(QtWidgets.QWidget):
|
|||
self._thumbnail_painter.clear_cache()
|
||||
|
||||
def _update_buttons_position(self):
|
||||
self._buttons_widget.setVisible(self._thumbnail_painter.has_pixes)
|
||||
size = self.size()
|
||||
my_width = size.width()
|
||||
my_height = size.height()
|
||||
height = self._buttons_widget.sizeHint().height()
|
||||
buttons_sh = self._buttons_widget.sizeHint()
|
||||
buttons_height = buttons_sh.height()
|
||||
buttons_width = buttons_sh.width()
|
||||
pos_x = my_width - (buttons_width + 3)
|
||||
pos_y = my_height - (buttons_height + 3)
|
||||
if pos_x < 0:
|
||||
pos_x = 0
|
||||
buttons_width = my_width
|
||||
if pos_y < 0:
|
||||
pos_y = 0
|
||||
buttons_height = my_height
|
||||
self._buttons_widget.setGeometry(
|
||||
0, my_height - height,
|
||||
size.width(), height
|
||||
pos_x,
|
||||
pos_y,
|
||||
buttons_width,
|
||||
buttons_height
|
||||
)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ from openpype.lib import (
|
|||
from openpype.lib.file_transaction import FileTransaction
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.pipeline import Anatomy
|
||||
from openpype.pipeline.version_start import get_versioning_start
|
||||
from openpype.pipeline.template_data import get_template_data
|
||||
from openpype.pipeline.publish import get_publish_template_name
|
||||
from openpype.pipeline.create import get_subset_name
|
||||
|
|
@ -940,9 +941,17 @@ class ProjectPushItemProcess:
|
|||
last_version_doc = get_last_version_by_subset_id(
|
||||
project_name, subset_id
|
||||
)
|
||||
version = 1
|
||||
if last_version_doc:
|
||||
version += int(last_version_doc["name"])
|
||||
version = int(last_version_doc["name"]) + 1
|
||||
else:
|
||||
version = get_versioning_start(
|
||||
project_name,
|
||||
self.host_name,
|
||||
task_name=self.task_info["name"],
|
||||
task_type=self.task_info["type"],
|
||||
family=families[0],
|
||||
subset=subset_doc["name"]
|
||||
)
|
||||
|
||||
existing_version_doc = get_version_by_name(
|
||||
project_name, version, subset_id
|
||||
|
|
@ -966,14 +975,6 @@ class ProjectPushItemProcess:
|
|||
|
||||
return
|
||||
|
||||
if version is None:
|
||||
last_version_doc = get_last_version_by_subset_id(
|
||||
project_name, subset_id
|
||||
)
|
||||
version = 1
|
||||
if last_version_doc:
|
||||
version += int(last_version_doc["name"])
|
||||
|
||||
version_doc = new_version_doc(
|
||||
version, subset_id, version_data
|
||||
)
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class InventoryModel(TreeModel):
|
|||
self.remote_provider = remote_provider
|
||||
self._site_icons = {
|
||||
provider: QtGui.QIcon(icon_path)
|
||||
for provider, icon_path in self.get_site_icons().items()
|
||||
for provider, icon_path in sync_server.get_site_icons().items()
|
||||
}
|
||||
if "active_site" not in self.Columns:
|
||||
self.Columns.append("active_site")
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ class SitesWidget(QtWidgets.QWidget):
|
|||
continue
|
||||
|
||||
site_inputs = []
|
||||
site_config = site_configs[site_name]
|
||||
site_config = site_configs.get(site_name, {})
|
||||
for root_name, path_entity in site_config.get("root", {}).items():
|
||||
if not path_entity:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from openpype.client import (
|
|||
)
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.pipeline import LegacyCreator
|
||||
from openpype.pipeline.version_start import get_versioning_start
|
||||
from openpype.pipeline.create import (
|
||||
SUBSET_NAME_ALLOWED_SYMBOLS,
|
||||
TaskNotSetError,
|
||||
|
|
@ -299,7 +300,15 @@ class FamilyWidget(QtWidgets.QWidget):
|
|||
project_name = self.dbcon.active_project()
|
||||
asset_name = self.asset_name
|
||||
subset_name = self.input_result.text()
|
||||
version = 1
|
||||
plugin = self.list_families.currentItem().data(PluginRole)
|
||||
family = plugin.family.rsplit(".", 1)[-1]
|
||||
version = get_versioning_start(
|
||||
project_name,
|
||||
"standalonepublisher",
|
||||
task_name=self.dbcon.Session["AVALON_TASK"],
|
||||
family=family,
|
||||
subset=subset_name
|
||||
)
|
||||
|
||||
asset_doc = None
|
||||
subset_doc = None
|
||||
|
|
|
|||
|
|
@ -410,6 +410,18 @@ class PixmapButtonPainter(QtWidgets.QWidget):
|
|||
|
||||
self._pixmap = pixmap
|
||||
self._cached_pixmap = None
|
||||
self._disabled = False
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(PixmapButtonPainter, self).resizeEvent(event)
|
||||
self._cached_pixmap = None
|
||||
self.repaint()
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
if self._disabled != enabled:
|
||||
return
|
||||
self._disabled = not enabled
|
||||
self.repaint()
|
||||
|
||||
def set_pixmap(self, pixmap):
|
||||
self._pixmap = pixmap
|
||||
|
|
@ -444,6 +456,8 @@ class PixmapButtonPainter(QtWidgets.QWidget):
|
|||
if self._cached_pixmap is None:
|
||||
self._cache_pixmap()
|
||||
|
||||
if self._disabled:
|
||||
painter.setOpacity(0.5)
|
||||
painter.drawPixmap(0, 0, self._cached_pixmap)
|
||||
|
||||
painter.end()
|
||||
|
|
@ -464,6 +478,10 @@ class PixmapButton(ClickableFrame):
|
|||
layout.setContentsMargins(*args)
|
||||
self._update_painter_geo()
|
||||
|
||||
def setEnabled(self, enabled):
|
||||
self._button_painter.set_enabled(enabled)
|
||||
super(PixmapButton, self).setEnabled(enabled)
|
||||
|
||||
def set_pixmap(self, pixmap):
|
||||
self._button_painter.set_pixmap(pixmap)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from openpype.pipeline import (
|
|||
from openpype.pipeline.workfile import get_last_workfile_with_version
|
||||
from openpype.pipeline.template_data import get_template_data_with_names
|
||||
from openpype.tools.utils import PlaceholderLineEdit
|
||||
from openpype.pipeline import version_start, get_current_host_name
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -218,7 +219,15 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
|
||||
# Version number input
|
||||
version_input = QtWidgets.QSpinBox(version_widget)
|
||||
version_input.setMinimum(1)
|
||||
version_input.setMinimum(
|
||||
version_start.get_versioning_start(
|
||||
self.data["project"]["name"],
|
||||
get_current_host_name(),
|
||||
task_name=self.data["task"]["name"],
|
||||
task_type=self.data["task"]["type"],
|
||||
family="workfile"
|
||||
)
|
||||
)
|
||||
version_input.setMaximum(9999)
|
||||
|
||||
# Last version checkbox
|
||||
|
|
@ -420,7 +429,13 @@ class SaveAsDialog(QtWidgets.QDialog):
|
|||
)[1]
|
||||
|
||||
if version is None:
|
||||
version = 1
|
||||
version = version_start.get_versioning_start(
|
||||
data["project"]["name"],
|
||||
get_current_host_name(),
|
||||
task_name=self.data["task"]["name"],
|
||||
task_type=self.data["task"]["type"],
|
||||
family="workfile"
|
||||
)
|
||||
else:
|
||||
version += 1
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ from ._api import (
|
|||
set_client_version,
|
||||
get_default_settings_variant,
|
||||
set_default_settings_variant,
|
||||
get_sender,
|
||||
set_sender,
|
||||
|
||||
get_base_url,
|
||||
get_rest_url,
|
||||
|
|
@ -92,6 +94,7 @@ from ._api import (
|
|||
get_users,
|
||||
|
||||
get_attributes_for_type,
|
||||
get_attributes_fields_for_type,
|
||||
get_default_fields_for_type,
|
||||
|
||||
get_project_anatomy_preset,
|
||||
|
|
@ -110,6 +113,11 @@ from ._api import (
|
|||
get_addons_project_settings,
|
||||
get_addons_settings,
|
||||
|
||||
get_secrets,
|
||||
get_secret,
|
||||
save_secret,
|
||||
delete_secret,
|
||||
|
||||
get_project_names,
|
||||
get_projects,
|
||||
get_project,
|
||||
|
|
@ -124,6 +132,8 @@ from ._api import (
|
|||
get_folders_hierarchy,
|
||||
|
||||
get_tasks,
|
||||
get_task_by_id,
|
||||
get_task_by_name,
|
||||
|
||||
get_folder_ids_with_products,
|
||||
get_product_by_id,
|
||||
|
|
@ -154,6 +164,7 @@ from ._api import (
|
|||
get_workfile_info,
|
||||
get_workfile_info_by_id,
|
||||
|
||||
get_thumbnail_by_id,
|
||||
get_thumbnail,
|
||||
get_folder_thumbnail,
|
||||
get_version_thumbnail,
|
||||
|
|
@ -216,6 +227,8 @@ __all__ = (
|
|||
"set_client_version",
|
||||
"get_default_settings_variant",
|
||||
"set_default_settings_variant",
|
||||
"get_sender",
|
||||
"set_sender",
|
||||
|
||||
"get_base_url",
|
||||
"get_rest_url",
|
||||
|
|
@ -278,6 +291,7 @@ __all__ = (
|
|||
"get_users",
|
||||
|
||||
"get_attributes_for_type",
|
||||
"get_attributes_fields_for_type",
|
||||
"get_default_fields_for_type",
|
||||
|
||||
"get_project_anatomy_preset",
|
||||
|
|
@ -295,6 +309,11 @@ __all__ = (
|
|||
"get_addons_project_settings",
|
||||
"get_addons_settings",
|
||||
|
||||
"get_secrets",
|
||||
"get_secret",
|
||||
"save_secret",
|
||||
"delete_secret",
|
||||
|
||||
"get_project_names",
|
||||
"get_projects",
|
||||
"get_project",
|
||||
|
|
@ -308,6 +327,8 @@ __all__ = (
|
|||
"get_folders",
|
||||
|
||||
"get_tasks",
|
||||
"get_task_by_id",
|
||||
"get_task_by_name",
|
||||
|
||||
"get_folder_ids_with_products",
|
||||
"get_product_by_id",
|
||||
|
|
@ -338,6 +359,7 @@ __all__ = (
|
|||
"get_workfile_info",
|
||||
"get_workfile_info_by_id",
|
||||
|
||||
"get_thumbnail_by_id",
|
||||
"get_thumbnail",
|
||||
"get_folder_thumbnail",
|
||||
"get_version_thumbnail",
|
||||
|
|
|
|||
62
openpype/vendor/python/common/ayon_api/_api.py
vendored
62
openpype/vendor/python/common/ayon_api/_api.py
vendored
|
|
@ -392,6 +392,28 @@ def set_default_settings_variant(variant):
|
|||
return con.set_default_settings_variant(variant)
|
||||
|
||||
|
||||
def get_sender():
|
||||
"""Sender used to send requests.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Sender name or None.
|
||||
"""
|
||||
|
||||
con = get_server_api_connection()
|
||||
return con.get_sender()
|
||||
|
||||
|
||||
def set_sender(sender):
|
||||
"""Change sender used for requests.
|
||||
|
||||
Args:
|
||||
sender (Union[str, None]): Sender name or None.
|
||||
"""
|
||||
|
||||
con = get_server_api_connection()
|
||||
return con.set_sender(sender)
|
||||
|
||||
|
||||
def get_base_url():
|
||||
con = get_server_api_connection()
|
||||
return con.get_base_url()
|
||||
|
|
@ -704,6 +726,26 @@ def get_addons_settings(*args, **kwargs):
|
|||
return con.get_addons_settings(*args, **kwargs)
|
||||
|
||||
|
||||
def get_secrets(*args, **kwargs):
|
||||
con = get_server_api_connection()
|
||||
return con.get_secrets(*args, **kwargs)
|
||||
|
||||
|
||||
def get_secret(*args, **kwargs):
|
||||
con = get_server_api_connection()
|
||||
return con.delete_secret(*args, **kwargs)
|
||||
|
||||
|
||||
def save_secret(*args, **kwargs):
|
||||
con = get_server_api_connection()
|
||||
return con.delete_secret(*args, **kwargs)
|
||||
|
||||
|
||||
def delete_secret(*args, **kwargs):
|
||||
con = get_server_api_connection()
|
||||
return con.delete_secret(*args, **kwargs)
|
||||
|
||||
|
||||
def get_project_names(*args, **kwargs):
|
||||
con = get_server_api_connection()
|
||||
return con.get_project_names(*args, **kwargs)
|
||||
|
|
@ -734,6 +776,16 @@ def get_tasks(*args, **kwargs):
|
|||
return con.get_tasks(*args, **kwargs)
|
||||
|
||||
|
||||
def get_task_by_id(*args, **kwargs):
|
||||
con = get_server_api_connection()
|
||||
return con.get_task_by_id(*args, **kwargs)
|
||||
|
||||
|
||||
def get_task_by_name(*args, **kwargs):
|
||||
con = get_server_api_connection()
|
||||
return con.get_task_by_name(*args, **kwargs)
|
||||
|
||||
|
||||
def get_folder_by_id(*args, **kwargs):
|
||||
con = get_server_api_connection()
|
||||
return con.get_folder_by_id(*args, **kwargs)
|
||||
|
|
@ -904,6 +956,11 @@ def delete_project(project_name):
|
|||
return con.delete_project(project_name)
|
||||
|
||||
|
||||
def get_thumbnail_by_id(project_name, thumbnail_id):
|
||||
con = get_server_api_connection()
|
||||
con.get_thumbnail_by_id(project_name, thumbnail_id)
|
||||
|
||||
|
||||
def get_thumbnail(project_name, entity_type, entity_id, thumbnail_id=None):
|
||||
con = get_server_api_connection()
|
||||
con.get_thumbnail(project_name, entity_type, entity_id, thumbnail_id)
|
||||
|
|
@ -934,6 +991,11 @@ def update_thumbnail(project_name, thumbnail_id, src_filepath):
|
|||
return con.update_thumbnail(project_name, thumbnail_id, src_filepath)
|
||||
|
||||
|
||||
def get_attributes_fields_for_type(entity_type):
|
||||
con = get_server_api_connection()
|
||||
return con.get_attributes_fields_for_type(entity_type)
|
||||
|
||||
|
||||
def get_default_fields_for_type(entity_type):
|
||||
con = get_server_api_connection()
|
||||
return con.get_default_fields_for_type(entity_type)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,25 @@ SERVER_API_ENV_KEY = "AYON_API_KEY"
|
|||
# Backwards compatibility
|
||||
SERVER_TOKEN_ENV_KEY = SERVER_API_ENV_KEY
|
||||
|
||||
# --- User ---
|
||||
DEFAULT_USER_FIELDS = {
|
||||
"roles",
|
||||
"name",
|
||||
"isService",
|
||||
"isManager",
|
||||
"isGuest",
|
||||
"isAdmin",
|
||||
"defaultRoles",
|
||||
"createdAt",
|
||||
"active",
|
||||
"hasPassword",
|
||||
"updatedAt",
|
||||
"apiKeyPreview",
|
||||
"attrib.avatarUrl",
|
||||
"attrib.email",
|
||||
"attrib.fullName",
|
||||
}
|
||||
|
||||
# --- Product types ---
|
||||
DEFAULT_PRODUCT_TYPE_FIELDS = {
|
||||
"name",
|
||||
|
|
|
|||
748
openpype/vendor/python/common/ayon_api/entity_hub.py
vendored
748
openpype/vendor/python/common/ayon_api/entity_hub.py
vendored
|
|
@ -1,10 +1,11 @@
|
|||
import re
|
||||
import copy
|
||||
import collections
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import six
|
||||
from ._api import get_server_api_connection
|
||||
from .utils import create_entity_id, convert_entity_id
|
||||
from .utils import create_entity_id, convert_entity_id, slugify_string
|
||||
|
||||
UNKNOWN_VALUE = object()
|
||||
PROJECT_PARENT_ID = object()
|
||||
|
|
@ -545,6 +546,7 @@ class EntityHub(object):
|
|||
library=project["library"],
|
||||
folder_types=project["folderTypes"],
|
||||
task_types=project["taskTypes"],
|
||||
statuses=project["statuses"],
|
||||
name=project["name"],
|
||||
attribs=project["ownAttrib"],
|
||||
data=project["data"],
|
||||
|
|
@ -775,8 +777,7 @@ class EntityHub(object):
|
|||
"projects/{}".format(self.project_name),
|
||||
**project_changes
|
||||
)
|
||||
if response.status_code != 204:
|
||||
raise ValueError("Failed to update project")
|
||||
response.raise_for_status()
|
||||
|
||||
self.project_entity.lock()
|
||||
|
||||
|
|
@ -1485,6 +1486,722 @@ class BaseEntity(object):
|
|||
self._children_ids = set(children_ids)
|
||||
|
||||
|
||||
class ProjectStatus:
|
||||
"""Project status class.
|
||||
|
||||
Args:
|
||||
name (str): Name of the status. e.g. 'In progress'
|
||||
short_name (Optional[str]): Short name of the status. e.g. 'IP'
|
||||
state (Optional[Literal[not_started, in_progress, done, blocked]]): A
|
||||
state of the status.
|
||||
icon (Optional[str]): Icon of the status. e.g. 'play_arrow'.
|
||||
color (Optional[str]): Color of the status. e.g. '#eeeeee'.
|
||||
index (Optional[int]): Index of the status.
|
||||
project_statuses (Optional[_ProjectStatuses]): Project statuses
|
||||
wrapper.
|
||||
"""
|
||||
|
||||
valid_states = ("not_started", "in_progress", "done", "blocked")
|
||||
color_regex = re.compile(r"#([a-f0-9]{6})$")
|
||||
default_state = "in_progress"
|
||||
default_color = "#eeeeee"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
short_name=None,
|
||||
state=None,
|
||||
icon=None,
|
||||
color=None,
|
||||
index=None,
|
||||
project_statuses=None,
|
||||
is_new=None,
|
||||
):
|
||||
short_name = short_name or ""
|
||||
icon = icon or ""
|
||||
state = state or self.default_state
|
||||
color = color or self.default_color
|
||||
self._name = name
|
||||
self._short_name = short_name
|
||||
self._icon = icon
|
||||
self._slugified_name = None
|
||||
self._state = None
|
||||
self._color = None
|
||||
self.set_state(state)
|
||||
self.set_color(color)
|
||||
|
||||
self._original_name = name
|
||||
self._original_short_name = short_name
|
||||
self._original_icon = icon
|
||||
self._original_state = state
|
||||
self._original_color = color
|
||||
self._original_index = index
|
||||
|
||||
self._index = index
|
||||
self._project_statuses = project_statuses
|
||||
if is_new is None:
|
||||
is_new = index is None or project_statuses is None
|
||||
self._is_new = is_new
|
||||
|
||||
def __str__(self):
|
||||
short_name = ""
|
||||
if self.short_name:
|
||||
short_name = "({})".format(self.short_name)
|
||||
return "<{} {}{}>".format(
|
||||
self.__class__.__name__, self.name, short_name
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key in {
|
||||
"name", "short_name", "icon", "state", "color", "slugified_name"
|
||||
}:
|
||||
return getattr(self, key)
|
||||
raise KeyError(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key in {"name", "short_name", "icon", "state", "color"}:
|
||||
return setattr(self, key, value)
|
||||
raise KeyError(key)
|
||||
|
||||
def lock(self):
|
||||
"""Lock status.
|
||||
|
||||
Changes were commited and current values are now the original values.
|
||||
"""
|
||||
|
||||
self._is_new = False
|
||||
self._original_name = self.name
|
||||
self._original_short_name = self.short_name
|
||||
self._original_icon = self.icon
|
||||
self._original_state = self.state
|
||||
self._original_color = self.color
|
||||
self._original_index = self.index
|
||||
|
||||
@staticmethod
|
||||
def slugify_name(name):
|
||||
"""Slugify status name for name comparison.
|
||||
|
||||
Args:
|
||||
name (str): Name of the status.
|
||||
|
||||
Returns:
|
||||
str: Slugified name.
|
||||
"""
|
||||
|
||||
return slugify_string(name.lower())
|
||||
|
||||
def get_project_statuses(self):
|
||||
"""Internal logic method.
|
||||
|
||||
Returns:
|
||||
_ProjectStatuses: Project statuses object.
|
||||
"""
|
||||
|
||||
return self._project_statuses
|
||||
|
||||
def set_project_statuses(self, project_statuses):
|
||||
"""Internal logic method to change parent object.
|
||||
|
||||
Args:
|
||||
project_statuses (_ProjectStatuses): Project statuses object.
|
||||
"""
|
||||
|
||||
self._project_statuses = project_statuses
|
||||
|
||||
def unset_project_statuses(self, project_statuses):
|
||||
"""Internal logic method to unset parent object.
|
||||
|
||||
Args:
|
||||
project_statuses (_ProjectStatuses): Project statuses object.
|
||||
"""
|
||||
|
||||
if self._project_statuses is project_statuses:
|
||||
self._project_statuses = None
|
||||
self._index = None
|
||||
|
||||
@property
|
||||
def changed(self):
|
||||
"""Status has changed.
|
||||
|
||||
Returns:
|
||||
bool: Status has changed.
|
||||
"""
|
||||
|
||||
return (
|
||||
self._is_new
|
||||
or self._original_name != self._name
|
||||
or self._original_short_name != self._short_name
|
||||
or self._original_index != self._index
|
||||
or self._original_state != self._state
|
||||
or self._original_icon != self._icon
|
||||
or self._original_color != self._color
|
||||
)
|
||||
|
||||
def delete(self):
|
||||
"""Remove status from project statuses object."""
|
||||
|
||||
if self._project_statuses is not None:
|
||||
self._project_statuses.remove(self)
|
||||
|
||||
def get_index(self):
|
||||
"""Get index of status.
|
||||
|
||||
Returns:
|
||||
Union[int, None]: Index of status or None if status is not under
|
||||
project.
|
||||
"""
|
||||
|
||||
return self._index
|
||||
|
||||
def set_index(self, index, **kwargs):
|
||||
"""Change status index.
|
||||
|
||||
Returns:
|
||||
Union[int, None]: Index of status or None if status is not under
|
||||
project.
|
||||
"""
|
||||
|
||||
if kwargs.get("from_parent"):
|
||||
self._index = index
|
||||
else:
|
||||
self._project_statuses.set_status_index(self, index)
|
||||
|
||||
def get_name(self):
|
||||
"""Status name.
|
||||
|
||||
Returns:
|
||||
str: Status name.
|
||||
"""
|
||||
|
||||
return self._name
|
||||
|
||||
def set_name(self, name):
|
||||
"""Change status name.
|
||||
|
||||
Args:
|
||||
name (str): New status name.
|
||||
"""
|
||||
|
||||
if not isinstance(name, six.string_types):
|
||||
raise TypeError("Name must be a string.")
|
||||
if name == self._name:
|
||||
return
|
||||
self._name = name
|
||||
self._slugified_name = None
|
||||
|
||||
def get_short_name(self):
|
||||
"""Status short name 3 letters tops.
|
||||
|
||||
Returns:
|
||||
str: Status short name.
|
||||
"""
|
||||
|
||||
return self._short_name
|
||||
|
||||
def set_short_name(self, short_name):
|
||||
"""Change status short name.
|
||||
|
||||
Args:
|
||||
short_name (str): New status short name. 3 letters tops.
|
||||
"""
|
||||
|
||||
if not isinstance(short_name, six.string_types):
|
||||
raise TypeError("Short name must be a string.")
|
||||
self._short_name = short_name
|
||||
|
||||
def get_icon(self):
|
||||
"""Name of icon to use for status.
|
||||
|
||||
Returns:
|
||||
str: Name of the icon.
|
||||
"""
|
||||
|
||||
return self._icon
|
||||
|
||||
def set_icon(self, icon):
|
||||
"""Change status icon name.
|
||||
|
||||
Args:
|
||||
icon (str): Name of the icon.
|
||||
"""
|
||||
|
||||
if icon is None:
|
||||
icon = ""
|
||||
if not isinstance(icon, six.string_types):
|
||||
raise TypeError("Icon name must be a string.")
|
||||
self._icon = icon
|
||||
|
||||
@property
|
||||
def slugified_name(self):
|
||||
"""Slugified and lowere status name.
|
||||
|
||||
Can be used for comparison of existing statuses. e.g. 'In Progress'
|
||||
vs. 'in-progress'.
|
||||
|
||||
Returns:
|
||||
str: Slugified and lower status name.
|
||||
"""
|
||||
|
||||
if self._slugified_name is None:
|
||||
self._slugified_name = self.slugify_name(self.name)
|
||||
return self._slugified_name
|
||||
|
||||
def get_state(self):
|
||||
"""Get state of project status.
|
||||
|
||||
Return:
|
||||
Literal[not_started, in_progress, done, blocked]: General
|
||||
state of status.
|
||||
"""
|
||||
|
||||
return self._state
|
||||
|
||||
def set_state(self, state):
|
||||
"""Set color of project status.
|
||||
|
||||
Args:
|
||||
state (Literal[not_started, in_progress, done, blocked]): General
|
||||
state of status.
|
||||
"""
|
||||
|
||||
if state not in self.valid_states:
|
||||
raise ValueError("Invalid state '{}'".format(str(state)))
|
||||
self._state = state
|
||||
|
||||
def get_color(self):
|
||||
"""Get color of project status.
|
||||
|
||||
Returns:
|
||||
str: Status color.
|
||||
"""
|
||||
|
||||
return self._color
|
||||
|
||||
def set_color(self, color):
|
||||
"""Set color of project status.
|
||||
|
||||
Args:
|
||||
color (str): Color in hex format. Example: '#ff0000'.
|
||||
"""
|
||||
|
||||
if not isinstance(color, six.string_types):
|
||||
raise TypeError(
|
||||
"Color must be string got '{}'".format(type(color)))
|
||||
color = color.lower()
|
||||
if self.color_regex.fullmatch(color) is None:
|
||||
raise ValueError("Invalid color value '{}'".format(color))
|
||||
self._color = color
|
||||
|
||||
name = property(get_name, set_name)
|
||||
short_name = property(get_short_name, set_short_name)
|
||||
project_statuses = property(get_project_statuses, set_project_statuses)
|
||||
index = property(get_index, set_index)
|
||||
state = property(get_state, set_state)
|
||||
color = property(get_color, set_color)
|
||||
icon = property(get_icon, set_icon)
|
||||
|
||||
def _validate_other_p_statuses(self, other):
|
||||
"""Validate if other status can be used for move.
|
||||
|
||||
To be able to work with other status, and position them in relation,
|
||||
they must belong to same existing object of '_ProjectStatuses'.
|
||||
|
||||
Args:
|
||||
other (ProjectStatus): Other status to validate.
|
||||
"""
|
||||
|
||||
o_project_statuses = other.project_statuses
|
||||
m_project_statuses = self.project_statuses
|
||||
if o_project_statuses is None and m_project_statuses is None:
|
||||
raise ValueError("Both statuses are not assigned to a project.")
|
||||
|
||||
missing_status = None
|
||||
if o_project_statuses is None:
|
||||
missing_status = other
|
||||
elif m_project_statuses is None:
|
||||
missing_status = self
|
||||
if missing_status is not None:
|
||||
raise ValueError(
|
||||
"Status '{}' is not assigned to a project.".format(
|
||||
missing_status.name))
|
||||
if m_project_statuses is not o_project_statuses:
|
||||
raise ValueError(
|
||||
"Statuse are assigned to different projects."
|
||||
" Cannot execute move."
|
||||
)
|
||||
|
||||
def move_before(self, other):
|
||||
"""Move status before other status.
|
||||
|
||||
Args:
|
||||
other (ProjectStatus): Status to move before.
|
||||
"""
|
||||
|
||||
self._validate_other_p_statuses(other)
|
||||
self._project_statuses.set_status_index(self, other.index)
|
||||
|
||||
def move_after(self, other):
|
||||
"""Move status after other status.
|
||||
|
||||
Args:
|
||||
other (ProjectStatus): Status to move after.
|
||||
"""
|
||||
|
||||
self._validate_other_p_statuses(other)
|
||||
self._project_statuses.set_status_index(self, other.index + 1)
|
||||
|
||||
def to_data(self):
|
||||
"""Convert status to data.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: Status data.
|
||||
"""
|
||||
|
||||
output = {
|
||||
"name": self.name,
|
||||
"shortName": self.short_name,
|
||||
"state": self.state,
|
||||
"icon": self.icon,
|
||||
"color": self.color,
|
||||
}
|
||||
if (
|
||||
not self._is_new
|
||||
and self._original_name
|
||||
and self.name != self._original_name
|
||||
):
|
||||
output["original_name"] = self._original_name
|
||||
return output
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data, index=None, project_statuses=None):
|
||||
"""Create project status from data.
|
||||
|
||||
Args:
|
||||
data (dict[str, str]): Status data.
|
||||
index (Optional[int]): Status index.
|
||||
project_statuses (Optional[ProjectStatuses]): Project statuses
|
||||
object which wraps the status for a project.
|
||||
"""
|
||||
|
||||
return cls(
|
||||
data["name"],
|
||||
data.get("shortName", data.get("short_name")),
|
||||
data.get("state"),
|
||||
data.get("icon"),
|
||||
data.get("color"),
|
||||
index=index,
|
||||
project_statuses=project_statuses
|
||||
)
|
||||
|
||||
|
||||
class _ProjectStatuses:
|
||||
"""Wrapper for project statuses.
|
||||
|
||||
Supports basic methods to add, change or remove statuses from a project.
|
||||
|
||||
To add new statuses use 'create' or 'add_status' methods. To change
|
||||
statuses receive them by one of the getter methods and change their
|
||||
values.
|
||||
|
||||
Todos:
|
||||
Validate if statuses are duplicated.
|
||||
"""
|
||||
|
||||
def __init__(self, statuses):
|
||||
self._statuses = [
|
||||
ProjectStatus.from_data(status, idx, self)
|
||||
for idx, status in enumerate(statuses)
|
||||
]
|
||||
self._orig_status_length = len(self._statuses)
|
||||
self._set_called = False
|
||||
|
||||
def __len__(self):
|
||||
return len(self._statuses)
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over statuses.
|
||||
|
||||
Yields:
|
||||
ProjectStatus: Project status.
|
||||
"""
|
||||
|
||||
for status in self._statuses:
|
||||
yield status
|
||||
|
||||
def create(
|
||||
self,
|
||||
name,
|
||||
short_name=None,
|
||||
state=None,
|
||||
icon=None,
|
||||
color=None,
|
||||
):
|
||||
"""Create project status.
|
||||
|
||||
Args:
|
||||
name (str): Name of the status. e.g. 'In progress'
|
||||
short_name (Optional[str]): Short name of the status. e.g. 'IP'
|
||||
state (Optional[Literal[not_started, in_progress, done, blocked]]): A
|
||||
state of the status.
|
||||
icon (Optional[str]): Icon of the status. e.g. 'play_arrow'.
|
||||
color (Optional[str]): Color of the status. e.g. '#eeeeee'.
|
||||
|
||||
Returns:
|
||||
ProjectStatus: Created project status.
|
||||
"""
|
||||
|
||||
status = ProjectStatus(
|
||||
name, short_name, state, icon, color, is_new=True
|
||||
)
|
||||
self.append(status)
|
||||
return status
|
||||
|
||||
def lock(self):
|
||||
"""Lock statuses.
|
||||
|
||||
Changes were commited and current values are now the original values.
|
||||
"""
|
||||
|
||||
self._orig_status_length = len(self._statuses)
|
||||
self._set_called = False
|
||||
for status in self._statuses:
|
||||
status.lock()
|
||||
|
||||
def to_data(self):
|
||||
"""Convert to project statuses data."""
|
||||
|
||||
return [
|
||||
status.to_data()
|
||||
for status in self._statuses
|
||||
]
|
||||
|
||||
def set(self, statuses):
|
||||
"""Explicitly override statuses.
|
||||
|
||||
This method does not handle if statuses changed or not.
|
||||
|
||||
Args:
|
||||
statuses (list[dict[str, str]]): List of statuses data.
|
||||
"""
|
||||
|
||||
self._set_called = True
|
||||
self._statuses = [
|
||||
ProjectStatus.from_data(status, idx, self)
|
||||
for idx, status in enumerate(statuses)
|
||||
]
|
||||
|
||||
@property
|
||||
def changed(self):
|
||||
"""Statuses have changed.
|
||||
|
||||
Returns:
|
||||
bool: True if statuses changed, False otherwise.
|
||||
"""
|
||||
|
||||
if self._set_called:
|
||||
return True
|
||||
|
||||
# Check if status length changed
|
||||
# - when all statuses are removed it is a changed
|
||||
if self._orig_status_length != len(self._statuses):
|
||||
return True
|
||||
# Go through all statuses and check if any of them changed
|
||||
for status in self._statuses:
|
||||
if status.changed:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get(self, name, default=None):
|
||||
"""Get status by name.
|
||||
|
||||
Args:
|
||||
name (str): Status name.
|
||||
default (Any): Default value of status is not found.
|
||||
|
||||
Returns:
|
||||
Union[ProjectStatus, Any]: Status or default value.
|
||||
"""
|
||||
|
||||
return next(
|
||||
(
|
||||
status
|
||||
for status in self._statuses
|
||||
if status.name == name
|
||||
),
|
||||
default
|
||||
)
|
||||
|
||||
get_status_by_name = get
|
||||
|
||||
def index(self, status, **kwargs):
|
||||
"""Get status index.
|
||||
|
||||
Args:
|
||||
status (ProjectStatus): Status to get index of.
|
||||
default (Optional[Any]): Default value if status is not found.
|
||||
|
||||
Returns:
|
||||
Union[int, Any]: Status index.
|
||||
|
||||
Raises:
|
||||
ValueError: If status is not found and default value is not
|
||||
defined.
|
||||
"""
|
||||
|
||||
output = next(
|
||||
(
|
||||
idx
|
||||
for idx, st in enumerate(self._statuses)
|
||||
if st is status
|
||||
),
|
||||
None
|
||||
)
|
||||
if output is not None:
|
||||
return output
|
||||
|
||||
if "default" in kwargs:
|
||||
return kwargs["default"]
|
||||
raise ValueError("Status '{}' not found".format(status.name))
|
||||
|
||||
def get_status_by_slugified_name(self, name):
|
||||
"""Get status by slugified name.
|
||||
|
||||
Args:
|
||||
name (str): Status name. Is slugified before search.
|
||||
|
||||
Returns:
|
||||
Union[ProjectStatus, None]: Status or None if not found.
|
||||
"""
|
||||
|
||||
slugified_name = ProjectStatus.slugify_name(name)
|
||||
return next(
|
||||
(
|
||||
status
|
||||
for status in self._statuses
|
||||
if status.slugified_name == slugified_name
|
||||
),
|
||||
None
|
||||
)
|
||||
|
||||
def remove_by_name(self, name, ignore_missing=False):
|
||||
"""Remove status by name.
|
||||
|
||||
Args:
|
||||
name (str): Status name.
|
||||
ignore_missing (Optional[bool]): If True, no error is raised if
|
||||
status is not found.
|
||||
|
||||
Returns:
|
||||
ProjectStatus: Removed status.
|
||||
"""
|
||||
|
||||
matching_status = self.get(name)
|
||||
if matching_status is None:
|
||||
if ignore_missing:
|
||||
return
|
||||
raise ValueError(
|
||||
"Status '{}' not found in project".format(name))
|
||||
return self.remove(matching_status)
|
||||
|
||||
def remove(self, status, ignore_missing=False):
|
||||
"""Remove status.
|
||||
|
||||
Args:
|
||||
status (ProjectStatus): Status to remove.
|
||||
ignore_missing (Optional[bool]): If True, no error is raised if
|
||||
status is not found.
|
||||
|
||||
Returns:
|
||||
Union[ProjectStatus, None]: Removed status.
|
||||
"""
|
||||
|
||||
index = self.index(status, default=None)
|
||||
if index is None:
|
||||
if ignore_missing:
|
||||
return None
|
||||
raise ValueError("Status '{}' not in project".format(status))
|
||||
|
||||
return self.pop(index)
|
||||
|
||||
def pop(self, index):
|
||||
"""Remove status by index.
|
||||
|
||||
Args:
|
||||
index (int): Status index.
|
||||
|
||||
Returns:
|
||||
ProjectStatus: Removed status.
|
||||
"""
|
||||
|
||||
status = self._statuses.pop(index)
|
||||
status.unset_project_statuses(self)
|
||||
for st in self._statuses[index:]:
|
||||
st.set_index(st.index - 1, from_parent=True)
|
||||
return status
|
||||
|
||||
def insert(self, index, status):
|
||||
"""Insert status at index.
|
||||
|
||||
Args:
|
||||
index (int): Status index.
|
||||
status (Union[ProjectStatus, dict[str, str]]): Status to insert.
|
||||
Can be either status object or status data.
|
||||
|
||||
Returns:
|
||||
ProjectStatus: Inserted status.
|
||||
"""
|
||||
|
||||
if not isinstance(status, ProjectStatus):
|
||||
status = ProjectStatus.from_data(status)
|
||||
|
||||
start_index = index
|
||||
end_index = len(self._statuses) + 1
|
||||
matching_index = self.index(status, default=None)
|
||||
if matching_index is not None:
|
||||
if matching_index == index:
|
||||
status.set_index(index, from_parent=True)
|
||||
return
|
||||
|
||||
self._statuses.pop(matching_index)
|
||||
if matching_index < index:
|
||||
start_index = matching_index
|
||||
end_index = index + 1
|
||||
else:
|
||||
end_index -= 1
|
||||
|
||||
status.set_project_statuses(self)
|
||||
self._statuses.insert(index, status)
|
||||
for idx, st in enumerate(self._statuses[start_index:end_index]):
|
||||
st.set_index(start_index + idx, from_parent=True)
|
||||
return status
|
||||
|
||||
def append(self, status):
|
||||
"""Add new status to the end of the list.
|
||||
|
||||
Args:
|
||||
status (Union[ProjectStatus, dict[str, str]]): Status to insert.
|
||||
Can be either status object or status data.
|
||||
|
||||
Returns:
|
||||
ProjectStatus: Inserted status.
|
||||
"""
|
||||
|
||||
return self.insert(len(self._statuses), status)
|
||||
|
||||
def set_status_index(self, status, index):
|
||||
"""Set status index.
|
||||
|
||||
Args:
|
||||
status (ProjectStatus): Status to set index.
|
||||
index (int): New status index.
|
||||
"""
|
||||
|
||||
return self.insert(index, status)
|
||||
|
||||
|
||||
class ProjectEntity(BaseEntity):
|
||||
"""Entity representing project on AYON server.
|
||||
|
||||
|
|
@ -1514,7 +2231,14 @@ class ProjectEntity(BaseEntity):
|
|||
default_task_type_icon = "task_alt"
|
||||
|
||||
def __init__(
|
||||
self, project_code, library, folder_types, task_types, *args, **kwargs
|
||||
self,
|
||||
project_code,
|
||||
library,
|
||||
folder_types,
|
||||
task_types,
|
||||
statuses,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
super(ProjectEntity, self).__init__(*args, **kwargs)
|
||||
|
||||
|
|
@ -1522,11 +2246,13 @@ class ProjectEntity(BaseEntity):
|
|||
self._library_project = library
|
||||
self._folder_types = folder_types
|
||||
self._task_types = task_types
|
||||
self._statuses_obj = _ProjectStatuses(statuses)
|
||||
|
||||
self._orig_project_code = project_code
|
||||
self._orig_library_project = library
|
||||
self._orig_folder_types = copy.deepcopy(folder_types)
|
||||
self._orig_task_types = copy.deepcopy(task_types)
|
||||
self._orig_statuses = copy.deepcopy(statuses)
|
||||
|
||||
def _prepare_entity_id(self, entity_id):
|
||||
if entity_id != self.project_name:
|
||||
|
|
@ -1573,13 +2299,24 @@ class ProjectEntity(BaseEntity):
|
|||
new_task_types.append(task_type)
|
||||
self._task_types = new_task_types
|
||||
|
||||
def get_orig_statuses(self):
|
||||
return copy.deepcopy(self._orig_statuses)
|
||||
|
||||
def get_statuses(self):
|
||||
return self._statuses_obj
|
||||
|
||||
def set_statuses(self, statuses):
|
||||
self._statuses_obj.set(statuses)
|
||||
|
||||
folder_types = property(get_folder_types, set_folder_types)
|
||||
task_types = property(get_task_types, set_task_types)
|
||||
statuses = property(get_statuses, set_statuses)
|
||||
|
||||
def lock(self):
|
||||
super(ProjectEntity, self).lock()
|
||||
self._orig_folder_types = copy.deepcopy(self._folder_types)
|
||||
self._orig_task_types = copy.deepcopy(self._task_types)
|
||||
self._statuses_obj.lock()
|
||||
|
||||
@property
|
||||
def changes(self):
|
||||
|
|
@ -1590,6 +2327,9 @@ class ProjectEntity(BaseEntity):
|
|||
if self._orig_task_types != self._task_types:
|
||||
changes["taskTypes"] = self.get_task_types()
|
||||
|
||||
if self._statuses_obj.changed:
|
||||
changes["statuses"] = self._statuses_obj.to_data()
|
||||
|
||||
return changes
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -462,3 +462,28 @@ def events_graphql_query(fields):
|
|||
for k, v in value.items():
|
||||
query_queue.append((k, v, field))
|
||||
return query
|
||||
|
||||
|
||||
def users_graphql_query(fields):
|
||||
query = GraphQlQuery("Users")
|
||||
names_var = query.add_variable("userNames", "[String!]")
|
||||
|
||||
users_field = query.add_field_with_edges("users")
|
||||
users_field.set_filter("names", names_var)
|
||||
|
||||
nested_fields = fields_to_dict(set(fields))
|
||||
|
||||
query_queue = collections.deque()
|
||||
for key, value in nested_fields.items():
|
||||
query_queue.append((key, value, users_field))
|
||||
|
||||
while query_queue:
|
||||
item = query_queue.popleft()
|
||||
key, value, parent = item
|
||||
field = parent.add_field(key)
|
||||
if value is FIELD_VALUE:
|
||||
continue
|
||||
|
||||
for k, v in value.items():
|
||||
query_queue.append((k, v, field))
|
||||
return query
|
||||
|
|
|
|||
117
openpype/vendor/python/common/ayon_api/operations.py
vendored
117
openpype/vendor/python/common/ayon_api/operations.py
vendored
|
|
@ -1,3 +1,4 @@
|
|||
import os
|
||||
import copy
|
||||
import collections
|
||||
import uuid
|
||||
|
|
@ -22,6 +23,8 @@ def new_folder_entity(
|
|||
name,
|
||||
folder_type,
|
||||
parent_id=None,
|
||||
status=None,
|
||||
tags=None,
|
||||
attribs=None,
|
||||
data=None,
|
||||
thumbnail_id=None,
|
||||
|
|
@ -32,12 +35,14 @@ def new_folder_entity(
|
|||
Args:
|
||||
name (str): Is considered as unique identifier of folder in project.
|
||||
folder_type (str): Type of folder.
|
||||
parent_id (Optional[str]]): Id of parent folder.
|
||||
parent_id (Optional[str]): Parent folder id.
|
||||
status (Optional[str]): Product status.
|
||||
tags (Optional[List[str]]): List of tags.
|
||||
attribs (Optional[Dict[str, Any]]): Explicitly set attributes
|
||||
of folder.
|
||||
data (Optional[Dict[str, Any]]): Custom folder data. Empty dictionary
|
||||
is used if not passed.
|
||||
thumbnail_id (Optional[str]): Id of thumbnail related to folder.
|
||||
thumbnail_id (Optional[str]): Thumbnail id related to folder.
|
||||
entity_id (Optional[str]): Predefined id of entity. New id is
|
||||
created if not passed.
|
||||
|
||||
|
|
@ -54,7 +59,7 @@ def new_folder_entity(
|
|||
if parent_id is not None:
|
||||
parent_id = _create_or_convert_to_id(parent_id)
|
||||
|
||||
return {
|
||||
output = {
|
||||
"id": _create_or_convert_to_id(entity_id),
|
||||
"name": name,
|
||||
# This will be ignored
|
||||
|
|
@ -64,6 +69,11 @@ def new_folder_entity(
|
|||
"attrib": attribs,
|
||||
"thumbnailId": thumbnail_id
|
||||
}
|
||||
if status:
|
||||
output["status"] = status
|
||||
if tags:
|
||||
output["tags"] = tags
|
||||
return output
|
||||
|
||||
|
||||
def new_product_entity(
|
||||
|
|
@ -71,6 +81,7 @@ def new_product_entity(
|
|||
product_type,
|
||||
folder_id,
|
||||
status=None,
|
||||
tags=None,
|
||||
attribs=None,
|
||||
data=None,
|
||||
entity_id=None
|
||||
|
|
@ -81,8 +92,9 @@ def new_product_entity(
|
|||
name (str): Is considered as unique identifier of
|
||||
product under folder.
|
||||
product_type (str): Product type.
|
||||
folder_id (str): Id of parent folder.
|
||||
folder_id (str): Parent folder id.
|
||||
status (Optional[str]): Product status.
|
||||
tags (Optional[List[str]]): List of tags.
|
||||
attribs (Optional[Dict[str, Any]]): Explicitly set attributes
|
||||
of product.
|
||||
data (Optional[Dict[str, Any]]): product entity data. Empty dictionary
|
||||
|
|
@ -110,6 +122,8 @@ def new_product_entity(
|
|||
}
|
||||
if status:
|
||||
output["status"] = status
|
||||
if tags:
|
||||
output["tags"] = tags
|
||||
return output
|
||||
|
||||
|
||||
|
|
@ -119,6 +133,8 @@ def new_version_entity(
|
|||
task_id=None,
|
||||
thumbnail_id=None,
|
||||
author=None,
|
||||
status=None,
|
||||
tags=None,
|
||||
attribs=None,
|
||||
data=None,
|
||||
entity_id=None
|
||||
|
|
@ -128,10 +144,12 @@ def new_version_entity(
|
|||
Args:
|
||||
version (int): Is considered as unique identifier of version
|
||||
under product.
|
||||
product_id (str): Id of parent product.
|
||||
task_id (Optional[str]]): Id of task under which product was created.
|
||||
thumbnail_id (Optional[str]]): Thumbnail related to version.
|
||||
author (Optional[str]]): Name of version author.
|
||||
product_id (str): Parent product id.
|
||||
task_id (Optional[str]): Task id under which product was created.
|
||||
thumbnail_id (Optional[str]): Thumbnail related to version.
|
||||
author (Optional[str]): Name of version author.
|
||||
status (Optional[str]): Version status.
|
||||
tags (Optional[List[str]]): List of tags.
|
||||
attribs (Optional[Dict[str, Any]]): Explicitly set attributes
|
||||
of version.
|
||||
data (Optional[Dict[str, Any]]): Version entity custom data.
|
||||
|
|
@ -164,6 +182,10 @@ def new_version_entity(
|
|||
output["thumbnailId"] = thumbnail_id
|
||||
if author:
|
||||
output["author"] = author
|
||||
if tags:
|
||||
output["tags"] = tags
|
||||
if status:
|
||||
output["status"] = status
|
||||
return output
|
||||
|
||||
|
||||
|
|
@ -173,6 +195,8 @@ def new_hero_version_entity(
|
|||
task_id=None,
|
||||
thumbnail_id=None,
|
||||
author=None,
|
||||
status=None,
|
||||
tags=None,
|
||||
attribs=None,
|
||||
data=None,
|
||||
entity_id=None
|
||||
|
|
@ -182,10 +206,12 @@ def new_hero_version_entity(
|
|||
Args:
|
||||
version (int): Is considered as unique identifier of version
|
||||
under product. Should be same as standard version if there is any.
|
||||
product_id (str): Id of parent product.
|
||||
task_id (Optional[str]): Id of task under which product was created.
|
||||
product_id (str): Parent product id.
|
||||
task_id (Optional[str]): Task id under which product was created.
|
||||
thumbnail_id (Optional[str]): Thumbnail related to version.
|
||||
author (Optional[str]): Name of version author.
|
||||
status (Optional[str]): Version status.
|
||||
tags (Optional[List[str]]): List of tags.
|
||||
attribs (Optional[Dict[str, Any]]): Explicitly set attributes
|
||||
of version.
|
||||
data (Optional[Dict[str, Any]]): Version entity data.
|
||||
|
|
@ -215,18 +241,32 @@ def new_hero_version_entity(
|
|||
output["thumbnailId"] = thumbnail_id
|
||||
if author:
|
||||
output["author"] = author
|
||||
if tags:
|
||||
output["tags"] = tags
|
||||
if status:
|
||||
output["status"] = status
|
||||
return output
|
||||
|
||||
|
||||
def new_representation_entity(
|
||||
name, version_id, attribs=None, data=None, entity_id=None
|
||||
name,
|
||||
version_id,
|
||||
files,
|
||||
status=None,
|
||||
tags=None,
|
||||
attribs=None,
|
||||
data=None,
|
||||
entity_id=None
|
||||
):
|
||||
"""Create skeleton data of representation entity.
|
||||
|
||||
Args:
|
||||
name (str): Representation name considered as unique identifier
|
||||
of representation under version.
|
||||
version_id (str): Id of parent version.
|
||||
version_id (str): Parent version id.
|
||||
files (list[dict[str, str]]): List of files in representation.
|
||||
status (Optional[str]): Representation status.
|
||||
tags (Optional[List[str]]): List of tags.
|
||||
attribs (Optional[Dict[str, Any]]): Explicitly set attributes
|
||||
of representation.
|
||||
data (Optional[Dict[str, Any]]): Representation entity data.
|
||||
|
|
@ -243,27 +283,42 @@ def new_representation_entity(
|
|||
if data is None:
|
||||
data = {}
|
||||
|
||||
return {
|
||||
output = {
|
||||
"id": _create_or_convert_to_id(entity_id),
|
||||
"versionId": _create_or_convert_to_id(version_id),
|
||||
"files": files,
|
||||
"name": name,
|
||||
"data": data,
|
||||
"attrib": attribs
|
||||
}
|
||||
if tags:
|
||||
output["tags"] = tags
|
||||
if status:
|
||||
output["status"] = status
|
||||
return output
|
||||
|
||||
|
||||
def new_workfile_info_doc(
|
||||
filename, folder_id, task_name, files, data=None, entity_id=None
|
||||
def new_workfile_info(
|
||||
filepath,
|
||||
task_id,
|
||||
status=None,
|
||||
tags=None,
|
||||
attribs=None,
|
||||
description=None,
|
||||
data=None,
|
||||
entity_id=None
|
||||
):
|
||||
"""Create skeleton data of workfile info entity.
|
||||
|
||||
Workfile entity is at this moment used primarily for artist notes.
|
||||
|
||||
Args:
|
||||
filename (str): Filename of workfile.
|
||||
folder_id (str): Id of folder under which workfile live.
|
||||
task_name (str): Task under which was workfile created.
|
||||
files (List[str]): List of rootless filepaths related to workfile.
|
||||
filepath (str): Rootless workfile filepath.
|
||||
task_id (str): Task under which was workfile created.
|
||||
status (Optional[str]): Workfile status.
|
||||
tags (Optional[List[str]]): Workfile tags.
|
||||
attribs (Options[dic[str, Any]]): Explicitly set attributes.
|
||||
description (Optional[str]): Workfile description.
|
||||
data (Optional[Dict[str, Any]]): Additional metadata.
|
||||
entity_id (Optional[str]): Predefined id of entity. New id is created
|
||||
if not passed.
|
||||
|
|
@ -272,17 +327,31 @@ def new_workfile_info_doc(
|
|||
Dict[str, Any]: Skeleton of workfile info entity.
|
||||
"""
|
||||
|
||||
if attribs is None:
|
||||
attribs = {}
|
||||
|
||||
if "extension" not in attribs:
|
||||
attribs["extension"] = os.path.splitext(filepath)[-1]
|
||||
|
||||
if description:
|
||||
attribs["description"] = description
|
||||
|
||||
if not data:
|
||||
data = {}
|
||||
|
||||
return {
|
||||
output = {
|
||||
"id": _create_or_convert_to_id(entity_id),
|
||||
"parent": _create_or_convert_to_id(folder_id),
|
||||
"task_name": task_name,
|
||||
"filename": filename,
|
||||
"taskId": task_id,
|
||||
"path": filepath,
|
||||
"data": data,
|
||||
"files": files
|
||||
"attrib": attribs
|
||||
}
|
||||
if status:
|
||||
output["status"] = status
|
||||
|
||||
if tags:
|
||||
output["tags"] = tags
|
||||
return output
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
|
|
|
|||
416
openpype/vendor/python/common/ayon_api/server_api.py
vendored
416
openpype/vendor/python/common/ayon_api/server_api.py
vendored
|
|
@ -14,7 +14,16 @@ except ImportError:
|
|||
HTTPStatus = None
|
||||
|
||||
import requests
|
||||
from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError
|
||||
try:
|
||||
# This should be used if 'requests' have it available
|
||||
from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError
|
||||
except ImportError:
|
||||
# Older versions of 'requests' don't have custom exception for json
|
||||
# decode error
|
||||
try:
|
||||
from simplejson import JSONDecodeError as RequestsJSONDecodeError
|
||||
except ImportError:
|
||||
from json import JSONDecodeError as RequestsJSONDecodeError
|
||||
|
||||
from .constants import (
|
||||
DEFAULT_PRODUCT_TYPE_FIELDS,
|
||||
|
|
@ -27,8 +36,8 @@ from .constants import (
|
|||
REPRESENTATION_FILES_FIELDS,
|
||||
DEFAULT_WORKFILE_INFO_FIELDS,
|
||||
DEFAULT_EVENT_FIELDS,
|
||||
DEFAULT_USER_FIELDS,
|
||||
)
|
||||
from .thumbnails import ThumbnailCache
|
||||
from .graphql import GraphQlQuery, INTROSPECTION_QUERY
|
||||
from .graphql_queries import (
|
||||
project_graphql_query,
|
||||
|
|
@ -43,6 +52,7 @@ from .graphql_queries import (
|
|||
representations_parents_qraphql_query,
|
||||
workfiles_info_graphql_query,
|
||||
events_graphql_query,
|
||||
users_graphql_query,
|
||||
)
|
||||
from .exceptions import (
|
||||
FailedOperations,
|
||||
|
|
@ -61,6 +71,7 @@ from .utils import (
|
|||
failed_json_default,
|
||||
TransferProgress,
|
||||
create_dependency_package_basename,
|
||||
ThumbnailContent,
|
||||
)
|
||||
|
||||
PatternType = type(re.compile(""))
|
||||
|
|
@ -319,6 +330,8 @@ class ServerAPI(object):
|
|||
default_settings_variant (Optional[Literal["production", "staging"]]):
|
||||
Settings variant used by default if a method for settings won't
|
||||
get any (by default is 'production').
|
||||
sender (Optional[str]): Sender of requests. Used in server logs and
|
||||
propagated into events.
|
||||
ssl_verify (Union[bool, str, None]): Verify SSL certificate
|
||||
Looks for env variable value 'AYON_CA_FILE' by default. If not
|
||||
available then 'True' is used.
|
||||
|
|
@ -335,6 +348,7 @@ class ServerAPI(object):
|
|||
site_id=None,
|
||||
client_version=None,
|
||||
default_settings_variant=None,
|
||||
sender=None,
|
||||
ssl_verify=None,
|
||||
cert=None,
|
||||
create_session=True,
|
||||
|
|
@ -354,6 +368,7 @@ class ServerAPI(object):
|
|||
default_settings_variant
|
||||
or "production"
|
||||
)
|
||||
self._sender = sender
|
||||
|
||||
if ssl_verify is None:
|
||||
# Custom AYON env variable for CA file or 'True'
|
||||
|
|
@ -390,7 +405,6 @@ class ServerAPI(object):
|
|||
self._entity_type_attributes_cache = {}
|
||||
|
||||
self._as_user_stack = _AsUserStack()
|
||||
self._thumbnail_cache = ThumbnailCache(True)
|
||||
|
||||
# Create session
|
||||
if self._access_token and create_session:
|
||||
|
|
@ -559,6 +573,29 @@ class ServerAPI(object):
|
|||
set_default_settings_variant
|
||||
)
|
||||
|
||||
def get_sender(self):
|
||||
"""Sender used to send requests.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Sender name or None.
|
||||
"""
|
||||
|
||||
return self._sender
|
||||
|
||||
def set_sender(self, sender):
|
||||
"""Change sender used for requests.
|
||||
|
||||
Args:
|
||||
sender (Union[str, None]): Sender name or None.
|
||||
"""
|
||||
|
||||
if sender == self._sender:
|
||||
return
|
||||
self._sender = sender
|
||||
self._update_session_headers()
|
||||
|
||||
sender = property(get_sender, set_sender)
|
||||
|
||||
def get_default_service_username(self):
|
||||
"""Default username used for callbacks when used with service API key.
|
||||
|
||||
|
|
@ -742,6 +779,7 @@ class ServerAPI(object):
|
|||
("X-as-user", self._as_user_stack.username),
|
||||
("x-ayon-version", self._client_version),
|
||||
("x-ayon-site-id", self._site_id),
|
||||
("x-sender", self._sender),
|
||||
):
|
||||
if value is not None:
|
||||
self._session.headers[key] = value
|
||||
|
|
@ -826,10 +864,36 @@ class ServerAPI(object):
|
|||
self._access_token_is_service = None
|
||||
return None
|
||||
|
||||
def get_users(self):
|
||||
# TODO how to find out if user have permission?
|
||||
users = self.get("users")
|
||||
return users.data
|
||||
def get_users(self, usernames=None, fields=None):
|
||||
"""Get Users.
|
||||
|
||||
Args:
|
||||
usernames (Optional[Iterable[str]]): Filter by usernames.
|
||||
fields (Optional[Iterable[str]]): fields to be queried
|
||||
for users.
|
||||
|
||||
Returns:
|
||||
Generator[dict[str, Any]]: Queried users.
|
||||
"""
|
||||
|
||||
filters = {}
|
||||
if usernames is not None:
|
||||
usernames = set(usernames)
|
||||
if not usernames:
|
||||
return
|
||||
filters["userNames"] = list(usernames)
|
||||
|
||||
if not fields:
|
||||
fields = self.get_default_fields_for_type("user")
|
||||
|
||||
query = users_graphql_query(set(fields))
|
||||
for attr, filter_value in filters.items():
|
||||
query.set_variable_value(attr, filter_value)
|
||||
|
||||
for parsed_data in query.continuous_query(self):
|
||||
for user in parsed_data["users"]:
|
||||
user["roles"] = json.loads(user["roles"])
|
||||
yield user
|
||||
|
||||
def get_user(self, username=None):
|
||||
output = None
|
||||
|
|
@ -859,6 +923,9 @@ class ServerAPI(object):
|
|||
if self._client_version is not None:
|
||||
headers["x-ayon-version"] = self._client_version
|
||||
|
||||
if self._sender is not None:
|
||||
headers["x-sender"] = self._sender
|
||||
|
||||
if self._access_token:
|
||||
if self._access_token_is_service:
|
||||
headers["X-Api-Key"] = self._access_token
|
||||
|
|
@ -900,18 +967,24 @@ class ServerAPI(object):
|
|||
|
||||
self.validate_server_availability()
|
||||
|
||||
response = self.post(
|
||||
"auth/login",
|
||||
name=username,
|
||||
password=password
|
||||
)
|
||||
if response.status_code != 200:
|
||||
_detail = response.data.get("detail")
|
||||
details = ""
|
||||
if _detail:
|
||||
details = " {}".format(_detail)
|
||||
self._token_validation_started = True
|
||||
|
||||
raise AuthenticationError("Login failed {}".format(details))
|
||||
try:
|
||||
response = self.post(
|
||||
"auth/login",
|
||||
name=username,
|
||||
password=password
|
||||
)
|
||||
if response.status_code != 200:
|
||||
_detail = response.data.get("detail")
|
||||
details = ""
|
||||
if _detail:
|
||||
details = " {}".format(_detail)
|
||||
|
||||
raise AuthenticationError("Login failed {}".format(details))
|
||||
|
||||
finally:
|
||||
self._token_validation_started = False
|
||||
|
||||
self._access_token = response["token"]
|
||||
|
||||
|
|
@ -1127,7 +1200,7 @@ class ServerAPI(object):
|
|||
filters["includeLogsFilter"] = include_logs
|
||||
|
||||
if not fields:
|
||||
fields = DEFAULT_EVENT_FIELDS
|
||||
fields = self.get_default_fields_for_type("event")
|
||||
|
||||
query = events_graphql_query(set(fields))
|
||||
for attr, filter_value in filters.items():
|
||||
|
|
@ -1228,7 +1301,8 @@ class ServerAPI(object):
|
|||
target_topic,
|
||||
sender,
|
||||
description=None,
|
||||
sequential=None
|
||||
sequential=None,
|
||||
events_filter=None,
|
||||
):
|
||||
"""Enroll job based on events.
|
||||
|
||||
|
|
@ -1270,6 +1344,8 @@ class ServerAPI(object):
|
|||
in target event.
|
||||
sequential (Optional[bool]): The source topic must be processed
|
||||
in sequence.
|
||||
events_filter (Optional[ayon_server.sqlfilter.Filter]): A dict-like
|
||||
with conditions to filter the source event.
|
||||
|
||||
Returns:
|
||||
Union[None, dict[str, Any]]: None if there is no event matching
|
||||
|
|
@ -1285,6 +1361,8 @@ class ServerAPI(object):
|
|||
kwargs["sequential"] = sequential
|
||||
if description is not None:
|
||||
kwargs["description"] = description
|
||||
if events_filter is not None:
|
||||
kwargs["filter"] = events_filter
|
||||
response = self.post("enroll", **kwargs)
|
||||
if response.status_code == 204:
|
||||
return None
|
||||
|
|
@ -1612,6 +1690,19 @@ class ServerAPI(object):
|
|||
|
||||
return copy.deepcopy(attributes)
|
||||
|
||||
def get_attributes_fields_for_type(self, entity_type):
|
||||
"""Prepare attribute fields for entity type.
|
||||
|
||||
Returns:
|
||||
set[str]: Attributes fields for entity type.
|
||||
"""
|
||||
|
||||
attributes = self.get_attributes_for_type(entity_type)
|
||||
return {
|
||||
"attrib.{}".format(attr)
|
||||
for attr in attributes
|
||||
}
|
||||
|
||||
def get_default_fields_for_type(self, entity_type):
|
||||
"""Default fields for entity type.
|
||||
|
||||
|
|
@ -1624,51 +1715,46 @@ class ServerAPI(object):
|
|||
set[str]: Fields that should be queried from server.
|
||||
"""
|
||||
|
||||
attributes = self.get_attributes_for_type(entity_type)
|
||||
# Event does not have attributes
|
||||
if entity_type == "event":
|
||||
return set(DEFAULT_EVENT_FIELDS)
|
||||
|
||||
if entity_type == "project":
|
||||
return DEFAULT_PROJECT_FIELDS | {
|
||||
"attrib.{}".format(attr)
|
||||
for attr in attributes
|
||||
}
|
||||
entity_type_defaults = DEFAULT_PROJECT_FIELDS
|
||||
|
||||
if entity_type == "folder":
|
||||
return DEFAULT_FOLDER_FIELDS | {
|
||||
"attrib.{}".format(attr)
|
||||
for attr in attributes
|
||||
}
|
||||
elif entity_type == "folder":
|
||||
entity_type_defaults = DEFAULT_FOLDER_FIELDS
|
||||
|
||||
if entity_type == "task":
|
||||
return DEFAULT_TASK_FIELDS | {
|
||||
"attrib.{}".format(attr)
|
||||
for attr in attributes
|
||||
}
|
||||
elif entity_type == "task":
|
||||
entity_type_defaults = DEFAULT_TASK_FIELDS
|
||||
|
||||
if entity_type == "product":
|
||||
return DEFAULT_PRODUCT_FIELDS | {
|
||||
"attrib.{}".format(attr)
|
||||
for attr in attributes
|
||||
}
|
||||
elif entity_type == "product":
|
||||
entity_type_defaults = DEFAULT_PRODUCT_FIELDS
|
||||
|
||||
if entity_type == "version":
|
||||
return DEFAULT_VERSION_FIELDS | {
|
||||
"attrib.{}".format(attr)
|
||||
for attr in attributes
|
||||
}
|
||||
elif entity_type == "version":
|
||||
entity_type_defaults = DEFAULT_VERSION_FIELDS
|
||||
|
||||
if entity_type == "representation":
|
||||
return (
|
||||
elif entity_type == "representation":
|
||||
entity_type_defaults = (
|
||||
DEFAULT_REPRESENTATION_FIELDS
|
||||
| REPRESENTATION_FILES_FIELDS
|
||||
| {
|
||||
"attrib.{}".format(attr)
|
||||
for attr in attributes
|
||||
}
|
||||
)
|
||||
|
||||
if entity_type == "productType":
|
||||
return DEFAULT_PRODUCT_TYPE_FIELDS
|
||||
elif entity_type == "productType":
|
||||
entity_type_defaults = DEFAULT_PRODUCT_TYPE_FIELDS
|
||||
|
||||
raise ValueError("Unknown entity type \"{}\"".format(entity_type))
|
||||
elif entity_type == "workfile":
|
||||
entity_type_defaults = DEFAULT_WORKFILE_INFO_FIELDS
|
||||
|
||||
elif entity_type == "user":
|
||||
entity_type_defaults = DEFAULT_USER_FIELDS
|
||||
|
||||
else:
|
||||
raise ValueError("Unknown entity type \"{}\"".format(entity_type))
|
||||
return (
|
||||
entity_type_defaults
|
||||
| self.get_attributes_fields_for_type(entity_type)
|
||||
)
|
||||
|
||||
def get_addons_info(self, details=True):
|
||||
"""Get information about addons available on server.
|
||||
|
|
@ -2926,6 +3012,79 @@ class ServerAPI(object):
|
|||
only_values=only_values
|
||||
)
|
||||
|
||||
def get_secrets(self):
|
||||
"""Get all secrets.
|
||||
|
||||
Example output:
|
||||
[
|
||||
{
|
||||
"name": "secret_1",
|
||||
"value": "secret_value_1",
|
||||
},
|
||||
{
|
||||
"name": "secret_2",
|
||||
"value": "secret_value_2",
|
||||
}
|
||||
]
|
||||
|
||||
Returns:
|
||||
list[dict[str, str]]: List of secret entities.
|
||||
"""
|
||||
|
||||
response = self.get("secrets")
|
||||
response.raise_for_status()
|
||||
return response.data
|
||||
|
||||
def get_secret(self, secret_name):
|
||||
"""Get secret by name.
|
||||
|
||||
Example output:
|
||||
{
|
||||
"name": "secret_name",
|
||||
"value": "secret_value",
|
||||
}
|
||||
|
||||
Args:
|
||||
secret_name (str): Name of secret.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: Secret entity data.
|
||||
"""
|
||||
|
||||
response = self.get("secrets/{}".format(secret_name))
|
||||
response.raise_for_status()
|
||||
return response.data
|
||||
|
||||
def save_secret(self, secret_name, secret_value):
|
||||
"""Save secret.
|
||||
|
||||
This endpoint can create and update secret.
|
||||
|
||||
Args:
|
||||
secret_name (str): Name of secret.
|
||||
secret_value (str): Value of secret.
|
||||
"""
|
||||
|
||||
response = self.put(
|
||||
"secrets/{}".format(secret_name),
|
||||
name=secret_name,
|
||||
value=secret_value,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.data
|
||||
|
||||
|
||||
def delete_secret(self, secret_name):
|
||||
"""Delete secret by name.
|
||||
|
||||
Args:
|
||||
secret_name (str): Name of secret to delete.
|
||||
"""
|
||||
|
||||
response = self.delete("secrets/{}".format(secret_name))
|
||||
response.raise_for_status()
|
||||
return response.data
|
||||
|
||||
# Entity getters
|
||||
def get_rest_project(self, project_name):
|
||||
"""Query project by name.
|
||||
|
|
@ -3070,8 +3229,6 @@ class ServerAPI(object):
|
|||
else:
|
||||
use_rest = False
|
||||
fields = set(fields)
|
||||
if own_attributes:
|
||||
fields.add("ownAttrib")
|
||||
for field in fields:
|
||||
if field.startswith("config"):
|
||||
use_rest = True
|
||||
|
|
@ -3084,6 +3241,13 @@ class ServerAPI(object):
|
|||
yield project
|
||||
|
||||
else:
|
||||
if "attrib" in fields:
|
||||
fields.remove("attrib")
|
||||
fields |= self.get_attributes_fields_for_type("project")
|
||||
|
||||
if own_attributes:
|
||||
fields.add("ownAttrib")
|
||||
|
||||
query = projects_graphql_query(fields)
|
||||
for parsed_data in query.continuous_query(self):
|
||||
for project in parsed_data["projects"]:
|
||||
|
|
@ -3124,8 +3288,12 @@ class ServerAPI(object):
|
|||
fill_own_attribs(project)
|
||||
return project
|
||||
|
||||
if "attrib" in fields:
|
||||
fields.remove("attrib")
|
||||
fields |= self.get_attributes_fields_for_type("project")
|
||||
|
||||
if own_attributes:
|
||||
field.add("ownAttrib")
|
||||
fields.add("ownAttrib")
|
||||
query = project_graphql_query(fields)
|
||||
query.set_variable_value("projectName", project_name)
|
||||
|
||||
|
|
@ -3282,10 +3450,13 @@ class ServerAPI(object):
|
|||
|
||||
filters["parentFolderIds"] = list(parent_ids)
|
||||
|
||||
if fields:
|
||||
fields = set(fields)
|
||||
else:
|
||||
if not fields:
|
||||
fields = self.get_default_fields_for_type("folder")
|
||||
else:
|
||||
fields = set(fields)
|
||||
if "attrib" in fields:
|
||||
fields.remove("attrib")
|
||||
fields |= self.get_attributes_fields_for_type("folder")
|
||||
|
||||
use_rest = False
|
||||
if "data" in fields:
|
||||
|
|
@ -3519,8 +3690,11 @@ class ServerAPI(object):
|
|||
|
||||
if not fields:
|
||||
fields = self.get_default_fields_for_type("task")
|
||||
|
||||
fields = set(fields)
|
||||
else:
|
||||
fields = set(fields)
|
||||
if "attrib" in fields:
|
||||
fields.remove("attrib")
|
||||
fields |= self.get_attributes_fields_for_type("task")
|
||||
|
||||
use_rest = False
|
||||
if "data" in fields:
|
||||
|
|
@ -3705,6 +3879,9 @@ class ServerAPI(object):
|
|||
# Convert fields and add minimum required fields
|
||||
if fields:
|
||||
fields = set(fields) | {"id"}
|
||||
if "attrib" in fields:
|
||||
fields.remove("attrib")
|
||||
fields |= self.get_attributes_fields_for_type("folder")
|
||||
else:
|
||||
fields = self.get_default_fields_for_type("product")
|
||||
|
||||
|
|
@ -3961,7 +4138,11 @@ class ServerAPI(object):
|
|||
|
||||
if not fields:
|
||||
fields = self.get_default_fields_for_type("version")
|
||||
fields = set(fields)
|
||||
else:
|
||||
fields = set(fields)
|
||||
if "attrib" in fields:
|
||||
fields.remove("attrib")
|
||||
fields |= self.get_attributes_fields_for_type("version")
|
||||
|
||||
if active is not None:
|
||||
fields.add("active")
|
||||
|
|
@ -4419,7 +4600,11 @@ class ServerAPI(object):
|
|||
|
||||
if not fields:
|
||||
fields = self.get_default_fields_for_type("representation")
|
||||
fields = set(fields)
|
||||
else:
|
||||
fields = set(fields)
|
||||
if "attrib" in fields:
|
||||
fields.remove("attrib")
|
||||
fields |= self.get_attributes_fields_for_type("representation")
|
||||
|
||||
use_rest = False
|
||||
if "data" in fields:
|
||||
|
|
@ -4765,8 +4950,15 @@ class ServerAPI(object):
|
|||
filters["workfileIds"] = list(workfile_ids)
|
||||
|
||||
if not fields:
|
||||
fields = DEFAULT_WORKFILE_INFO_FIELDS
|
||||
fields = self.get_default_fields_for_type("workfile")
|
||||
|
||||
fields = set(fields)
|
||||
if "attrib" in fields:
|
||||
fields.remove("attrib")
|
||||
fields |= {
|
||||
"attrib.{}".format(attr)
|
||||
for attr in self.get_attributes_for_type("workfile")
|
||||
}
|
||||
if own_attributes:
|
||||
fields.add("ownAttrib")
|
||||
|
||||
|
|
@ -4843,18 +5035,61 @@ class ServerAPI(object):
|
|||
return workfile_info
|
||||
return None
|
||||
|
||||
def _prepare_thumbnail_content(self, project_name, response):
|
||||
content = None
|
||||
content_type = response.content_type
|
||||
|
||||
# It is expected the response contains thumbnail id otherwise the
|
||||
# content cannot be cached and filepath returned
|
||||
thumbnail_id = response.headers.get("X-Thumbnail-Id")
|
||||
if thumbnail_id is not None:
|
||||
content = response.content
|
||||
|
||||
return ThumbnailContent(
|
||||
project_name, thumbnail_id, content, content_type
|
||||
)
|
||||
|
||||
def get_thumbnail_by_id(self, project_name, thumbnail_id):
|
||||
"""Get thumbnail from server by id.
|
||||
|
||||
Permissions of thumbnails are related to entities so thumbnails must
|
||||
be queried per entity. So an entity type and entity type is required
|
||||
to be passed.
|
||||
|
||||
Notes:
|
||||
It is recommended to use one of prepared entity type specific
|
||||
methods 'get_folder_thumbnail', 'get_version_thumbnail' or
|
||||
'get_workfile_thumbnail'.
|
||||
We do recommend pass thumbnail id if you have access to it. Each
|
||||
entity that allows thumbnails has 'thumbnailId' field, so it
|
||||
can be queried.
|
||||
|
||||
Args:
|
||||
project_name (str): Project under which the entity is located.
|
||||
thumbnail_id (Optional[str]): DEPRECATED Use
|
||||
'get_thumbnail_by_id'.
|
||||
|
||||
Returns:
|
||||
ThumbnailContent: Thumbnail content wrapper. Does not have to be
|
||||
valid.
|
||||
"""
|
||||
|
||||
response = self.raw_get(
|
||||
"projects/{}/thumbnails/{}".format(
|
||||
project_name,
|
||||
thumbnail_id
|
||||
)
|
||||
)
|
||||
return self._prepare_thumbnail_content(project_name, response)
|
||||
|
||||
def get_thumbnail(
|
||||
self, project_name, entity_type, entity_id, thumbnail_id=None
|
||||
):
|
||||
"""Get thumbnail from server.
|
||||
|
||||
Permissions of thumbnails are related to entities so thumbnails must be
|
||||
queried per entity. So an entity type and entity type is required to
|
||||
be passed.
|
||||
|
||||
If thumbnail id is passed logic can look into locally cached thumbnails
|
||||
before calling server which can enhance loading time. If thumbnail id
|
||||
is not passed the thumbnail is always downloaded even if is available.
|
||||
Permissions of thumbnails are related to entities so thumbnails must
|
||||
be queried per entity. So an entity type and entity type is required
|
||||
to be passed.
|
||||
|
||||
Notes:
|
||||
It is recommended to use one of prepared entity type specific
|
||||
|
|
@ -4868,20 +5103,16 @@ class ServerAPI(object):
|
|||
project_name (str): Project under which the entity is located.
|
||||
entity_type (str): Entity type which passed entity id represents.
|
||||
entity_id (str): Entity id for which thumbnail should be returned.
|
||||
thumbnail_id (Optional[str]): Prepared thumbnail id from entity.
|
||||
Used only to check if thumbnail was already cached.
|
||||
thumbnail_id (Optional[str]): DEPRECATED Use
|
||||
'get_thumbnail_by_id'.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Path to downloaded thumbnail or none if entity
|
||||
does not have any (or if user does not have permissions).
|
||||
ThumbnailContent: Thumbnail content wrapper. Does not have to be
|
||||
valid.
|
||||
"""
|
||||
|
||||
# Look for thumbnail into cache and return the path if was found
|
||||
filepath = self._thumbnail_cache.get_thumbnail_filepath(
|
||||
project_name, thumbnail_id
|
||||
)
|
||||
if filepath:
|
||||
return filepath
|
||||
if thumbnail_id:
|
||||
return self.get_thumbnail_by_id(project_name, thumbnail_id)
|
||||
|
||||
if entity_type in (
|
||||
"folder",
|
||||
|
|
@ -4890,29 +5121,12 @@ class ServerAPI(object):
|
|||
):
|
||||
entity_type += "s"
|
||||
|
||||
# Receive thumbnail content from server
|
||||
result = self.raw_get("projects/{}/{}/{}/thumbnail".format(
|
||||
response = self.raw_get("projects/{}/{}/{}/thumbnail".format(
|
||||
project_name,
|
||||
entity_type,
|
||||
entity_id
|
||||
))
|
||||
|
||||
if result.content_type is None:
|
||||
return None
|
||||
|
||||
# It is expected the response contains thumbnail id otherwise the
|
||||
# content cannot be cached and filepath returned
|
||||
thumbnail_id = result.headers.get("X-Thumbnail-Id")
|
||||
if thumbnail_id is None:
|
||||
return None
|
||||
|
||||
# Cache thumbnail and return path
|
||||
return self._thumbnail_cache.store_thumbnail(
|
||||
project_name,
|
||||
thumbnail_id,
|
||||
result.content,
|
||||
result.content_type
|
||||
)
|
||||
return self._prepare_thumbnail_content(project_name, response)
|
||||
|
||||
def get_folder_thumbnail(
|
||||
self, project_name, folder_id, thumbnail_id=None
|
||||
|
|
|
|||
39
openpype/vendor/python/common/ayon_api/utils.py
vendored
39
openpype/vendor/python/common/ayon_api/utils.py
vendored
|
|
@ -27,6 +27,45 @@ RepresentationParents = collections.namedtuple(
|
|||
)
|
||||
|
||||
|
||||
class ThumbnailContent:
|
||||
"""Wrapper for thumbnail content.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
thumbnail_id (Union[str, None]): Thumbnail id.
|
||||
content_type (Union[str, None]): Content type e.g. 'image/png'.
|
||||
content (Union[bytes, None]): Thumbnail content.
|
||||
"""
|
||||
|
||||
def __init__(self, project_name, thumbnail_id, content, content_type):
|
||||
self.project_name = project_name
|
||||
self.thumbnail_id = thumbnail_id
|
||||
self.content_type = content_type
|
||||
self.content = content or b""
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Wrapper for thumbnail id.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
|
||||
return self.thumbnail_id
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
"""Content of thumbnail is valid.
|
||||
|
||||
Returns:
|
||||
bool: Content is valid and can be used.
|
||||
"""
|
||||
return (
|
||||
self.thumbnail_id is not None
|
||||
and self.content_type is not None
|
||||
)
|
||||
|
||||
|
||||
def prepare_query_string(key_values):
|
||||
"""Prepare data to query string.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
"""Package declaring Python API for Ayon server."""
|
||||
__version__ = "0.3.3"
|
||||
__version__ = "0.3.5"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.16.3-nightly.5"
|
||||
__version__ = "3.16.5-nightly.1"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "OpenPype"
|
||||
version = "3.16.2" # OpenPype
|
||||
version = "3.16.4" # OpenPype
|
||||
description = "Open VFX and Animation pipeline with support."
|
||||
authors = ["OpenPype Team <info@openpype.io>"]
|
||||
license = "MIT License"
|
||||
|
|
|
|||
|
|
@ -127,9 +127,7 @@
|
|||
"linux": []
|
||||
},
|
||||
"arguments": {
|
||||
"windows": [
|
||||
"-U MAXScript {OPENPYPE_ROOT}\\openpype\\hosts\\max\\startup\\startup.ms"
|
||||
],
|
||||
"windows": [],
|
||||
"darwin": [],
|
||||
"linux": []
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,12 +6,18 @@ from ayon_server.settings import BaseSettingsModel
|
|||
# Creator Plugins
|
||||
class CreatorModel(BaseSettingsModel):
|
||||
enabled: bool = Field(title="Enabled")
|
||||
defaults: list[str] = Field(title="Default Products")
|
||||
default_variants: list[str] = Field(
|
||||
title="Default Products",
|
||||
default_factory=list,
|
||||
)
|
||||
|
||||
|
||||
class CreateArnoldAssModel(BaseSettingsModel):
|
||||
enabled: bool = Field(title="Enabled")
|
||||
defaults: list[str] = Field(title="Default Products")
|
||||
default_variants: list[str] = Field(
|
||||
title="Default Products",
|
||||
default_factory=list,
|
||||
)
|
||||
ext: str = Field(Title="Extension")
|
||||
|
||||
|
||||
|
|
@ -54,49 +60,49 @@ class CreatePluginsModel(BaseSettingsModel):
|
|||
DEFAULT_HOUDINI_CREATE_SETTINGS = {
|
||||
"CreateArnoldAss": {
|
||||
"enabled": True,
|
||||
"default_variants": [],
|
||||
"default_variants": ["Main"],
|
||||
"ext": ".ass"
|
||||
},
|
||||
"CreateAlembicCamera": {
|
||||
"enabled": True,
|
||||
"defaults": []
|
||||
"default_variants": ["Main"]
|
||||
},
|
||||
"CreateCompositeSequence": {
|
||||
"enabled": True,
|
||||
"defaults": []
|
||||
"default_variants": ["Main"]
|
||||
},
|
||||
"CreatePointCache": {
|
||||
"enabled": True,
|
||||
"defaults": []
|
||||
"default_variants": ["Main"]
|
||||
},
|
||||
"CreateRedshiftROP": {
|
||||
"enabled": True,
|
||||
"defaults": []
|
||||
"default_variants": ["Main"]
|
||||
},
|
||||
"CreateRemotePublish": {
|
||||
"enabled": True,
|
||||
"defaults": []
|
||||
"default_variants": ["Main"]
|
||||
},
|
||||
"CreateVDBCache": {
|
||||
"enabled": True,
|
||||
"defaults": []
|
||||
"default_variants": ["Main"]
|
||||
},
|
||||
"CreateUSD": {
|
||||
"enabled": False,
|
||||
"defaults": []
|
||||
"default_variants": ["Main"]
|
||||
},
|
||||
"CreateUSDModel": {
|
||||
"enabled": False,
|
||||
"defaults": []
|
||||
"default_variants": ["Main"]
|
||||
},
|
||||
"USDCreateShadingWorkspace": {
|
||||
"enabled": False,
|
||||
"defaults": []
|
||||
"default_variants": ["Main"]
|
||||
},
|
||||
"CreateUSDRender": {
|
||||
"enabled": False,
|
||||
"defaults": []
|
||||
}
|
||||
"default_variants": ["Main"]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
17
server_addon/max/server/__init__.py
Normal file
17
server_addon/max/server/__init__.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from typing import Type
|
||||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import MaxSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class MaxAddon(BaseServerAddon):
|
||||
name = "max"
|
||||
title = "Max"
|
||||
version = __version__
|
||||
settings_model: Type[MaxSettings] = MaxSettings
|
||||
|
||||
async def get_default_settings(self):
|
||||
settings_model_cls = self.get_settings_model()
|
||||
return settings_model_cls(**DEFAULT_VALUES)
|
||||
10
server_addon/max/server/settings/__init__.py
Normal file
10
server_addon/max/server/settings/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from .main import (
|
||||
MaxSettings,
|
||||
DEFAULT_VALUES,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"MaxSettings",
|
||||
"DEFAULT_VALUES",
|
||||
)
|
||||
48
server_addon/max/server/settings/imageio.py
Normal file
48
server_addon/max/server/settings/imageio.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from pydantic import Field, validator
|
||||
from ayon_server.settings import BaseSettingsModel
|
||||
from ayon_server.settings.validators import ensure_unique_names
|
||||
|
||||
|
||||
class ImageIOConfigModel(BaseSettingsModel):
|
||||
override_global_config: bool = Field(
|
||||
False,
|
||||
title="Override global OCIO config"
|
||||
)
|
||||
filepath: list[str] = Field(
|
||||
default_factory=list,
|
||||
title="Config path"
|
||||
)
|
||||
|
||||
|
||||
class ImageIOFileRuleModel(BaseSettingsModel):
|
||||
name: str = Field("", title="Rule name")
|
||||
pattern: str = Field("", title="Regex pattern")
|
||||
colorspace: str = Field("", title="Colorspace name")
|
||||
ext: str = Field("", title="File extension")
|
||||
|
||||
|
||||
class ImageIOFileRulesModel(BaseSettingsModel):
|
||||
activate_host_rules: bool = Field(False)
|
||||
rules: list[ImageIOFileRuleModel] = Field(
|
||||
default_factory=list,
|
||||
title="Rules"
|
||||
)
|
||||
|
||||
@validator("rules")
|
||||
def validate_unique_outputs(cls, value):
|
||||
ensure_unique_names(value)
|
||||
return value
|
||||
|
||||
|
||||
class ImageIOSettings(BaseSettingsModel):
|
||||
activate_host_color_management: bool = Field(
|
||||
True, title="Enable Color Management"
|
||||
)
|
||||
ocio_config: ImageIOConfigModel = Field(
|
||||
default_factory=ImageIOConfigModel,
|
||||
title="OCIO config"
|
||||
)
|
||||
file_rules: ImageIOFileRulesModel = Field(
|
||||
default_factory=ImageIOFileRulesModel,
|
||||
title="File Rules"
|
||||
)
|
||||
60
server_addon/max/server/settings/main.py
Normal file
60
server_addon/max/server/settings/main.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
from pydantic import Field
|
||||
from ayon_server.settings import BaseSettingsModel
|
||||
from .imageio import ImageIOSettings
|
||||
from .render_settings import (
|
||||
RenderSettingsModel, DEFAULT_RENDER_SETTINGS
|
||||
)
|
||||
from .publishers import (
|
||||
PublishersModel, DEFAULT_PUBLISH_SETTINGS
|
||||
)
|
||||
|
||||
|
||||
class PRTAttributesModel(BaseSettingsModel):
|
||||
_layout = "compact"
|
||||
name: str = Field(title="Name")
|
||||
value: str = Field(title="Attribute")
|
||||
|
||||
|
||||
class PointCloudSettings(BaseSettingsModel):
|
||||
attribute: list[PRTAttributesModel] = Field(
|
||||
default_factory=list, title="Channel Attribute")
|
||||
|
||||
|
||||
class MaxSettings(BaseSettingsModel):
|
||||
imageio: ImageIOSettings = Field(
|
||||
default_factory=ImageIOSettings,
|
||||
title="Color Management (ImageIO)"
|
||||
)
|
||||
RenderSettings: RenderSettingsModel = Field(
|
||||
default_factory=RenderSettingsModel,
|
||||
title="Render Settings"
|
||||
)
|
||||
PointCloud: PointCloudSettings = Field(
|
||||
default_factory=PointCloudSettings,
|
||||
title="Point Cloud"
|
||||
)
|
||||
publish: PublishersModel = Field(
|
||||
default_factory=PublishersModel,
|
||||
title="Publish Plugins")
|
||||
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
"RenderSettings": DEFAULT_RENDER_SETTINGS,
|
||||
"PointCloud": {
|
||||
"attribute": [
|
||||
{"name": "Age", "value": "age"},
|
||||
{"name": "Radius", "value": "radius"},
|
||||
{"name": "Position", "value": "position"},
|
||||
{"name": "Rotation", "value": "rotation"},
|
||||
{"name": "Scale", "value": "scale"},
|
||||
{"name": "Velocity", "value": "velocity"},
|
||||
{"name": "Color", "value": "color"},
|
||||
{"name": "TextureCoordinate", "value": "texcoord"},
|
||||
{"name": "MaterialID", "value": "matid"},
|
||||
{"name": "custFloats", "value": "custFloats"},
|
||||
{"name": "custVecs", "value": "custVecs"},
|
||||
]
|
||||
},
|
||||
"publish": DEFAULT_PUBLISH_SETTINGS
|
||||
|
||||
}
|
||||
26
server_addon/max/server/settings/publishers.py
Normal file
26
server_addon/max/server/settings/publishers.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from pydantic import Field
|
||||
|
||||
from ayon_server.settings import BaseSettingsModel
|
||||
|
||||
|
||||
class BasicValidateModel(BaseSettingsModel):
|
||||
enabled: bool = Field(title="Enabled")
|
||||
optional: bool = Field(title="Optional")
|
||||
active: bool = Field(title="Active")
|
||||
|
||||
|
||||
class PublishersModel(BaseSettingsModel):
|
||||
ValidateFrameRange: BasicValidateModel = Field(
|
||||
default_factory=BasicValidateModel,
|
||||
title="Validate Frame Range",
|
||||
section="Validators"
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_PUBLISH_SETTINGS = {
|
||||
"ValidateFrameRange": {
|
||||
"enabled": True,
|
||||
"optional": True,
|
||||
"active": True
|
||||
}
|
||||
}
|
||||
49
server_addon/max/server/settings/render_settings.py
Normal file
49
server_addon/max/server/settings/render_settings.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
from pydantic import Field
|
||||
|
||||
from ayon_server.settings import BaseSettingsModel
|
||||
|
||||
|
||||
def aov_separators_enum():
|
||||
return [
|
||||
{"value": "dash", "label": "- (dash)"},
|
||||
{"value": "underscore", "label": "_ (underscore)"},
|
||||
{"value": "dot", "label": ". (dot)"}
|
||||
]
|
||||
|
||||
|
||||
def image_format_enum():
|
||||
"""Return enumerator for image output formats."""
|
||||
return [
|
||||
{"label": "bmp", "value": "bmp"},
|
||||
{"label": "exr", "value": "exr"},
|
||||
{"label": "tif", "value": "tif"},
|
||||
{"label": "tiff", "value": "tiff"},
|
||||
{"label": "jpg", "value": "jpg"},
|
||||
{"label": "png", "value": "png"},
|
||||
{"label": "tga", "value": "tga"},
|
||||
{"label": "dds", "value": "dds"}
|
||||
]
|
||||
|
||||
|
||||
class RenderSettingsModel(BaseSettingsModel):
|
||||
default_render_image_folder: str = Field(
|
||||
title="Default render image folder"
|
||||
)
|
||||
aov_separator: str = Field(
|
||||
"underscore",
|
||||
title="AOV Separator character",
|
||||
enum_resolver=aov_separators_enum
|
||||
)
|
||||
image_format: str = Field(
|
||||
enum_resolver=image_format_enum,
|
||||
title="Output Image Format"
|
||||
)
|
||||
multipass: bool = Field(title="multipass")
|
||||
|
||||
|
||||
DEFAULT_RENDER_SETTINGS = {
|
||||
"default_render_image_folder": "renders/3dsmax",
|
||||
"aov_separator": "underscore",
|
||||
"image_format": "png",
|
||||
"multipass": True
|
||||
}
|
||||
1
server_addon/max/server/version.py
Normal file
1
server_addon/max/server/version.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
__version__ = "0.1.0"
|
||||
|
|
@ -7,14 +7,14 @@ class CreateLookModel(BaseSettingsModel):
|
|||
enabled: bool = Field(title="Enabled")
|
||||
make_tx: bool = Field(title="Make tx files")
|
||||
rs_tex: bool = Field(title="Make Redshift texture files")
|
||||
defaults: list[str] = Field(
|
||||
default_factory=["Main"], title="Default Products"
|
||||
default_variants: list[str] = Field(
|
||||
default_factory=list, title="Default Products"
|
||||
)
|
||||
|
||||
|
||||
class BasicCreatorModel(BaseSettingsModel):
|
||||
enabled: bool = Field(title="Enabled")
|
||||
defaults: list[str] = Field(
|
||||
default_variants: list[str] = Field(
|
||||
default_factory=list,
|
||||
title="Default Products"
|
||||
)
|
||||
|
|
@ -22,20 +22,21 @@ class BasicCreatorModel(BaseSettingsModel):
|
|||
|
||||
class CreateUnrealStaticMeshModel(BaseSettingsModel):
|
||||
enabled: bool = Field(title="Enabled")
|
||||
defaults: list[str] = Field(
|
||||
default_factory=["", "_Main"],
|
||||
default_variants: list[str] = Field(
|
||||
default_factory=list,
|
||||
title="Default Products"
|
||||
)
|
||||
static_mesh_prefixes: str = Field("S", title="Static Mesh Prefix")
|
||||
collision_prefixes: list[str] = Field(
|
||||
default_factory=["UBX", "UCP", "USP", "UCX"],
|
||||
default_factory=list,
|
||||
title="Collision Prefixes"
|
||||
)
|
||||
|
||||
|
||||
class CreateUnrealSkeletalMeshModel(BaseSettingsModel):
|
||||
enabled: bool = Field(title="Enabled")
|
||||
defaults: list[str] = Field(default_factory=[], title="Default Products")
|
||||
default_variants: list[str] = Field(
|
||||
default_factory=list, title="Default Products")
|
||||
joint_hints: str = Field("jnt_org", title="Joint root hint")
|
||||
|
||||
|
||||
|
|
@ -48,7 +49,7 @@ class BasicExportMeshModel(BaseSettingsModel):
|
|||
enabled: bool = Field(title="Enabled")
|
||||
write_color_sets: bool = Field(title="Write Color Sets")
|
||||
write_face_sets: bool = Field(title="Write Face Sets")
|
||||
defaults: list[str] = Field(
|
||||
default_variants: list[str] = Field(
|
||||
default_factory=list,
|
||||
title="Default Products"
|
||||
)
|
||||
|
|
@ -61,7 +62,7 @@ class CreateAnimationModel(BaseSettingsModel):
|
|||
title="Include Parent Hierarchy")
|
||||
include_user_defined_attributes: bool = Field(
|
||||
title="Include User Defined Attributes")
|
||||
defaults: list[str] = Field(
|
||||
default_variants: list[str] = Field(
|
||||
default_factory=list,
|
||||
title="Default Products"
|
||||
)
|
||||
|
|
@ -74,8 +75,8 @@ class CreatePointCacheModel(BaseSettingsModel):
|
|||
include_user_defined_attributes: bool = Field(
|
||||
title="Include User Defined Attributes"
|
||||
)
|
||||
defaults: list[str] = Field(
|
||||
default_factory=["Main"],
|
||||
default_variants: list[str] = Field(
|
||||
default_factory=list,
|
||||
title="Default Products"
|
||||
)
|
||||
|
||||
|
|
@ -84,8 +85,8 @@ class CreateProxyAlembicModel(BaseSettingsModel):
|
|||
enabled: bool = Field(title="Enabled")
|
||||
write_color_sets: bool = Field(title="Write Color Sets")
|
||||
write_face_sets: bool = Field(title="Write Face Sets")
|
||||
defaults: list[str] = Field(
|
||||
default_factory=["Main"],
|
||||
default_variants: list[str] = Field(
|
||||
default_factory=list,
|
||||
title="Default Products"
|
||||
)
|
||||
|
||||
|
|
@ -115,7 +116,8 @@ class CreateVrayProxyModel(BaseSettingsModel):
|
|||
enabled: bool = Field(True)
|
||||
vrmesh: bool = Field(title="VrMesh")
|
||||
alembic: bool = Field(title="Alembic")
|
||||
defaults: list[str] = Field(default_factory=list, title="Default Products")
|
||||
default_variants: list[str] = Field(
|
||||
default_factory=list, title="Default Products")
|
||||
|
||||
|
||||
class CreatorsModel(BaseSettingsModel):
|
||||
|
|
@ -230,7 +232,7 @@ DEFAULT_CREATORS_SETTINGS = {
|
|||
},
|
||||
"CreateRender": {
|
||||
"enabled": True,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
|
|
@ -295,19 +297,19 @@ DEFAULT_CREATORS_SETTINGS = {
|
|||
},
|
||||
"CreateMultiverseUsd": {
|
||||
"enabled": True,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateMultiverseUsdComp": {
|
||||
"enabled": True,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateMultiverseUsdOver": {
|
||||
"enabled": True,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
|
|
@ -333,31 +335,31 @@ DEFAULT_CREATORS_SETTINGS = {
|
|||
},
|
||||
"CreateAssembly": {
|
||||
"enabled": True,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateCamera": {
|
||||
"enabled": True,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateLayout": {
|
||||
"enabled": True,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateMayaScene": {
|
||||
"enabled": True,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateRenderSetup": {
|
||||
"enabled": True,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
|
|
@ -370,7 +372,7 @@ DEFAULT_CREATORS_SETTINGS = {
|
|||
},
|
||||
"CreateRig": {
|
||||
"enabled": True,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main",
|
||||
"Sim",
|
||||
"Cloth"
|
||||
|
|
@ -378,7 +380,7 @@ DEFAULT_CREATORS_SETTINGS = {
|
|||
},
|
||||
"CreateSetDress": {
|
||||
"enabled": True,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main",
|
||||
"Anim"
|
||||
]
|
||||
|
|
@ -393,13 +395,13 @@ DEFAULT_CREATORS_SETTINGS = {
|
|||
},
|
||||
"CreateVRayScene": {
|
||||
"enabled": True,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateYetiRig": {
|
||||
"enabled": True,
|
||||
"defaults": [
|
||||
"default_variants": [
|
||||
"Main"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,11 @@ class ReferenceLoaderModel(BaseSettingsModel):
|
|||
display_handle: bool = Field(title="Display Handle On Load References")
|
||||
|
||||
|
||||
class ImportLoaderModel(BaseSettingsModel):
|
||||
namespace: str = Field(title="Namespace")
|
||||
group_name: str = Field(title="Group name")
|
||||
|
||||
|
||||
class LoadersModel(BaseSettingsModel):
|
||||
colors: ColorsSetting = Field(
|
||||
default_factory=ColorsSetting,
|
||||
|
|
@ -55,6 +60,10 @@ class LoadersModel(BaseSettingsModel):
|
|||
title="Reference Loader"
|
||||
)
|
||||
|
||||
import_loader: ImportLoaderModel = Field(
|
||||
default_factory=ImportLoaderModel,
|
||||
title="Import Loader"
|
||||
)
|
||||
|
||||
DEFAULT_LOADERS_SETTING = {
|
||||
"colors": {
|
||||
|
|
@ -111,5 +120,10 @@ DEFAULT_LOADERS_SETTING = {
|
|||
"namespace": "{folder[name]}_{product[name]}_##_",
|
||||
"group_name": "_GRP",
|
||||
"display_handle": True
|
||||
},
|
||||
"import_loader": {
|
||||
"namespace": "{folder[name]}_{product[name]}_##_",
|
||||
"group_name": "_GRP",
|
||||
"display_handle": True
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring addon version."""
|
||||
__version__ = "0.1.2"
|
||||
__version__ = "0.1.3"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue