Merge branch 'develop' into enhancement/OP-6317_Nuke-publish-existing-frames-on-farm

This commit is contained in:
Jakub Ježek 2023-08-23 16:38:52 +02:00 committed by GitHub
commit a8db933143
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
98 changed files with 4333 additions and 559 deletions

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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"])

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,10 +11,8 @@ Provides:
"""
import os
import sys
import collections
import six
import pyblish.api
import clique

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

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

View file

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

View file

@ -53,8 +53,8 @@ class ExtractBurnin(publish.Extractor):
"flame",
"houdini",
"max",
"blender"
# "resolve"
"blender",
"unreal"
]
optional = True

View file

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

View file

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

View file

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

View file

@ -12,7 +12,7 @@
},
"create": {
"RenderCreator": {
"defaults": [
"default_variants": [
"Main"
],
"mark_for_review": true

View file

@ -1,4 +1,7 @@
{
"version_start_category": {
"profiles": []
},
"imageio": {
"activate_global_color_management": false,
"ocio_config": {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,8 +13,8 @@
},
{
"type": "list",
"key": "defaults",
"label": "Default Subsets",
"key": "default_variants",
"label": "Default Variants",
"object_type": "text"
}
]

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View 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()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,2 +1,2 @@
"""Package declaring Python API for Ayon server."""
__version__ = "0.3.3"
__version__ = "0.3.5"

View file

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

View file

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

View file

@ -127,9 +127,7 @@
"linux": []
},
"arguments": {
"windows": [
"-U MAXScript {OPENPYPE_ROOT}\\openpype\\hosts\\max\\startup\\startup.ms"
],
"windows": [],
"darwin": [],
"linux": []
},

View file

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

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

View file

@ -0,0 +1,10 @@
from .main import (
MaxSettings,
DEFAULT_VALUES,
)
__all__ = (
"MaxSettings",
"DEFAULT_VALUES",
)

View 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"
)

View 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
}

View 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
}
}

View 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
}

View file

@ -0,0 +1 @@
__version__ = "0.1.0"

View file

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

View file

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

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring addon version."""
__version__ = "0.1.2"
__version__ = "0.1.3"