Merge branch 'develop' into feature/igniter-improvements

This commit is contained in:
Ondrej Samohel 2021-03-03 09:55:50 +01:00
commit 1ee274ffc5
No known key found for this signature in database
GPG key ID: 8A29C663C672C2B7
68 changed files with 708 additions and 213 deletions

View file

@ -44,6 +44,9 @@ from .lib.avalon_context import (
from . import resources
from .plugin import (
PypeCreatorMixin,
Creator,
Extractor,
ValidatePipelineOrder,
@ -86,6 +89,9 @@ __all__ = [
# Resources
"resources",
# Pype creator mixin
"PypeCreatorMixin",
"Creator",
# plugin classes
"Extractor",
# ordering

View file

@ -1,4 +1,4 @@
from avalon import api
import pype.api
from avalon.vendor import Qt
from avalon import aftereffects
@ -7,7 +7,7 @@ import logging
log = logging.getLogger(__name__)
class CreateRender(api.Creator):
class CreateRender(pype.api.Creator):
"""Render folder for publish."""
name = "renderDefault"

View file

@ -6,6 +6,8 @@ from typing import Dict, List, Optional
import bpy
from avalon import api
import avalon.blender
from pype.api import PypeCreatorMixin
VALID_EXTENSIONS = [".blend", ".json"]
@ -100,6 +102,10 @@ def get_local_collection_with_name(name):
return None
class Creator(PypeCreatorMixin, avalon.blender.Creator):
pass
class AssetLoader(api.Loader):
"""A basic AssetLoader for Blender

View file

@ -3,11 +3,11 @@
import bpy
from avalon import api
from avalon.blender import Creator, lib
import pype.hosts.blender.api.plugin
from avalon.blender import lib
class CreateAction(Creator):
class CreateAction(pype.hosts.blender.api.plugin.Creator):
"""Action output for character rigs"""
name = "actionMain"

View file

@ -6,7 +6,7 @@ from avalon import api, blender
import pype.hosts.blender.api.plugin
class CreateAnimation(blender.Creator):
class CreateAnimation(pype.hosts.blender.api.plugin.Creator):
"""Animation output for character rigs"""
name = "animationMain"

View file

@ -3,11 +3,11 @@
import bpy
from avalon import api
from avalon.blender import Creator, lib
from avalon.blender import lib
import pype.hosts.blender.api.plugin
class CreateCamera(Creator):
class CreateCamera(pype.hosts.blender.api.plugin.Creator):
"""Polygonal static geometry"""
name = "cameraMain"

View file

@ -3,11 +3,11 @@
import bpy
from avalon import api
from avalon.blender import Creator, lib
from avalon.blender import lib
import pype.hosts.blender.api.plugin
class CreateLayout(Creator):
class CreateLayout(pype.hosts.blender.api.plugin.Creator):
"""Layout output for character rigs"""
name = "layoutMain"

View file

@ -3,11 +3,11 @@
import bpy
from avalon import api
from avalon.blender import Creator, lib
from avalon.blender import lib
import pype.hosts.blender.api.plugin
class CreateModel(Creator):
class CreateModel(pype.hosts.blender.api.plugin.Creator):
"""Polygonal static geometry"""
name = "modelMain"

View file

@ -3,11 +3,11 @@
import bpy
from avalon import api
from avalon.blender import Creator, lib
from avalon.blender import lib
import pype.hosts.blender.api.plugin
class CreateRig(Creator):
class CreateRig(pype.hosts.blender.api.plugin.Creator):
"""Artist-friendly rig with controls to direct motion"""
name = "rigMain"

View file

@ -3,7 +3,8 @@ import bpy
from avalon import api, blender
import pype.hosts.blender.api.plugin
class CreateSetDress(blender.Creator):
class CreateSetDress(pype.hosts.blender.api.plugin.Creator):
"""A grouped package of loaded content"""
name = "setdressMain"

View file

@ -1,10 +1,10 @@
import os
import avalon.api
import pype.api
from avalon import fusion
class CreateOpenEXRSaver(avalon.api.Creator):
class CreateOpenEXRSaver(pype.api.Creator):
name = "openexrDefault"
label = "Create OpenEXR Saver"

View file

@ -0,0 +1,6 @@
from avalon import harmony
from pype.api import PypeCreatorMixin
class Creator(PypeCreatorMixin, harmony.Creator):
pass

View file

@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
"""Create Composite node for render on farm."""
from avalon import harmony
from pype.hosts.harmony.api import plugin
class CreateFarmRender(harmony.Creator):
class CreateFarmRender(plugin.Creator):
"""Composite node for publishing renders."""
name = "renderDefault"

View file

@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
"""Create render node."""
from avalon import harmony
from pype.hosts.harmony.api import plugin
class CreateRender(harmony.Creator):
class CreateRender(plugin.Creator):
"""Composite node for publishing renders."""
name = "renderDefault"

View file

@ -1,7 +1,7 @@
from avalon import harmony
from pype.hosts.harmony.api import plugin
class CreateTemplate(harmony.Creator):
class CreateTemplate(plugin.Creator):
"""Composite node for publishing to templates."""
name = "templateDefault"

View file

@ -592,7 +592,7 @@ class ClipLoader:
return track_item
class Creator(avalon.Creator):
class Creator(pype.Creator):
"""Creator class wrapper
"""
clip_color = "Purple"

View file

@ -0,0 +1,6 @@
from avalon import houdini
from pype.api import PypeCreatorMixin
class Creator(PypeCreatorMixin, houdini.Creator):
pass

View file

@ -1,7 +1,7 @@
from avalon import houdini
from pype.hosts.houdini.api import plugin
class CreateAlembicCamera(houdini.Creator):
class CreateAlembicCamera(plugin.Creator):
"""Single baked camera from Alembic ROP"""
name = "camera"

View file

@ -1,7 +1,7 @@
from avalon import houdini
from pype.hosts.houdini.api import plugin
class CreatePointCache(houdini.Creator):
class CreatePointCache(plugin.Creator):
"""Alembic ROP to pointcache"""
name = "pointcache"

View file

@ -1,7 +1,7 @@
from avalon import houdini
from pype.hosts.houdini.api import plugin
class CreateVDBCache(houdini.Creator):
class CreateVDBCache(plugin.Creator):
"""OpenVDB from Geometry ROP"""
name = "vbdcache"

View file

@ -151,7 +151,7 @@ def on_open(_):
"""On scene open let's assume the containers have changed."""
from avalon.vendor.Qt import QtWidgets
from ...widgets import popup
from pype.widgets import popup
cmds.evalDeferred(
"from pype.hosts.maya.api import lib;"
@ -223,4 +223,4 @@ def on_task_changed(*args):
lib.show_message(
"Context was changed",
("Context was changed to {}".format(avalon.Session["AVALON_ASSET"])),
)
)

View file

@ -1,5 +1,7 @@
from avalon import api
from avalon.vendor import qargparse
import avalon.maya
from pype.api import PypeCreatorMixin
def get_reference_node_parents(ref):
@ -26,6 +28,10 @@ def get_reference_node_parents(ref):
return parents
class Creator(PypeCreatorMixin, avalon.maya.Creator):
pass
class ReferenceLoader(api.Loader):
"""A basic ReferenceLoader for Maya

View file

@ -1,8 +1,10 @@
import avalon.maya
from pype.hosts.maya.api import lib
from pype.hosts.maya.api import (
lib,
plugin
)
class CreateAnimation(avalon.maya.Creator):
class CreateAnimation(plugin.Creator):
"""Animation output for character rigs"""
name = "animationDefault"

View file

@ -1,12 +1,14 @@
from collections import OrderedDict
import avalon.maya
from pype.hosts.maya.api import lib
from pype.hosts.maya.api import (
lib,
plugin
)
from maya import cmds
class CreateAss(avalon.maya.Creator):
class CreateAss(plugin.Creator):
"""Arnold Archive"""
name = "ass"

View file

@ -1,7 +1,7 @@
import avalon.maya
from pype.hosts.maya.api import plugin
class CreateAssembly(avalon.maya.Creator):
class CreateAssembly(plugin.Creator):
"""A grouped package of loaded content"""
name = "assembly"

View file

@ -1,8 +1,10 @@
import avalon.maya
from pype.hosts.maya.api import lib
from pype.hosts.maya.api import (
lib,
plugin
)
class CreateCamera(avalon.maya.Creator):
class CreateCamera(plugin.Creator):
"""Single baked camera"""
name = "cameraMain"
@ -24,7 +26,7 @@ class CreateCamera(avalon.maya.Creator):
self.data['bakeToWorldSpace'] = True
class CreateCameraRig(avalon.maya.Creator):
class CreateCameraRig(plugin.Creator):
"""Complex hierarchy with camera."""
name = "camerarigMain"

View file

@ -1,7 +1,7 @@
import avalon.maya
from pype.hosts.maya.api import plugin
class CreateLayout(avalon.maya.Creator):
class CreateLayout(plugin.Creator):
"""A grouped package of loaded content"""
name = "layoutMain"

View file

@ -1,8 +1,10 @@
import avalon.maya
from pype.hosts.maya.api import lib
from pype.hosts.maya.api import (
lib,
plugin
)
class CreateLook(avalon.maya.Creator):
class CreateLook(plugin.Creator):
"""Shader connections defining shape look"""
name = "look"

View file

@ -1,7 +1,7 @@
import avalon.maya
from pype.hosts.maya.api import plugin
class CreateMayaAscii(avalon.maya.Creator):
class CreateMayaAscii(plugin.Creator):
"""Raw Maya Ascii file export"""
name = "mayaAscii"

View file

@ -1,7 +1,7 @@
import avalon.maya
from pype.hosts.maya.api import plugin
class CreateModel(avalon.maya.Creator):
class CreateModel(plugin.Creator):
"""Polygonal static geometry"""
name = "modelMain"

View file

@ -1,8 +1,10 @@
import avalon.maya
from pype.hosts.maya.api import lib
from pype.hosts.maya.api import (
lib,
plugin
)
class CreatePointCache(avalon.maya.Creator):
class CreatePointCache(plugin.Creator):
"""Alembic pointcache for animated data"""
name = "pointcache"

View file

@ -8,13 +8,14 @@ import requests
from maya import cmds
import maya.app.renderSetup.model.renderSetup as renderSetup
from pype.hosts.maya.api import lib
from pype.hosts.maya.api import (
lib,
plugin
)
from pype.api import get_system_settings
import avalon.maya
class CreateRender(avalon.maya.Creator):
class CreateRender(plugin.Creator):
"""Create *render* instance.
Render instances are not actually published, they hold options for

View file

@ -1,9 +1,11 @@
import avalon.maya
from pype.hosts.maya.api import lib
from pype.hosts.maya.api import (
lib,
plugin
)
from maya import cmds
class CreateRenderSetup(avalon.maya.Creator):
class CreateRenderSetup(plugin.Creator):
"""Create rendersetup template json data"""
name = "rendersetup"

View file

@ -1,9 +1,11 @@
from collections import OrderedDict
import avalon.maya
from pype.hosts.maya.api import lib
from pype.hosts.maya.api import (
lib,
plugin
)
class CreateReview(avalon.maya.Creator):
class CreateReview(plugin.Creator):
"""Single baked camera"""
name = "reviewDefault"

View file

@ -1,10 +1,12 @@
from maya import cmds
from pype.hosts.maya.api import lib
import avalon.maya
from pype.hosts.maya.api import (
lib,
plugin
)
class CreateRig(avalon.maya.Creator):
class CreateRig(plugin.Creator):
"""Artist-friendly rig with controls to direct motion"""
name = "rigDefault"

View file

@ -1,7 +1,7 @@
import avalon.maya
from pype.hosts.maya.api import plugin
class CreateSetDress(avalon.maya.Creator):
class CreateSetDress(plugin.Creator):
"""A grouped package of loaded content"""
name = "setdressMain"

View file

@ -1,7 +1,7 @@
import avalon.maya
from pype.hosts.maya.api import plugin
class CreateUnrealStaticMesh(avalon.maya.Creator):
class CreateUnrealStaticMesh(plugin.Creator):
name = "staticMeshMain"
label = "Unreal - Static Mesh"
family = "unrealStaticMesh"

View file

@ -1,7 +1,7 @@
import avalon.maya
from pype.hosts.maya.api import plugin
class CreateVrayProxy(avalon.maya.Creator):
class CreateVrayProxy(plugin.Creator):
"""Alembic pointcache for animated data"""
name = "vrayproxy"

View file

@ -8,13 +8,14 @@ import requests
from maya import cmds
import maya.app.renderSetup.model.renderSetup as renderSetup
from pype.hosts.maya.api import lib
from pype.hosts.maya.api import (
lib,
plugin
)
from pype.api import get_system_settings
import avalon.maya
class CreateVRayScene(avalon.maya.Creator):
class CreateVRayScene(plugin.Creator):
"""Create Vray Scene."""
label = "VRay Scene"

View file

@ -1,10 +1,12 @@
from collections import OrderedDict
import avalon.maya
from pype.hosts.maya.api import lib
from pype.hosts.maya.api import (
lib,
plugin
)
class CreateYetiCache(avalon.maya.Creator):
class CreateYetiCache(plugin.Creator):
"""Output for procedural plugin nodes of Yeti """
name = "yetiDefault"

View file

@ -1,10 +1,12 @@
from maya import cmds
from pype.hosts.maya.api import lib
import avalon.maya
from pype.hosts.maya.api import (
lib,
plugin
)
class CreateYetiRig(avalon.maya.Creator):
class CreateYetiRig(plugin.Creator):
"""Output for procedural plugin nodes ( Yeti / XGen / etc)"""
label = "Yeti Rig"

View file

@ -1,11 +1,13 @@
import avalon.api
import avalon.nuke
from pype.api import get_current_project_settings
from pype.api import (
get_current_project_settings,
PypeCreatorMixin
)
from .lib import check_subsetname_exists
import nuke
class PypeCreator(avalon.nuke.pipeline.Creator):
class PypeCreator(PypeCreatorMixin, avalon.nuke.pipeline.Creator):
"""Pype Nuke Creator class wrapper
"""
def __init__(self, *args, **kwargs):

View file

@ -1,9 +1,9 @@
import avalon.nuke
from avalon.nuke import lib as anlib
from pype.hosts.nuke.api import plugin
import nuke
class CreateBackdrop(avalon.nuke.Creator):
class CreateBackdrop(plugin.Creator):
"""Add Publishable Backdrop"""
name = "nukenodes"

View file

@ -1,9 +1,9 @@
import avalon.nuke
from avalon.nuke import lib as anlib
from pype.hosts.nuke.api import plugin
import nuke
class CreateCamera(avalon.nuke.Creator):
class CreateCamera(plugin.PypeCreator):
"""Add Publishable Backdrop"""
name = "camera"

View file

@ -1,9 +1,9 @@
import avalon.nuke
from avalon.nuke import lib as anlib
from pype.hosts.nuke.api import plugin
import nuke
class CreateGizmo(avalon.nuke.Creator):
class CreateGizmo(plugin.PypeCreator):
"""Add Publishable "gizmo" group
The name is symbolically gizmo as presumably

View file

@ -2,11 +2,12 @@ from collections import OrderedDict
import avalon.api
import avalon.nuke
from pype import api as pype
from pype.hosts.nuke.api import plugin
import nuke
class CrateRead(avalon.nuke.Creator):
class CrateRead(plugin.PypeCreator):
# change this to template preset
name = "ReadCopy"
label = "Create Read Copy"

View file

@ -1,9 +1,9 @@
from avalon import api
import pype.api
from avalon.vendor import Qt
from avalon import photoshop
class CreateImage(api.Creator):
class CreateImage(pype.api.Creator):
"""Image folder for publish."""
name = "imageDefault"

View file

@ -492,7 +492,7 @@ class TimelineItemLoader(api.Loader):
pass
class Creator(api.Creator):
class Creator(pype.PypeCreatorMixin, api.Creator):
"""Creator class wrapper
"""
marker_color = "Purple"

View file

@ -0,0 +1,6 @@
from pype.api import PypeCreatorMixin
from avalon.tvpaint import pipeline
class Creator(PypeCreatorMixin, pipeline.Creator):
pass

View file

@ -1,7 +1,8 @@
from avalon.tvpaint import pipeline, lib
from pype.hosts.tvpaint.api import plugin
class CreateRenderlayer(pipeline.Creator):
class CreateRenderlayer(plugin.Creator):
"""Mark layer group as one instance."""
name = "render_layer"
label = "RenderLayer"

View file

@ -1,7 +1,8 @@
from avalon.tvpaint import pipeline, lib
from pype.hosts.tvpaint.api import plugin
class CreateRenderPass(pipeline.Creator):
class CreateRenderPass(plugin.Creator):
"""Render pass is combination of one or more layers from same group.
Requirement to create Render Pass is to have already created beauty

View file

@ -1,7 +1,8 @@
from avalon.tvpaint import pipeline
from pype.hosts.tvpaint.api import plugin
class CreateReview(pipeline.Creator):
class CreateReview(plugin.Creator):
"""Review for global review of all layers."""
name = "review"
label = "Review"

View file

@ -1,7 +1,8 @@
from avalon import api
import pype.api
class Creator(api.Creator):
class Creator(pype.api.Creator):
"""This serves as skeleton for future Pype specific functionality"""
pass

View file

@ -78,9 +78,13 @@ from .applications import (
EnvironmentPrepData,
prepare_host_environments,
prepare_context_environments,
get_app_environments_for_context
get_app_environments_for_context,
compile_list_of_regexes
)
from .profiles_filtering import filter_profiles
from .plugin_tools import (
filter_pyblish_plugins,
source_hash,
@ -167,6 +171,10 @@ __all__ = [
"prepare_context_environments",
"get_app_environments_for_context",
"compile_list_of_regexes",
"filter_profiles",
"filter_pyblish_plugins",
"source_hash",
"get_unique_layer_name",

View file

@ -201,8 +201,11 @@ class PypeLogger:
# Information about mongo url
log_mongo_url = None
log_mongo_url_components = None
log_database_name = None
log_collection_name = None
# Database name in Mongo
log_database_name = "pype"
# Collection name under database in Mongo
log_collection_name = "logs"
# PYPE_DEBUG
pype_debug = 0
@ -348,25 +351,14 @@ class PypeLogger:
cls.pype_debug = int(os.getenv("PYPE_DEBUG") or "0")
# Mongo URL where logs will be stored
cls.log_mongo_url = (
os.environ.get("PYPE_LOG_MONGO_URL")
or os.environ.get("PYPE_MONGO")
)
cls.log_mongo_url = os.environ.get("PYPE_MONGO")
if not cls.log_mongo_url:
cls.use_mongo_logging = False
else:
# Decompose url
cls.log_mongo_url_components = decompose_url(cls.log_mongo_url)
# Database name in Mongo
cls.log_database_name = (
os.environ.get("PYPE_LOG_MONGO_DB") or "pype"
)
# Collection name under database in Mongo
cls.log_collection_name = (
os.environ.get("PYPE_LOG_MONGO_COL") or "logs"
)
# Mark as initialized
cls.initialized = True

View file

@ -0,0 +1,193 @@
import re
import logging
from .applications import compile_list_of_regexes
log = logging.getLogger(__name__)
def _profile_exclusion(matching_profiles, logger):
"""Find out most matching profile byt host, task and family match.
Profiles are selectively filtered. Each item in passed argument must
contain tuple of (profile, profile's score) where score is list of
booleans. Each boolean represents existence of filter for specific key.
Profiles are looped in sequence. In each sequence are profiles split into
true_list and false_list. For next sequence loop are used profiles in
true_list if there are any profiles else false_list is used.
Filtering ends when only one profile left in true_list. Or when all
existence booleans loops passed, in that case first profile from remainded
profiles is returned.
Args:
matching_profiles (list): Profiles with same scores. Each item is tuple
with (profile, profile values)
Returns:
dict: Most matching profile.
"""
logger.info(
"Search for first most matching profile in match order:"
" Host name -> Task name -> Family."
)
if not matching_profiles:
return None
if len(matching_profiles) == 1:
return matching_profiles[0][0]
scores_len = len(matching_profiles[0][1])
for idx in range(scores_len):
profiles_true = []
profiles_false = []
for profile, score in matching_profiles:
if score[idx]:
profiles_true.append((profile, score))
else:
profiles_false.append((profile, score))
if profiles_true:
matching_profiles = profiles_true
else:
matching_profiles = profiles_false
if len(matching_profiles) == 1:
return matching_profiles[0][0]
return matching_profiles[0][0]
def validate_value_by_regexes(value, in_list):
"""Validates in any regex from list match entered value.
Args:
value (str): String where regexes is checked.
in_list (list): List with regexes.
Returns:
int: Returns `0` when list is not set, is empty or contain "*".
Returns `1` when any regex match value and returns `-1`
when none of regexes match entered value.
"""
if not in_list:
return 0
if not isinstance(in_list, (list, tuple, set)):
in_list = [in_list]
if "*" in in_list:
return 0
# If value is not set and in list has specific values then resolve value
# as not matching.
if not value:
return -1
regexes = compile_list_of_regexes(in_list)
for regex in regexes:
if re.match(regex, value):
return 1
return -1
def filter_profiles(profiles_data, key_values, keys_order=None, logger=None):
""" Filter profiles by entered key -> values.
Profile if marked with score for each key/value from `key_values` with
points -1, 0 or 1.
- if profile contain the key and profile's value contain value from
`key_values` then profile gets 1 point
- if profile does not contain the key or profile's value is empty or
contain "*" then got 0 point
- if profile contain the key, profile's value is not empty and does not
contain "*" and value from `key_values` is not available in the value
then got -1 point
If profile gets -1 point at any time then is skipped and not used for
output. Profile with higher score is returned. If there are multiple
profiles with same score then first in order is used (order of profiles
matter).
Args:
profiles_data (list): Profile definitions as dictionaries.
key_values (dict): Mapping of Key <-> Value. Key is checked if is
available in profile and if Value is matching it's values.
keys_order (list, tuple): Order of keys from `key_values` which matters
only when multiple profiles have same score.
logger (logging.Logger): Optionally can be passed different logger.
Returns:
dict/None: Return most matching profile or None if none of profiles
match at least one criteria.
"""
if not profiles_data:
return None
if not logger:
logger = log
if not keys_order:
keys_order = tuple(key_values.keys())
else:
_keys_order = list(keys_order)
# Make all keys from `key_values` are passed
for key in key_values.keys():
if key not in _keys_order:
_keys_order.append(key)
keys_order = tuple(_keys_order)
matching_profiles = None
highest_profile_points = -1
# Each profile get 1 point for each matching filter. Profile with most
# points is returned. For cases when more than one profile will match
# are also stored ordered lists of matching values.
for profile in profiles_data:
profile_points = 0
profile_scores = []
for key in keys_order:
value = key_values[key]
match = validate_value_by_regexes(value, profile.get(key))
if match == -1:
profile_value = profile.get(key) or []
logger.debug(
"\"{}\" not found in {}".format(key, profile_value)
)
profile_points = -1
break
profile_points += match
profile_scores.append(bool(match))
if (
profile_points < 0
or profile_points < highest_profile_points
):
continue
if profile_points > highest_profile_points:
matching_profiles = []
highest_profile_points = profile_points
if profile_points == highest_profile_points:
matching_profiles.append((profile, profile_scores))
log_parts = " | ".join([
"{}: \"{}\"".format(*item)
for item in key_values.items()
])
if not matching_profiles:
logger.warning(
"None of profiles match your setup. {}".format(log_parts)
)
return None
if len(matching_profiles) > 1:
logger.warning(
"More than one profile match your setup. {}".format(log_parts)
)
return _profile_exclusion(matching_profiles, logger)

View file

@ -38,11 +38,6 @@ class AvalonModule(PypeModule, ITrayModule, IRestApi):
self.avalon_mongo_url = avalon_mongo_url
self.avalon_mongo_timeout = avalon_mongo_timeout
self.schema_path = os.path.join(
os.path.dirname(pype.PACKAGE_DIR),
"schema"
)
# Tray attributes
self.libraryloader = None
self.rest_api_obj = None
@ -50,23 +45,11 @@ class AvalonModule(PypeModule, ITrayModule, IRestApi):
def get_global_environments(self):
"""Avalon global environments for pype implementation."""
return {
# 100% hardcoded
"AVALON_SCHEMA": self.schema_path,
"AVALON_CONFIG": "pype",
"AVALON_LABEL": "Pype",
# Modifiable by settings
# - mongo ulr for avalon projects
"AVALON_MONGO": self.avalon_mongo_url,
# TODO thumbnails root should be multiplafrom
# - thumbnails root
"AVALON_THUMBNAIL_ROOT": self.thumbnail_root,
# - mongo timeout in ms
"AVALON_TIMEOUT": str(self.avalon_mongo_timeout),
# May be modifiable?
# - mongo database name where projects are stored
"AVALON_DB": "avalon"
}
def tray_init(self):

View file

@ -1,9 +1,9 @@
import tempfile
import os
import pyblish.api
import avalon.api
from pype.api import get_project_settings
import inspect
from pype.lib import filter_profiles
ValidatePipelineOrder = pyblish.api.ValidatorOrder + 0.05
ValidateContentsOrder = pyblish.api.ValidatorOrder + 0.1
@ -11,6 +11,89 @@ ValidateSceneOrder = pyblish.api.ValidatorOrder + 0.2
ValidateMeshOrder = pyblish.api.ValidatorOrder + 0.3
class TaskNotSetError(KeyError):
def __init__(self, msg=None):
if not msg:
msg = "Creator's subset name template requires task name."
super(TaskNotSetError, self).__init__(msg)
class PypeCreatorMixin:
"""Helper to override avalon's default class methods.
Mixin class must be used as first in inheritance order to override methods.
"""
default_tempate = "{family}{Variant}"
@classmethod
def get_subset_name(
cls, variant, task_name, asset_id, project_name, host_name=None
):
if not cls.family:
return ""
if not host_name:
host_name = os.environ["AVALON_APP"]
# Use only last part of class family value split by dot (`.`)
family = cls.family.rsplit(".", 1)[-1]
# Get settings
tools_settings = get_project_settings(project_name)["global"]["tools"]
profiles = tools_settings["creator"]["subset_name_profiles"]
filtering_criteria = {
"families": family,
"hosts": host_name,
"tasks": task_name
}
matching_profile = filter_profiles(profiles, filtering_criteria)
template = None
if matching_profile:
template = matching_profile["template"]
# Make sure template is set (matching may have empty string)
if not template:
template = cls.default_tempate
# Simple check of task name existence for template with {task} in
# - missing task should be possible only in Standalone publisher
if not task_name and "{task" in template.lower():
raise TaskNotSetError()
fill_pairs = (
("variant", variant),
("family", family),
("task", task_name)
)
fill_data = {}
for key, value in fill_pairs:
# Handle cases when value is `None` (standalone publisher)
if value is None:
continue
# Keep value as it is
fill_data[key] = value
# Both key and value are with upper case
fill_data[key.upper()] = value.upper()
# Capitalize only first char of value
# - conditions are because of possible index errors
capitalized = ""
if value:
# Upper first character
capitalized += value[0].upper()
# Append rest of string if there is any
if len(value) > 1:
capitalized += value[1:]
fill_data[key.capitalize()] = capitalized
return template.format(**fill_data)
class Creator(PypeCreatorMixin, avalon.api.Creator):
pass
class ContextPlugin(pyblish.api.ContextPlugin):
def process(cls, *args, **kwargs):
super(ContextPlugin, cls).process(cls, *args, **kwargs)

View file

@ -130,7 +130,23 @@
"rigging",
"rig"
]
}
},
"subset_name_profiles": [
{
"families": [],
"hosts": [],
"tasks": [],
"template": "{family}{Variant}"
},
{
"families": [
"render"
],
"hosts": [],
"tasks": [],
"template": "{family}{Task}{Variant}"
}
]
},
"Workfiles": {
"last_workfile_on_startup": [

View file

@ -1,13 +1,11 @@
{
"avalon": {
"AVALON_MONGO": "",
"AVALON_TIMEOUT": 1000,
"AVALON_THUMBNAIL_ROOT": {
"windows": "",
"darwin": "",
"linux": ""
},
"AVALON_DB_DATA": "{PYPE_SETUP_PATH}/../mongo_db_data"
}
},
"ftrack": {
"enabled": true,

View file

@ -12,13 +12,50 @@
"children": [
{
"type": "dict-modifiable",
"collapsible": false,
"collapsible": true,
"key": "families_smart_select",
"label": "Families smart select",
"object_type": {
"type": "list",
"object_type": "text"
}
},
{
"type": "list",
"key": "subset_name_profiles",
"label": "Subset name profiles",
"use_label_wrap": true,
"object_type": {
"type": "dict",
"children": [
{
"key": "families",
"label": "Families",
"type": "list",
"object_type": "text"
},
{
"key": "hosts",
"label": "Hosts",
"type": "list",
"object_type": "text"
},
{
"key": "tasks",
"label": "Task names",
"type": "list",
"object_type": "text"
},
{
"type": "separator"
},
{
"type": "text",
"key": "template",
"label": "Template"
}
]
}
}
]
},

View file

@ -11,12 +11,6 @@
"label": "Avalon",
"collapsible": true,
"children": [
{
"type": "text",
"key": "AVALON_MONGO",
"label": "Avalon Mongo URL",
"placeholder": "Pype Mongo is used if not filled."
},
{
"type": "number",
"key": "AVALON_TIMEOUT",
@ -29,11 +23,6 @@
"key": "AVALON_THUMBNAIL_ROOT",
"multiplatform": true,
"multipath": false
},
{
"type": "text",
"key": "AVALON_DB_DATA",
"label": "Avalon Mongo Data Location"
}
]
},

View file

@ -9,6 +9,7 @@ from Qt import QtWidgets, QtCore, QtGui
from .widgets import (
AssetWidget, FamilyWidget, ComponentsWidget, ShadowWidget
)
from .widgets.constants import HOST_NAME
from avalon import style
from pype.api import resources
from avalon.api import AvalonMongoDB
@ -73,6 +74,7 @@ class Window(QtWidgets.QDialog):
# signals
widget_assets.selection_changed.connect(self.on_asset_changed)
widget_assets.task_changed.connect(self._on_task_change)
widget_assets.project_changed.connect(self.on_project_change)
widget_family.stateChanged.connect(self.set_valid_family)
@ -150,6 +152,9 @@ class Window(QtWidgets.QDialog):
self.widget_family.change_asset(None)
self.widget_family.on_data_changed()
def _on_task_change(self):
self.widget_family.on_task_change()
def keyPressEvent(self, event):
''' Handling Ctrl+V KeyPress event
Can handle:
@ -208,6 +213,8 @@ class Window(QtWidgets.QDialog):
def main():
os.environ["AVALON_APP"] = HOST_NAME
# Allow to change icon of running process in windows taskbar
if os.name == "nt":
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(

View file

@ -0,0 +1 @@
HOST_NAME = "standalonepublisher"

View file

@ -125,6 +125,7 @@ class AssetWidget(QtWidgets.QWidget):
assets_refreshed = QtCore.Signal() # on model refresh
selection_changed = QtCore.Signal() # on view selection change
current_changed = QtCore.Signal() # on view current index change
task_changed = QtCore.Signal()
def __init__(self, dbcon, parent=None):
super(AssetWidget, self).__init__(parent=parent)
@ -190,6 +191,9 @@ class AssetWidget(QtWidgets.QWidget):
selection = view.selectionModel()
selection.selectionChanged.connect(self.selection_changed)
selection.currentChanged.connect(self.current_changed)
task_view.selectionModel().selectionChanged.connect(
self._on_task_change
)
refresh.clicked.connect(self.refresh)
self.selection_changed.connect(self._refresh_tasks)
@ -269,7 +273,18 @@ class AssetWidget(QtWidgets.QWidget):
def refresh(self):
self._refresh_model()
def _on_task_change(self):
try:
index = self.task_view.selectedIndexes()[0]
task_name = self.task_model.itemData(index)[0]
except Exception:
task_name = None
self.dbcon.Session["AVALON_TASK"] = task_name
self.task_changed.emit()
def _refresh_tasks(self):
self.dbcon.Session["AVALON_TASK"] = None
tasks = []
selected = self.get_selected_assets()
if len(selected) == 1:
@ -279,7 +294,8 @@ class AssetWidget(QtWidgets.QWidget):
if asset:
tasks = asset.get('data', {}).get('tasks', [])
self.task_model.set_tasks(tasks)
self.task_view.setVisible(len(tasks)>0)
self.task_view.setVisible(len(tasks) > 0)
self.task_changed.emit()
def get_active_asset(self):
"""Return the asset id the current asset."""

View file

@ -7,6 +7,7 @@ import string
from Qt import QtWidgets, QtCore
from . import DropDataFrame
from .constants import HOST_NAME
from avalon import io
from pype.api import execute, Logger
from pype.lib import get_pype_execute_args
@ -178,8 +179,8 @@ def set_context(project, asset, task):
io.Session["current_dir"] = os.path.normpath(os.getcwd())
os.environ["AVALON_APP"] = "standalonepublish"
io.Session["AVALON_APP"] = "standalonepublish"
os.environ["AVALON_APP"] = HOST_NAME
io.Session["AVALON_APP"] = HOST_NAME
io.uninstall()

View file

@ -1,11 +1,16 @@
import os
from collections import namedtuple
import re
from Qt import QtWidgets, QtCore
from . import HelpRole, FamilyRole, ExistsRole, PluginRole, PluginKeyRole
from . import FamilyDescriptionWidget
from pype.api import get_project_settings
from pype.api import (
get_project_settings,
Creator
)
from pype.plugin import TaskNotSetError
from avalon.tools.creator.app import SubsetAllowedSymbols
class FamilyWidget(QtWidgets.QWidget):
@ -123,6 +128,9 @@ class FamilyWidget(QtWidgets.QWidget):
}
return data
def on_task_change(self):
self.on_data_changed()
def change_asset(self, name):
if name is None:
name = self.NOT_SELECTED
@ -168,65 +176,113 @@ class FamilyWidget(QtWidgets.QWidget):
def _on_data_changed(self):
asset_name = self.asset_name
subset_name = self.input_subset.text()
user_input_text = self.input_subset.text()
item = self.list_families.currentItem()
if item is None:
return
assets = None
asset_doc = None
if asset_name != self.NOT_SELECTED:
# Get the assets from the database which match with the name
assets_db = self.dbcon.find(
filter={"type": "asset"},
projection={"name": 1}
asset_doc = self.dbcon.find_one(
{
"type": "asset",
"name": asset_name
},
{"_id": 1}
)
assets = [
asset for asset in assets_db if asset_name in asset["name"]
]
# Get plugin and family
plugin = item.data(PluginRole)
if plugin is None:
# Early exit if no asset name
if not asset_name.strip():
self._build_menu([])
item.setData(ExistsRole, False)
print("Asset name is required ..")
self.stateChanged.emit(False)
return
family = plugin.family.rsplit(".", 1)[-1]
# Get the asset from the database which match with the name
asset_doc = self.dbcon.find_one(
{"name": asset_name, "type": "asset"},
projection={"_id": 1}
)
# Get plugin
plugin = item.data(PluginRole)
if asset_doc and plugin:
project_name = self.dbcon.Session["AVALON_PROJECT"]
asset_id = asset_doc["_id"]
task_name = self.dbcon.Session["AVALON_TASK"]
# Update the result
if subset_name:
subset_name = subset_name[0].upper() + subset_name[1:]
self.input_result.setText("{}{}".format(family, subset_name))
# Calculate subset name with Creator plugin
try:
subset_name = plugin.get_subset_name(
user_input_text, task_name, asset_id, project_name
)
# Force replacement of prohibited symbols
# QUESTION should Creator care about this and here should be
# only validated with schema regex?
subset_name = re.sub(
"[^{}]+".format(SubsetAllowedSymbols),
"",
subset_name
)
self.input_result.setText(subset_name)
except TaskNotSetError:
subset_name = ""
self.input_result.setText("Select task please")
if assets:
# Get all subsets of the current asset
asset_ids = [asset["_id"] for asset in assets]
subsets = self.dbcon.find(filter={"type": "subset",
"name": {"$regex": "{}*".format(family),
"$options": "i"},
"parent": {"$in": asset_ids}}) or []
subset_docs = self.dbcon.find(
{
"type": "subset",
"parent": asset_id
},
{"name": 1}
)
existing_subset_names = set(subset_docs.distinct("name"))
# Get all subsets' their subset name, "Default", "High", "Low"
existed_subsets = [sub["name"].split(family)[-1]
for sub in subsets]
# Defaults to dropdown
defaults = []
# Check if Creator plugin has set defaults
if (
plugin.defaults
and isinstance(plugin.defaults, (list, tuple, set))
):
defaults = list(plugin.defaults)
if plugin.defaults and isinstance(plugin.defaults, list):
defaults = plugin.defaults[:] + [self.Separator]
lowered = [d.lower() for d in plugin.defaults]
for sub in [s for s in existed_subsets
if s.lower() not in lowered]:
defaults.append(sub)
else:
defaults = existed_subsets
# Replace
compare_regex = re.compile(
subset_name.replace(user_input_text, "(.+)")
)
subset_hints = set()
if user_input_text:
for _name in existing_subset_names:
_result = compare_regex.search(_name)
if _result:
subset_hints |= set(_result.groups())
subset_hints = subset_hints - set(defaults)
if subset_hints:
if defaults:
defaults.append(self.Separator)
defaults.extend(subset_hints)
self._build_menu(defaults)
item.setData(ExistsRole, True)
else:
subset_name = user_input_text
self._build_menu([])
item.setData(ExistsRole, False)
if asset_name != self.NOT_SELECTED:
# TODO add logging into standalone_publish
print("'%s' not found .." % asset_name)
if not plugin:
print("No registered families ..")
else:
print("Asset '%s' not found .." % asset_name)
self.on_version_refresh()
@ -249,30 +305,46 @@ class FamilyWidget(QtWidgets.QWidget):
subset_name = self.input_result.text()
version = 1
asset_doc = None
subset_doc = None
versions = None
if (
asset_name != self.NOT_SELECTED and
subset_name.strip() != ''
):
asset = self.dbcon.find_one({
'type': 'asset',
'name': asset_name
})
subset = self.dbcon.find_one({
'type': 'subset',
'parent': asset['_id'],
'name': subset_name
})
if subset:
versions = self.dbcon.find({
asset_doc = self.dbcon.find_one(
{
'type': 'asset',
'name': asset_name
},
{"_id": 1}
)
if asset_doc:
subset_doc = self.dbcon.find_one(
{
'type': 'subset',
'parent': asset_doc['_id'],
'name': subset_name
},
{"_id": 1}
)
if subset_doc:
versions = self.dbcon.find(
{
'type': 'version',
'parent': subset['_id']
})
if versions:
versions = sorted(
[v for v in versions],
key=lambda ver: ver['name']
)
version = int(versions[-1]['name']) + 1
'parent': subset_doc['_id']
},
{"name": 1}
).distinct("name")
if versions:
versions = sorted(
[v for v in versions],
key=lambda ver: ver['name']
)
version = int(versions[-1]['name']) + 1
self.version_spinbox.setValue(version)
@ -322,11 +394,8 @@ class FamilyWidget(QtWidgets.QWidget):
settings = get_project_settings(project_name)
sp_settings = settings.get('standalonepublisher', {})
for key, creator in sp_settings.get("create", {}).items():
if key == "__dynamic_keys_labels__":
continue
creator = namedtuple("Creator", creator.keys())(*creator.values())
for key, creator_data in sp_settings.get("create", {}).items():
creator = type(key, (Creator, ), creator_data)
label = creator.label or creator.family
item = QtWidgets.QListWidgetItem(label)

View file

@ -185,6 +185,38 @@ def run(arguments: list, env: dict = None) -> int:
return p.returncode
def set_avalon_environments():
"""Set avalon specific environments.
These are non modifiable environments for avalon workflow that must be set
before avalon module is imported because avalon works with globals set with
environment variables.
"""
from pype import PACKAGE_DIR
# Path to pype's schema
schema_path = os.path.join(
os.path.dirname(PACKAGE_DIR),
"schema"
)
# Avalon mongo URL
avalon_mongo_url = (
os.environ.get("AVALON_MONGO")
or os.environ["PYPE_MONGO"]
)
os.environ.update({
# Mongo url (use same as pype has)
"AVALON_MONGO": avalon_mongo_url,
"AVALON_SCHEMA": schema_path,
# Mongo DB name where avalon docs are stored
"AVALON_DB": "avalon",
# Name of config
"AVALON_CONFIG": "pype",
"AVALON_LABEL": "Pype"
})
def set_modules_environments():
"""Set global environments for pype modules.
@ -571,6 +603,8 @@ def boot():
from pype.lib import terminal as t
from pype.version import __version__
print(">>> loading environments ...")
# Must happen before `set_modules_environments`
set_avalon_environments()
set_modules_environments()
assert version_path, "Version path not defined."