Merge branch 'develop' of github.com:pypeclub/OpenPype into feature/OP-2766_PS-to-new-publisher

This commit is contained in:
Petr Kalis 2022-04-05 15:13:57 +02:00
commit 9c49efa87a
36 changed files with 614 additions and 467 deletions

View file

@ -1,34 +1,50 @@
# Changelog
## [3.9.2-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD)
## [3.9.2](https://github.com/pypeclub/OpenPype/tree/3.9.2) (2022-04-04)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.1...HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.1...3.9.2)
### 📖 Documentation
- Documentation: Added mention of adding My Drive as a root [\#2999](https://github.com/pypeclub/OpenPype/pull/2999)
- Docs: Added MongoDB requirements [\#2951](https://github.com/pypeclub/OpenPype/pull/2951)
- Documentation: New publisher develop docs [\#2896](https://github.com/pypeclub/OpenPype/pull/2896)
**🆕 New features**
- Multiverse: First PR [\#2908](https://github.com/pypeclub/OpenPype/pull/2908)
- nuke: bypass baking [\#2992](https://github.com/pypeclub/OpenPype/pull/2992)
- Multiverse: Initial Support [\#2908](https://github.com/pypeclub/OpenPype/pull/2908)
**🚀 Enhancements**
- Photoshop: create image without instance [\#3001](https://github.com/pypeclub/OpenPype/pull/3001)
- TVPaint: Render scene family [\#3000](https://github.com/pypeclub/OpenPype/pull/3000)
- Nuke: ReviewDataMov Read RAW attribute [\#2985](https://github.com/pypeclub/OpenPype/pull/2985)
- General: `METADATA\_KEYS` constant as `frozenset` for optimal immutable lookup [\#2980](https://github.com/pypeclub/OpenPype/pull/2980)
- General: Tools with host filters [\#2975](https://github.com/pypeclub/OpenPype/pull/2975)
- Hero versions: Use custom templates [\#2967](https://github.com/pypeclub/OpenPype/pull/2967)
- Slack: Added configurable maximum file size of review upload to Slack [\#2945](https://github.com/pypeclub/OpenPype/pull/2945)
- NewPublisher: Prepared implementation of optional pyblish plugin [\#2943](https://github.com/pypeclub/OpenPype/pull/2943)
- TVPaint: Extractor to convert PNG into EXR [\#2942](https://github.com/pypeclub/OpenPype/pull/2942)
- Workfiles: Open published workfiles [\#2925](https://github.com/pypeclub/OpenPype/pull/2925)
- General: Default modules loaded dynamically [\#2923](https://github.com/pypeclub/OpenPype/pull/2923)
- CI: change the version bump logic [\#2919](https://github.com/pypeclub/OpenPype/pull/2919)
- Deadline: Add headless argument [\#2916](https://github.com/pypeclub/OpenPype/pull/2916)
- Nuke: Add no-audio Tag [\#2911](https://github.com/pypeclub/OpenPype/pull/2911)
- Ftrack: Fill workfile in custom attribute [\#2906](https://github.com/pypeclub/OpenPype/pull/2906)
- Nuke: improving readability [\#2903](https://github.com/pypeclub/OpenPype/pull/2903)
- Settings UI: Add simple tooltips for settings entities [\#2901](https://github.com/pypeclub/OpenPype/pull/2901)
**🐛 Bug fixes**
- Hosts: Remove path existence checks in 'add\_implementation\_envs' [\#3004](https://github.com/pypeclub/OpenPype/pull/3004)
- Fix - remove doubled dot in workfile created from template [\#2998](https://github.com/pypeclub/OpenPype/pull/2998)
- PS: fix renaming subset incorrectly in PS [\#2991](https://github.com/pypeclub/OpenPype/pull/2991)
- Fix: Disable setuptools auto discovery [\#2990](https://github.com/pypeclub/OpenPype/pull/2990)
- AEL: fix opening existing workfile if no scene opened [\#2989](https://github.com/pypeclub/OpenPype/pull/2989)
- Maya: Don't do hardlinks on windows for look publishing [\#2986](https://github.com/pypeclub/OpenPype/pull/2986)
- Settings UI: Fix version completer on linux [\#2981](https://github.com/pypeclub/OpenPype/pull/2981)
- Photoshop: Fix creation of subset names in PS review and workfile [\#2969](https://github.com/pypeclub/OpenPype/pull/2969)
- Slack: Added default for review\_upload\_limit for Slack [\#2965](https://github.com/pypeclub/OpenPype/pull/2965)
- General: OIIO conversion for ffmeg can handle sequences [\#2958](https://github.com/pypeclub/OpenPype/pull/2958)
- Settings: Conditional dictionary avoid invalid logs [\#2956](https://github.com/pypeclub/OpenPype/pull/2956)
- General: Smaller fixes and typos [\#2950](https://github.com/pypeclub/OpenPype/pull/2950)
- LogViewer: Don't refresh on initialization [\#2949](https://github.com/pypeclub/OpenPype/pull/2949)
- nuke: python3 compatibility issue with `iteritems` [\#2948](https://github.com/pypeclub/OpenPype/pull/2948)
- General: anatomy data with correct task short key [\#2947](https://github.com/pypeclub/OpenPype/pull/2947)
@ -39,20 +55,21 @@
- Settings UI: Collapsed of collapsible wrapper works as expected [\#2934](https://github.com/pypeclub/OpenPype/pull/2934)
- Maya: Do not pass `set` to maya commands \(fixes support for older maya versions\) [\#2932](https://github.com/pypeclub/OpenPype/pull/2932)
- General: Don't print log record on OSError [\#2926](https://github.com/pypeclub/OpenPype/pull/2926)
- Hiero: Fix import of 'register\_event\_callback' [\#2924](https://github.com/pypeclub/OpenPype/pull/2924)
- Ftrack: Missing Ftrack id after editorial publish [\#2905](https://github.com/pypeclub/OpenPype/pull/2905)
- AfterEffects: Fix rendering for single frame in DL [\#2875](https://github.com/pypeclub/OpenPype/pull/2875)
- Flame: centos related debugging [\#2922](https://github.com/pypeclub/OpenPype/pull/2922)
**🔀 Refactored code**
- General: Move plugins register and discover [\#2935](https://github.com/pypeclub/OpenPype/pull/2935)
- General: Move Attribute Definitions from pipeline [\#2931](https://github.com/pypeclub/OpenPype/pull/2931)
- General: Removed silo references and terminal splash [\#2927](https://github.com/pypeclub/OpenPype/pull/2927)
- General: Move pipeline constants to OpenPype [\#2918](https://github.com/pypeclub/OpenPype/pull/2918)
- General: Move formatting and workfile functions [\#2914](https://github.com/pypeclub/OpenPype/pull/2914)
- General: Move remaining plugins from avalon [\#2912](https://github.com/pypeclub/OpenPype/pull/2912)
**Merged pull requests:**
- Bump paramiko from 2.9.2 to 2.10.1 [\#2973](https://github.com/pypeclub/OpenPype/pull/2973)
- Bump minimist from 1.2.5 to 1.2.6 in /website [\#2954](https://github.com/pypeclub/OpenPype/pull/2954)
- Bump node-forge from 1.2.1 to 1.3.0 in /website [\#2953](https://github.com/pypeclub/OpenPype/pull/2953)
- Maya - added transparency into review creator [\#2952](https://github.com/pypeclub/OpenPype/pull/2952)
## [3.9.1](https://github.com/pypeclub/OpenPype/tree/3.9.1) (2022-03-18)
@ -77,7 +94,6 @@
- General: Remove forgotten use of avalon Creator [\#2885](https://github.com/pypeclub/OpenPype/pull/2885)
- General: Avoid circular import [\#2884](https://github.com/pypeclub/OpenPype/pull/2884)
- Fixes for attaching loaded containers \(\#2837\) [\#2874](https://github.com/pypeclub/OpenPype/pull/2874)
- Maya: Deformer node ids validation plugin [\#2826](https://github.com/pypeclub/OpenPype/pull/2826)
**🔀 Refactored code**
@ -88,10 +104,6 @@
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.9.0-nightly.9...3.9.0)
**Deprecated:**
- AssetCreator: Remove the tool [\#2845](https://github.com/pypeclub/OpenPype/pull/2845)
### 📖 Documentation
- Documentation: Change Photoshop & AfterEffects plugin path [\#2878](https://github.com/pypeclub/OpenPype/pull/2878)
@ -102,10 +114,6 @@
- NewPublisher: Descriptions and Icons in creator dialog [\#2867](https://github.com/pypeclub/OpenPype/pull/2867)
- NewPublisher: Changing task on publishing instance [\#2863](https://github.com/pypeclub/OpenPype/pull/2863)
- TrayPublisher: Choose project widget is more clear [\#2859](https://github.com/pypeclub/OpenPype/pull/2859)
- New: Validation exceptions [\#2841](https://github.com/pypeclub/OpenPype/pull/2841)
- Maya: add loaded containers to published instance [\#2837](https://github.com/pypeclub/OpenPype/pull/2837)
- Ftrack: Can sync fps as string [\#2836](https://github.com/pypeclub/OpenPype/pull/2836)
- General: Custom function for find executable [\#2822](https://github.com/pypeclub/OpenPype/pull/2822)
**🐛 Bug fixes**
@ -118,30 +126,11 @@
- New Publisher: Error dialog got right styles [\#2857](https://github.com/pypeclub/OpenPype/pull/2857)
- General: Fix getattr clalback on dynamic modules [\#2855](https://github.com/pypeclub/OpenPype/pull/2855)
- Nuke: slate resolution to input video resolution [\#2853](https://github.com/pypeclub/OpenPype/pull/2853)
- WebPublisher: Fix username stored in DB [\#2852](https://github.com/pypeclub/OpenPype/pull/2852)
- WebPublisher: Fix wrong number of frames for video file [\#2851](https://github.com/pypeclub/OpenPype/pull/2851)
- Nuke: Fix family test in validate\_write\_legacy to work with stillImage [\#2847](https://github.com/pypeclub/OpenPype/pull/2847)
- Nuke: fix multiple baking profile farm publishing [\#2842](https://github.com/pypeclub/OpenPype/pull/2842)
- Blender: Fixed parameters for FBX export of the camera [\#2840](https://github.com/pypeclub/OpenPype/pull/2840)
- Maya: Stop creation of reviews for Cryptomattes [\#2832](https://github.com/pypeclub/OpenPype/pull/2832)
- Deadline: Remove recreated event [\#2828](https://github.com/pypeclub/OpenPype/pull/2828)
- Deadline: Added missing events folder [\#2827](https://github.com/pypeclub/OpenPype/pull/2827)
- Settings: Missing document with OP versions may break start of OpenPype [\#2825](https://github.com/pypeclub/OpenPype/pull/2825)
- Deadline: more detailed temp file name for environment json [\#2824](https://github.com/pypeclub/OpenPype/pull/2824)
- General: Host name was formed from obsolete code [\#2821](https://github.com/pypeclub/OpenPype/pull/2821)
- Settings UI: Fix "Apply from" action [\#2820](https://github.com/pypeclub/OpenPype/pull/2820)
- Ftrack: Job killer with missing user [\#2819](https://github.com/pypeclub/OpenPype/pull/2819)
- Nuke: Use AVALON\_APP to get value for "app" key [\#2818](https://github.com/pypeclub/OpenPype/pull/2818)
**🔀 Refactored code**
- Refactor: move webserver tool to openpype [\#2876](https://github.com/pypeclub/OpenPype/pull/2876)
- General: Move create logic from avalon to OpenPype [\#2854](https://github.com/pypeclub/OpenPype/pull/2854)
- General: Add vendors from avalon [\#2848](https://github.com/pypeclub/OpenPype/pull/2848)
- General: Basic event system [\#2846](https://github.com/pypeclub/OpenPype/pull/2846)
- General: Move change context functions [\#2839](https://github.com/pypeclub/OpenPype/pull/2839)
- Tools: Don't use avalon tools code [\#2829](https://github.com/pypeclub/OpenPype/pull/2829)
- Move Unreal Implementation to OpenPype [\#2823](https://github.com/pypeclub/OpenPype/pull/2823)
## [3.8.2](https://github.com/pypeclub/OpenPype/tree/3.8.2) (2022-02-07)

View file

@ -29,12 +29,12 @@ def add_implementation_envs(env, _app):
env.get("OPENPYPE_BLENDER_USER_SCRIPTS") or ""
)
for path in openpype_blender_user_scripts.split(os.pathsep):
if path and os.path.exists(path):
if path:
previous_user_scripts.add(os.path.normpath(path))
blender_user_scripts = env.get("BLENDER_USER_SCRIPTS") or ""
for path in blender_user_scripts.split(os.pathsep):
if path and os.path.exists(path):
if path:
previous_user_scripts.add(os.path.normpath(path))
# Remove implementation path from user script paths as is set to

View file

@ -10,7 +10,7 @@ def add_implementation_envs(env, _app):
]
old_hiero_path = env.get("HIERO_PLUGIN_PATH") or ""
for path in old_hiero_path.split(os.pathsep):
if not path or not os.path.exists(path):
if not path:
continue
norm_path = os.path.normpath(path)

View file

@ -15,7 +15,7 @@ def add_implementation_envs(env, _app):
old_houdini_menu_path = env.get("HOUDINI_MENU_PATH") or ""
for path in old_houdini_path.split(os.pathsep):
if not path or not os.path.exists(path):
if not path:
continue
norm_path = os.path.normpath(path)
@ -23,7 +23,7 @@ def add_implementation_envs(env, _app):
new_houdini_path.append(norm_path)
for path in old_houdini_menu_path.split(os.pathsep):
if not path or not os.path.exists(path):
if not path:
continue
norm_path = os.path.normpath(path)

View file

@ -9,7 +9,7 @@ def add_implementation_envs(env, _app):
]
old_python_path = env.get("PYTHONPATH") or ""
for path in old_python_path.split(os.pathsep):
if not path or not os.path.exists(path):
if not path:
continue
norm_path = os.path.normpath(path)

View file

@ -10,7 +10,7 @@ def add_implementation_envs(env, _app):
]
old_nuke_path = env.get("NUKE_PATH") or ""
for path in old_nuke_path.split(os.pathsep):
if not path or not os.path.exists(path):
if not path:
continue
norm_path = os.path.normpath(path)

View file

@ -1048,17 +1048,28 @@ def add_review_knob(node):
def add_deadline_tab(node):
node.addKnob(nuke.Tab_Knob("Deadline"))
knob = nuke.Int_Knob("deadlineChunkSize", "Chunk Size")
knob.setValue(0)
node.addKnob(knob)
knob = nuke.Int_Knob("deadlinePriority", "Priority")
knob.setValue(50)
node.addKnob(knob)
knob = nuke.Int_Knob("deadlineChunkSize", "Chunk Size")
knob.setValue(0)
node.addKnob(knob)
knob = nuke.Int_Knob("deadlineConcurrentTasks", "Concurrent tasks")
# zero as default will get value from Settings during collection
# instead of being an explicit user override, see precollect_write.py
knob.setValue(0)
node.addKnob(knob)
def get_deadline_knob_names():
return ["Deadline", "deadlineChunkSize", "deadlinePriority"]
return [
"Deadline",
"deadlineChunkSize",
"deadlinePriority",
"deadlineConcurrentTasks"
]
def create_backdrop(label="", color=None, layer=0,

View file

@ -1,6 +1,7 @@
import json
from collections import OrderedDict
import nuke
import six
from avalon import io
@ -333,7 +334,7 @@ class LoadEffects(load.LoaderPlugin):
for key, value in input.items()}
elif isinstance(input, list):
return [self.byteify(element) for element in input]
elif isinstance(input, str):
elif isinstance(input, six.text_type):
return str(input)
else:
return input

View file

@ -1,6 +1,6 @@
import json
from collections import OrderedDict
import six
import nuke
from avalon import io
@ -353,7 +353,7 @@ class LoadEffectsInputProcess(load.LoaderPlugin):
for key, value in input.items()}
elif isinstance(input, list):
return [self.byteify(element) for element in input]
elif isinstance(input, str):
elif isinstance(input, six.text_type):
return str(input)
else:
return input

View file

@ -1,5 +1,5 @@
import nuke
import six
from avalon import io
from openpype.pipeline import (
@ -243,8 +243,8 @@ class LoadGizmoInputProcess(load.LoaderPlugin):
for key, value in input.items()}
elif isinstance(input, list):
return [self.byteify(element) for element in input]
elif isinstance(input, unicode):
return input.encode('utf-8')
elif isinstance(input, six.text_type):
return str(input)
else:
return input

View file

@ -0,0 +1,47 @@
import os
import pyblish.api
import openpype
from pprint import pformat
class ExtractReviewData(openpype.api.Extractor):
"""Extracts review tag into available representation
"""
order = pyblish.api.ExtractorOrder + 0.01
# order = pyblish.api.CollectorOrder + 0.499
label = "Extract Review Data"
families = ["review"]
hosts = ["nuke"]
def process(self, instance):
fpath = instance.data["path"]
ext = os.path.splitext(fpath)[-1][1:]
representations = instance.data.get("representations", [])
# review can be removed since `ProcessSubmittedJobOnFarm` will create
# reviable representation if needed
if (
"render.farm" in instance.data["families"]
and "review" in instance.data["families"]
):
instance.data["families"].remove("review")
# iterate representations and add `review` tag
for repre in representations:
if ext != repre["ext"]:
continue
if not repre.get("tags"):
repre["tags"] = []
if "review" not in repre["tags"]:
repre["tags"].append("review")
self.log.debug("Matching representation: {}".format(
pformat(repre)
))
instance.data["representations"] = representations

View file

@ -123,6 +123,7 @@ class ExtractReviewDataMov(openpype.api.Extractor):
if generated_repres:
# assign to representations
instance.data["representations"] += generated_repres
instance.data["hasReviewableRepresentations"] = True
else:
instance.data["families"].remove("review")
self.log.info((

View file

@ -128,13 +128,17 @@ class CollectNukeWrites(pyblish.api.InstancePlugin):
}
group_node = [x for x in instance if x.Class() == "Group"][0]
deadlineChunkSize = 1
dl_chunk_size = 1
if "deadlineChunkSize" in group_node.knobs():
deadlineChunkSize = group_node["deadlineChunkSize"].value()
dl_chunk_size = group_node["deadlineChunkSize"].value()
deadlinePriority = 50
dl_priority = 50
if "deadlinePriority" in group_node.knobs():
deadlinePriority = group_node["deadlinePriority"].value()
dl_priority = group_node["deadlinePriority"].value()
dl_concurrent_tasks = 0
if "deadlineConcurrentTasks" in group_node.knobs():
dl_concurrent_tasks = group_node["deadlineConcurrentTasks"].value()
instance.data.update({
"versionData": version_data,
@ -144,8 +148,9 @@ class CollectNukeWrites(pyblish.api.InstancePlugin):
"label": label,
"outputType": output_type,
"colorspace": colorspace,
"deadlineChunkSize": deadlineChunkSize,
"deadlinePriority": deadlinePriority
"deadlineChunkSize": dl_chunk_size,
"deadlinePriority": dl_priority,
"deadlineConcurrentTasks": dl_concurrent_tasks
})
if self.is_prerender(_families_test):

View file

@ -20,21 +20,30 @@ class CollectInstances(pyblish.api.ContextPlugin):
json.dumps(workfile_instances, indent=4)
))
filtered_instance_data = []
# Backwards compatibility for workfiles that already have review
# instance in metadata.
review_instance_exist = False
for instance_data in workfile_instances:
if instance_data["family"] == "review":
family = instance_data["family"]
if family == "review":
review_instance_exist = True
break
elif family not in ("renderPass", "renderLayer"):
self.log.info("Unknown family \"{}\". Skipping {}".format(
family, json.dumps(instance_data, indent=4)
))
continue
filtered_instance_data.append(instance_data)
# Fake review instance if review was not found in metadata families
if not review_instance_exist:
workfile_instances.append(
filtered_instance_data.append(
self._create_review_instance_data(context)
)
for instance_data in workfile_instances:
for instance_data in filtered_instance_data:
instance_data["fps"] = context.data["sceneFps"]
# Store workfile instance data to instance data
@ -42,8 +51,11 @@ class CollectInstances(pyblish.api.ContextPlugin):
# Global instance data modifications
# Fill families
family = instance_data["family"]
families = [family]
if family != "review":
families.append("review")
# Add `review` family for thumbnail integration
instance_data["families"] = [family, "review"]
instance_data["families"] = families
# Instance name
subset_name = instance_data["subset"]
@ -78,7 +90,7 @@ class CollectInstances(pyblish.api.ContextPlugin):
# Project name from workfile context
project_name = context.data["workfile_context"]["project"]
# Host name from environment variable
host_name = os.environ["AVALON_APP"]
host_name = context.data["hostName"]
# Use empty variant value
variant = ""
task_name = io.Session["AVALON_TASK"]
@ -106,12 +118,6 @@ class CollectInstances(pyblish.api.ContextPlugin):
instance = self.create_render_pass_instance(
context, instance_data
)
else:
raise AssertionError(
"Instance with unknown family \"{}\": {}".format(
family, instance_data
)
)
if instance is None:
continue

View file

@ -0,0 +1,110 @@
import json
import copy
import pyblish.api
from avalon import io
from openpype.lib import get_subset_name_with_asset_doc
class CollectRenderScene(pyblish.api.ContextPlugin):
"""Collect instance which renders whole scene in PNG.
Creates instance with family 'renderScene' which will have all layers
to render which will be composite into one result. The instance is not
collected from scene.
Scene will be rendered with all visible layers similar way like review is.
Instance is disabled if there are any created instances of 'renderLayer'
or 'renderPass'. That is because it is expected that this instance is
used as lazy publish of TVPaint file.
Subset name is created similar way like 'renderLayer' family. It can use
`renderPass` and `renderLayer` keys which can be set using settings and
`variant` is filled using `renderPass` value.
"""
label = "Collect Render Scene"
order = pyblish.api.CollectorOrder - 0.39
hosts = ["tvpaint"]
# Value of 'render_pass' in subset name template
render_pass = "beauty"
# Settings attributes
enabled = False
# Value of 'render_layer' and 'variant' in subset name template
render_layer = "Main"
def process(self, context):
# Check if there are created instances of renderPass and renderLayer
# - that will define if renderScene instance is enabled after
# collection
any_created_instance = False
for instance in context:
family = instance.data["family"]
if family in ("renderPass", "renderLayer"):
any_created_instance = True
break
# Global instance data modifications
# Fill families
family = "renderScene"
# Add `review` family for thumbnail integration
families = [family, "review"]
# Collect asset doc to get asset id
# - not sure if it's good idea to require asset id in
# get_subset_name?
workfile_context = context.data["workfile_context"]
asset_name = workfile_context["asset"]
asset_doc = io.find_one({
"type": "asset",
"name": asset_name
})
# Project name from workfile context
project_name = context.data["workfile_context"]["project"]
# Host name from environment variable
host_name = context.data["hostName"]
# Variant is using render pass name
variant = self.render_layer
dynamic_data = {
"render_layer": self.render_layer,
"render_pass": self.render_pass
}
task_name = workfile_context["task"]
subset_name = get_subset_name_with_asset_doc(
"render",
variant,
task_name,
asset_doc,
project_name,
host_name,
dynamic_data=dynamic_data
)
instance_data = {
"family": family,
"families": families,
"fps": context.data["sceneFps"],
"subset": subset_name,
"name": subset_name,
"label": "{} [{}-{}]".format(
subset_name,
context.data["sceneMarkIn"] + 1,
context.data["sceneMarkOut"] + 1
),
"active": not any_created_instance,
"publish": not any_created_instance,
"representations": [],
"layers": copy.deepcopy(context.data["layersData"]),
"asset": asset_name,
"task": task_name
}
instance = context.create_instance(**instance_data)
self.log.debug("Created instance: {}\n{}".format(
instance, json.dumps(instance.data, indent=4)
))

View file

@ -18,7 +18,7 @@ from openpype.hosts.tvpaint.lib import (
class ExtractSequence(pyblish.api.Extractor):
label = "Extract Sequence"
hosts = ["tvpaint"]
families = ["review", "renderPass", "renderLayer"]
families = ["review", "renderPass", "renderLayer", "renderScene"]
# Modifiable with settings
review_bg = [255, 255, 255, 255]
@ -159,7 +159,7 @@ class ExtractSequence(pyblish.api.Extractor):
# Fill tags and new families
tags = []
if family_lowered in ("review", "renderlayer"):
if family_lowered in ("review", "renderlayer", "renderscene"):
tags.append("review")
# Sequence of one frame
@ -185,7 +185,7 @@ class ExtractSequence(pyblish.api.Extractor):
instance.data["representations"].append(new_repre)
if family_lowered in ("renderpass", "renderlayer"):
if family_lowered in ("renderpass", "renderlayer", "renderscene"):
# Change family to render
instance.data["family"] = "render"

View file

@ -8,7 +8,7 @@ class ValidateLayersVisiblity(pyblish.api.InstancePlugin):
label = "Validate Layers Visibility"
order = pyblish.api.ValidatorOrder
families = ["review", "renderPass", "renderLayer"]
families = ["review", "renderPass", "renderLayer", "renderScene"]
def process(self, instance):
layer_names = set()

View file

@ -27,6 +27,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin):
# presets
priority = 50
chunk_size = 1
concurrent_tasks = 1
primary_pool = ""
secondary_pool = ""
group = ""
@ -149,11 +150,16 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin):
pass
# define chunk and priority
chunk_size = instance.data.get("deadlineChunkSize")
chunk_size = instance.data["deadlineChunkSize"]
if chunk_size == 0 and self.chunk_size:
chunk_size = self.chunk_size
priority = instance.data.get("deadlinePriority")
# define chunk and priority
concurrent_tasks = instance.data["deadlineConcurrentTasks"]
if concurrent_tasks == 0 and self.concurrent_tasks:
concurrent_tasks = self.concurrent_tasks
priority = instance.data["deadlinePriority"]
if not priority:
priority = self.priority
@ -177,6 +183,8 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin):
"Priority": priority,
"ChunkSize": chunk_size,
"ConcurrentTasks": concurrent_tasks,
"Department": self.department,
"Pool": self.primary_pool,

View file

@ -509,8 +509,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
most cases, but if not - we create representation from each of them.
Arguments:
instance (pyblish.plugin.Instance): instance for which we are
setting representations
instance (dict): instance data for which we are
setting representations
exp_files (list): list of expected files
Returns:
@ -528,6 +528,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
# preview video rendering
for app in self.aov_filter.keys():
if os.environ.get("AVALON_APP", "") == app:
# no need to add review if `hasReviewableRepresentations`
if instance.get("hasReviewableRepresentations"):
break
# iteratre all aov filters
for aov in self.aov_filter[app]:
if re.match(
aov,

View file

@ -35,6 +35,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
"image": "img",
"reference": "reference"
}
keep_first_subset_name_for_review = True
def process(self, instance):
self.log.debug("instance {}".format(instance))
@ -168,7 +169,40 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
# Change asset name of each new component for review
is_first_review_repre = True
not_first_components = []
extended_asset_name = ""
multiple_reviewable = len(review_representations) > 1
for repre in review_representations:
# Create copy of base comp item and append it
review_item = copy.deepcopy(base_component_item)
# condition for multiple reviewable representations
# expand name to better label componenst
if (
not self.keep_first_subset_name_for_review
and multiple_reviewable
):
asset_name = review_item["asset_data"]["name"]
# define new extended name
extended_asset_name = "_".join(
(asset_name, repre["name"])
)
review_item["asset_data"]["name"] = extended_asset_name
# rename asset name only if multiple reviewable repre
if is_first_review_repre:
# and rename all already created components
for _ci in component_list:
_ci["asset_data"]["name"] = extended_asset_name
# and rename all already created src components
for _sci in src_components_to_add:
_sci["asset_data"]["name"] = extended_asset_name
# rename also first thumbnail component if any
if first_thumbnail_component is not None:
first_thumbnail_component[
"asset_data"]["name"] = extended_asset_name
frame_start = repre.get("frameStartFtrack")
frame_end = repre.get("frameEndFtrack")
if frame_start is None or frame_end is None:
@ -184,8 +218,6 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
if fps is None:
fps = instance_fps
# Create copy of base comp item and append it
review_item = copy.deepcopy(base_component_item)
# Change location
review_item["component_path"] = repre["published_path"]
# Change component data
@ -200,18 +232,15 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
})
}
}
# Create copy of item before setting location or changing asset
src_components_to_add.append(copy.deepcopy(review_item))
if is_first_review_repre:
is_first_review_repre = False
else:
# Add representation name to asset name of "not first" review
asset_name = review_item["asset_data"]["name"]
review_item["asset_data"]["name"] = "_".join(
(asset_name, repre["name"])
)
not_first_components.append(review_item)
# Create copy of item before setting location
src_components_to_add.append(copy.deepcopy(review_item))
# Set location
review_item["component_location"] = ftrack_server_location
# Add item to component list
@ -249,6 +278,11 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
continue
# Create copy of base comp item and append it
other_item = copy.deepcopy(base_component_item)
# add extended name if any
if extended_asset_name:
other_item["asset_data"]["name"] = extended_asset_name
other_item["component_data"] = {
"name": repre["name"]
}

View file

@ -35,6 +35,7 @@
"use_published": true,
"priority": 50,
"chunk_size": 10,
"concurrent_tasks": 1,
"primary_pool": "",
"secondary_pool": "",
"group": "",

View file

@ -407,7 +407,8 @@
"vrayproxy": "cache",
"redshiftproxy": "cache",
"usd": "usd"
}
},
"keep_first_subset_name_for_review": true
}
}
}

View file

@ -106,6 +106,9 @@
]
}
},
"ExtractReviewData": {
"enabled": false
},
"ExtractReviewDataLut": {
"enabled": false
},

View file

@ -1,6 +1,10 @@
{
"stop_timer_on_application_exit": false,
"publish": {
"CollectRenderScene": {
"enabled": false,
"render_layer": "Main"
},
"ExtractSequence": {
"review_bg": [
255,

View file

@ -192,6 +192,9 @@
"key": "use_published",
"label": "Use Published scene"
},
{
"type": "splitter"
},
{
"type": "number",
"key": "priority",
@ -202,6 +205,14 @@
"key": "chunk_size",
"label": "Chunk Size"
},
{
"type": "number",
"key": "concurrent_tasks",
"label": "Number of concurrent tasks"
},
{
"type": "splitter"
},
{
"type": "text",
"key": "primary_pool",
@ -217,6 +228,9 @@
"key": "group",
"label": "Group"
},
{
"type": "splitter"
},
{
"type": "text",
"key": "department",

View file

@ -784,6 +784,12 @@
"object_type": {
"type": "text"
}
},
{
"type": "boolean",
"key": "keep_first_subset_name_for_review",
"label": "Make subset name as first asset name",
"default": true
}
]
}

View file

@ -16,6 +16,30 @@
"key": "publish",
"label": "Publish plugins",
"children": [
{
"type": "dict",
"collapsible": true,
"key": "CollectRenderScene",
"label": "Collect Render Scene",
"is_group": true,
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "label",
"label": "It is possible to fill <b>'render_layer'</b> or <b>'variant'</b> in subset name template with custom value.<br/>- value of <b>'render_pass'</b> is always \"beauty\"."
},
{
"type": "text",
"key": "render_layer",
"label": "Render Layer"
}
]
},
{
"type": "dict",
"collapsible": true,

View file

@ -138,6 +138,21 @@
}
]
},
{
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"key": "ExtractReviewData",
"label": "ExtractReviewData",
"is_group": true,
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
}
]
},
{
"type": "dict",
"collapsible": true,

View file

@ -1269,6 +1269,14 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
background: #21252B;
}
/* Workfiles */
#WorkfilesPublishedContextSelect {
background: rgba(0, 0, 0, 127);
}
#WorkfilesPublishedContextSelect QLabel {
font-size: 17pt;
}
/* Tray */
#TrayRestartButton {
background: {color:restart-btn-bg};

View file

@ -26,7 +26,6 @@ from .model import (
DATE_MODIFIED_ROLE,
)
from .save_as_dialog import SaveAsDialog
from .lib import TempPublishFiles
log = logging.getLogger(__name__)
@ -45,11 +44,35 @@ class FilesView(QtWidgets.QTreeView):
return super(FilesView, self).mouseDoubleClickEvent(event)
class SelectContextOverlay(QtWidgets.QFrame):
def __init__(self, parent):
super(SelectContextOverlay, self).__init__(parent)
self.setObjectName("WorkfilesPublishedContextSelect")
label_widget = QtWidgets.QLabel(
"Please choose context on the left<br/>&lt",
self
)
label_widget.setAlignment(QtCore.Qt.AlignCenter)
layout = QtWidgets.QHBoxLayout(self)
layout.addWidget(label_widget, 1, QtCore.Qt.AlignCenter)
label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
parent.installEventFilter(self)
def eventFilter(self, obj, event):
if event.type() == QtCore.QEvent.Resize:
self.resize(obj.size())
return super(SelectContextOverlay, self).eventFilter(obj, event)
class FilesWidget(QtWidgets.QWidget):
"""A widget displaying files that allows to save and open files."""
file_selected = QtCore.Signal(str)
file_opened = QtCore.Signal()
publish_file_viewed = QtCore.Signal()
workfile_created = QtCore.Signal(str)
published_visible_changed = QtCore.Signal(bool)
@ -71,9 +94,6 @@ class FilesWidget(QtWidgets.QWidget):
self._workfiles_root = None
self._workdir_path = None
self.host = api.registered_host()
temp_publish_files = TempPublishFiles()
temp_publish_files.cleanup()
self._temp_publish_files = temp_publish_files
# Whether to automatically select the latest modified
# file on a refresh of the files model.
@ -93,14 +113,14 @@ class FilesWidget(QtWidgets.QWidget):
filter_layout = QtWidgets.QHBoxLayout(filter_widget)
filter_layout.setContentsMargins(0, 0, 0, 0)
filter_layout.addWidget(published_checkbox, 0)
filter_layout.addWidget(filter_input, 1)
filter_layout.addWidget(published_checkbox, 0)
# Create the Files models
extensions = set(self.host.file_extensions())
views_widget = QtWidgets.QWidget(self)
# Workarea view
# --- Workarea view ---
workarea_files_model = WorkAreaFilesModel(extensions)
# Create proxy model for files to be able sort and filter
@ -118,13 +138,14 @@ class FilesWidget(QtWidgets.QWidget):
# Date modified delegate
workarea_time_delegate = PrettyTimeDelegate()
workarea_files_view.setItemDelegateForColumn(1, workarea_time_delegate)
workarea_files_view.setIndentation(3) # smaller indentation
# smaller indentation
workarea_files_view.setIndentation(3)
# Default to a wider first filename column it is what we mostly care
# about and the date modified is relatively small anyway.
workarea_files_view.setColumnWidth(0, 330)
# Publish files view
# --- Publish files view ---
publish_files_model = PublishFilesModel(extensions, io, self.anatomy)
publish_proxy_model = QtCore.QSortFilterProxyModel()
@ -141,12 +162,16 @@ class FilesWidget(QtWidgets.QWidget):
# Date modified delegate
publish_time_delegate = PrettyTimeDelegate()
publish_files_view.setItemDelegateForColumn(1, publish_time_delegate)
publish_files_view.setIndentation(3) # smaller indentation
# smaller indentation
publish_files_view.setIndentation(3)
# Default to a wider first filename column it is what we mostly care
# about and the date modified is relatively small anyway.
publish_files_view.setColumnWidth(0, 330)
publish_context_overlay = SelectContextOverlay(views_widget)
publish_context_overlay.setVisible(False)
views_layout = QtWidgets.QHBoxLayout(views_widget)
views_layout.setContentsMargins(0, 0, 0, 0)
views_layout.addWidget(workarea_files_view, 1)
@ -155,18 +180,43 @@ class FilesWidget(QtWidgets.QWidget):
# Home Page
# Build buttons widget for files widget
btns_widget = QtWidgets.QWidget(self)
btn_save = QtWidgets.QPushButton("Save As", btns_widget)
btn_browse = QtWidgets.QPushButton("Browse", btns_widget)
btn_open = QtWidgets.QPushButton("Open", btns_widget)
btn_view_published = QtWidgets.QPushButton("View", btns_widget)
workarea_btns_widget = QtWidgets.QWidget(btns_widget)
btn_save = QtWidgets.QPushButton("Save As", workarea_btns_widget)
btn_browse = QtWidgets.QPushButton("Browse", workarea_btns_widget)
btn_open = QtWidgets.QPushButton("Open", workarea_btns_widget)
workarea_btns_layout = QtWidgets.QHBoxLayout(workarea_btns_widget)
workarea_btns_layout.setContentsMargins(0, 0, 0, 0)
workarea_btns_layout.addWidget(btn_open, 1)
workarea_btns_layout.addWidget(btn_browse, 1)
workarea_btns_layout.addWidget(btn_save, 1)
publish_btns_widget = QtWidgets.QWidget(btns_widget)
btn_save_as_published = QtWidgets.QPushButton(
"Copy && Open", publish_btns_widget
)
btn_change_context = QtWidgets.QPushButton(
"Choose different context", publish_btns_widget
)
btn_select_context_published = QtWidgets.QPushButton(
"Copy && Open", publish_btns_widget
)
btn_cancel_published = QtWidgets.QPushButton(
"Cancel", publish_btns_widget
)
publish_btns_layout = QtWidgets.QHBoxLayout(publish_btns_widget)
publish_btns_layout.setContentsMargins(0, 0, 0, 0)
publish_btns_layout.addWidget(btn_save_as_published, 1)
publish_btns_layout.addWidget(btn_change_context, 1)
publish_btns_layout.addWidget(btn_select_context_published, 1)
publish_btns_layout.addWidget(btn_cancel_published, 1)
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
btns_layout.setContentsMargins(0, 0, 0, 0)
btns_layout.addWidget(btn_open, 1)
btns_layout.addWidget(btn_browse, 1)
btns_layout.addWidget(btn_save, 1)
btns_layout.addWidget(btn_view_published, 1)
btns_layout.addWidget(workarea_btns_widget, 1)
btns_layout.addWidget(publish_btns_widget, 1)
# Build files widgets for home page
main_layout = QtWidgets.QVBoxLayout(self)
@ -188,14 +238,22 @@ class FilesWidget(QtWidgets.QWidget):
workarea_files_view.selectionModel().selectionChanged.connect(
self.on_file_select
)
publish_files_view.doubleClickedLeft.connect(
self._on_view_published_pressed
)
btn_open.pressed.connect(self._on_workarea_open_pressed)
btn_browse.pressed.connect(self.on_browse_pressed)
btn_save.pressed.connect(self.on_save_as_pressed)
btn_view_published.pressed.connect(self._on_view_published_pressed)
btn_save.pressed.connect(self._on_save_as_pressed)
btn_save_as_published.pressed.connect(
self._on_published_save_as_pressed
)
btn_change_context.pressed.connect(
self._on_publish_change_context_pressed
)
btn_select_context_published.pressed.connect(
self._on_publish_select_context_pressed
)
btn_cancel_published.pressed.connect(
self._on_publish_cancel_pressed
)
# Store attributes
self._published_checkbox = published_checkbox
@ -211,18 +269,29 @@ class FilesWidget(QtWidgets.QWidget):
self._publish_files_model = publish_files_model
self._publish_proxy_model = publish_proxy_model
self._btns_widget = btns_widget
self._publish_context_overlay = publish_context_overlay
self._workarea_btns_widget = workarea_btns_widget
self._publish_btns_widget = publish_btns_widget
self._btn_open = btn_open
self._btn_browse = btn_browse
self._btn_save = btn_save
self._btn_view_published = btn_view_published
self._btn_save_as_published = btn_save_as_published
self._btn_change_context = btn_change_context
self._btn_select_context_published = btn_select_context_published
self._btn_cancel_published = btn_cancel_published
# Create a proxy widget for files widget
self.setFocusProxy(btn_open)
# Hide publish files widgets
publish_files_view.setVisible(False)
btn_view_published.setVisible(False)
publish_btns_widget.setVisible(False)
btn_select_context_published.setVisible(False)
btn_cancel_published.setVisible(False)
self._publish_context_select_mode = False
@property
def published_enabled(self):
@ -232,12 +301,10 @@ class FilesWidget(QtWidgets.QWidget):
published_enabled = self.published_enabled
self._workarea_files_view.setVisible(not published_enabled)
self._btn_open.setVisible(not published_enabled)
self._btn_browse.setVisible(not published_enabled)
self._btn_save.setVisible(not published_enabled)
self._workarea_btns_widget.setVisible(not published_enabled)
self._publish_files_view.setVisible(published_enabled)
self._btn_view_published.setVisible(published_enabled)
self._publish_btns_widget.setVisible(published_enabled)
self._update_filtering()
self._update_asset_task()
@ -258,6 +325,9 @@ class FilesWidget(QtWidgets.QWidget):
def set_save_enabled(self, enabled):
self._btn_save.setEnabled(enabled)
if not enabled and self._published_checkbox.isChecked():
self._published_checkbox.setChecked(False)
self._published_checkbox.setVisible(enabled)
def set_asset_task(self, asset_id, task_name, task_type):
if asset_id != self._asset_id:
@ -268,12 +338,14 @@ class FilesWidget(QtWidgets.QWidget):
self._update_asset_task()
def _update_asset_task(self):
if self.published_enabled:
if self.published_enabled and not self._publish_context_select_mode:
self._publish_files_model.set_context(
self._asset_id, self._task_name
)
has_valid_items = self._publish_files_model.has_valid_items()
self._btn_view_published.setEnabled(has_valid_items)
self._btn_save_as_published.setEnabled(has_valid_items)
self._btn_change_context.setEnabled(has_valid_items)
else:
# Define a custom session so we can query the work root
# for a "Work area" that is not our current Session.
@ -291,6 +363,13 @@ class FilesWidget(QtWidgets.QWidget):
has_valid_items = self._workarea_files_model.has_valid_items()
self._btn_browse.setEnabled(has_valid_items)
self._btn_open.setEnabled(has_valid_items)
if self._publish_context_select_mode:
self._btn_select_context_published.setEnabled(
bool(self._asset_id) and bool(self._task_name)
)
return
# Manually trigger file selection
if not has_valid_items:
self.on_file_select()
@ -400,11 +479,18 @@ class FilesWidget(QtWidgets.QWidget):
"""
session = self._get_session()
if self.published_enabled:
filepath = self._get_selected_filepath()
extensions = [os.path.splitext(filepath)[1]]
else:
extensions = self.host.file_extensions()
window = SaveAsDialog(
parent=self,
root=self._workfiles_root,
anatomy=self.anatomy,
template_key=self.template_key,
extensions=extensions,
session=session
)
window.exec_()
@ -462,10 +548,15 @@ class FilesWidget(QtWidgets.QWidget):
if work_file:
self.open_file(work_file)
def on_save_as_pressed(self):
def _on_save_as_pressed(self):
self._save_as_with_dialog()
def _save_as_with_dialog(self):
work_filename = self.get_filename()
if not work_filename:
return
return None
src_path = self._get_selected_filepath()
# Trigger before save event
emit_event(
@ -486,13 +577,20 @@ class FilesWidget(QtWidgets.QWidget):
log.debug("Initializing Work Directory: %s", self._workfiles_root)
os.makedirs(self._workfiles_root)
# Update session if context has changed
self._enter_session()
# Prepare full path to workfile and save it
filepath = os.path.join(
os.path.normpath(self._workfiles_root), work_filename
)
self.host.save_file(filepath)
# Update session if context has changed
self._enter_session()
if not self.published_enabled:
self.host.save_file(filepath)
else:
shutil.copy(src_path, filepath)
self.host.open_file(filepath)
# Create extra folders
create_workdir_extra_folders(
self._workdir_path,
@ -510,17 +608,55 @@ class FilesWidget(QtWidgets.QWidget):
self.workfile_created.emit(filepath)
# Refresh files model
self.refresh()
if self.published_enabled:
self._published_checkbox.setChecked(False)
else:
self.refresh()
return filepath
def _on_view_published_pressed(self):
filepath = self._get_selected_filepath()
if not filepath or not os.path.exists(filepath):
return
item = self._temp_publish_files.add_file(filepath)
self.host.open_file(item.filepath)
self.publish_file_viewed.emit()
# Change state back to workarea
self._published_checkbox.setChecked(False)
def _on_published_save_as_pressed(self):
self._save_as_with_dialog()
def _set_publish_context_select_mode(self, enabled):
self._publish_context_select_mode = enabled
# Show buttons related to context selection
self._publish_context_overlay.setVisible(enabled)
self._btn_cancel_published.setVisible(enabled)
self._btn_select_context_published.setVisible(enabled)
# Change enabled state based on select context
self._btn_select_context_published.setEnabled(
bool(self._asset_id) and bool(self._task_name)
)
self._btn_save_as_published.setVisible(not enabled)
self._btn_change_context.setVisible(not enabled)
# Change views and disable workarea view if enabled
self._workarea_files_view.setEnabled(not enabled)
if self.published_enabled:
self._workarea_files_view.setVisible(enabled)
self._publish_files_view.setVisible(not enabled)
else:
self._workarea_files_view.setVisible(True)
self._publish_files_view.setVisible(False)
# Disable filter widgets
self._published_checkbox.setEnabled(not enabled)
self._filter_input.setEnabled(not enabled)
def _on_publish_change_context_pressed(self):
self._set_publish_context_select_mode(True)
def _on_publish_select_context_pressed(self):
result = self._save_as_with_dialog()
if result is not None:
self._set_publish_context_select_mode(False)
self._update_asset_task()
def _on_publish_cancel_pressed(self):
self._set_publish_context_select_mode(False)
self._update_asset_task()
def on_file_select(self):
self.file_selected.emit(self._get_selected_filepath())

View file

@ -1,272 +0,0 @@
import os
import shutil
import uuid
import time
import json
import logging
import contextlib
import appdirs
class TempPublishFilesItem(object):
"""Object representing copied workfile in app temp folder.
Args:
item_id (str): Id of item used as subfolder.
data (dict): Metadata about temp files.
directory (str): Path to directory where files are copied to.
"""
def __init__(self, item_id, data, directory):
self._id = item_id
self._directory = directory
self._filepath = os.path.join(directory, data["filename"])
@property
def directory(self):
return self._directory
@property
def filepath(self):
return self._filepath
@property
def id(self):
return self._id
@property
def size(self):
if os.path.exists(self.filepath):
s = os.stat(self.filepath)
return s.st_size
return 0
class TempPublishFiles(object):
"""Directory where published workfiles are copied when opened.
Directory is located in appdirs on the machine. Folder contains file
with metadata about stored files. Each item in metadata has id, filename
and expiration time. When expiration time is higher then current time the
item is removed from metadata and it's files are deleted. Files of items
are stored in subfolder named by item's id.
Metadata file can be in theory opened and modified by multiple processes,
threads at one time. For those cases is created simple lock file which
is created before modification begins and is removed when modification
ends. Existence of the file means that it should not be modified by
any other process at the same time.
Metadata example:
```
{
"96050b4a-8974-4fca-8179-7c446c478d54": {
"created": 1647880725.555,
"expiration": 1647884325.555,
"filename": "cg_pigeon_workfileModeling_v025.ma"
},
...
}
```
## Why is this needed
Combination of more issues. Temp files are not automatically removed by
OS on windows so using tempfiles in TEMP would lead to kill disk space of
machine. There are also cases when someone wants to open multiple files
in short period of time and want to manually remove those files so keeping
track of temporary copied files in pre-defined structure is needed.
"""
minute_in_seconds = 60
hour_in_seconds = 60 * minute_in_seconds
day_in_seconds = 24 * hour_in_seconds
def __init__(self):
root_dir = appdirs.user_data_dir(
"published_workfiles_temp", "openpype"
)
if not os.path.exists(root_dir):
os.makedirs(root_dir)
metadata_path = os.path.join(root_dir, "metadata.json")
lock_path = os.path.join(root_dir, "lock.json")
self._root_dir = root_dir
self._metadata_path = metadata_path
self._lock_path = lock_path
self._log = None
@property
def log(self):
if self._log is None:
self._log = logging.getLogger(self.__class__.__name__)
return self._log
@property
def life_time(self):
"""How long will be new item kept in temp in seconds.
Returns:
int: Lifetime of temp item.
"""
return int(self.hour_in_seconds)
@property
def size(self):
"""File size of existing items."""
size = 0
for item in self.get_items():
size += item.size
return size
def add_file(self, src_path):
"""Add workfile to temp directory.
This will create new item and source path is copied to it's directory.
"""
filename = os.path.basename(src_path)
item_id = str(uuid.uuid4())
dst_dirpath = os.path.join(self._root_dir, item_id)
if not os.path.exists(dst_dirpath):
os.makedirs(dst_dirpath)
dst_path = os.path.join(dst_dirpath, filename)
shutil.copy(src_path, dst_path)
now = time.time()
item_data = {
"filename": filename,
"expiration": now + self.life_time,
"created": now
}
with self._modify_data() as data:
data[item_id] = item_data
return TempPublishFilesItem(item_id, item_data, dst_dirpath)
@contextlib.contextmanager
def _modify_data(self):
"""Create lock file when data in metadata file are modified."""
start_time = time.time()
timeout = 3
while os.path.exists(self._lock_path):
time.sleep(0.01)
if start_time > timeout:
self.log.warning((
"Waited for {} seconds to free lock file. Overriding lock."
).format(timeout))
with open(self._lock_path, "w") as stream:
json.dump({"pid": os.getpid()}, stream)
try:
data = self._get_data()
yield data
with open(self._metadata_path, "w") as stream:
json.dump(data, stream)
finally:
os.remove(self._lock_path)
def _get_data(self):
output = {}
if not os.path.exists(self._metadata_path):
return output
try:
with open(self._metadata_path, "r") as stream:
output = json.load(stream)
except Exception:
self.log.warning("Failed to read metadata file.", exc_info=True)
return output
def cleanup(self, check_expiration=True):
"""Cleanup files based on metadata.
Items that passed expiration are removed when this is called. Or all
files are removed when `check_expiration` is set to False.
Args:
check_expiration (bool): All items and files are removed when set
to True.
"""
data = self._get_data()
now = time.time()
remove_ids = set()
all_ids = set()
for item_id, item_data in data.items():
all_ids.add(item_id)
if check_expiration and now < item_data["expiration"]:
continue
remove_ids.add(item_id)
for item_id in remove_ids:
try:
self.remove_id(item_id)
except Exception:
self.log.warning(
"Failed to remove temp publish item \"{}\"".format(
item_id
),
exc_info=True
)
# Remove unknown folders/files
for filename in os.listdir(self._root_dir):
if filename in all_ids:
continue
full_path = os.path.join(self._root_dir, filename)
if full_path in (self._metadata_path, self._lock_path):
continue
try:
shutil.rmtree(full_path)
except Exception:
self.log.warning(
"Couldn't remove arbitrary path \"{}\"".format(full_path),
exc_info=True
)
def clear(self):
self.cleanup(False)
def get_items(self):
"""Receive all items from metadata file.
Returns:
list<TempPublishFilesItem>: Info about each item in metadata.
"""
output = []
data = self._get_data()
for item_id, item_data in data.items():
item_path = os.path.join(self._root_dir, item_id)
output.append(TempPublishFilesItem(item_id, item_data, item_path))
return output
def remove_id(self, item_id):
"""Remove files of item and then remove the item from metadata."""
filepath = os.path.join(self._root_dir, item_id)
if os.path.exists(filepath):
shutil.rmtree(filepath)
with self._modify_data() as data:
data.pop(item_id, None)
def file_size_to_string(file_size):
size = 0
size_ending_mapping = {
"KB": 1024 ** 1,
"MB": 1024 ** 2,
"GB": 1024 ** 3
}
ending = "B"
for _ending, _size in size_ending_mapping.items():
if file_size < _size:
break
size = file_size / _size
ending = _ending
return "{:.2f} {}".format(size, ending)

View file

@ -193,7 +193,9 @@ class SaveAsDialog(QtWidgets.QDialog):
"""
def __init__(self, parent, root, anatomy, template_key, session=None):
def __init__(
self, parent, root, anatomy, template_key, extensions, session=None
):
super(SaveAsDialog, self).__init__(parent=parent)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)
@ -201,6 +203,7 @@ class SaveAsDialog(QtWidgets.QDialog):
self.host = api.registered_host()
self.root = root
self.work_file = None
self._extensions = extensions
if not session:
# Fallback to active session
@ -257,7 +260,7 @@ class SaveAsDialog(QtWidgets.QDialog):
# Add styled delegate to use stylesheets
ext_delegate = QtWidgets.QStyledItemDelegate()
ext_combo.setItemDelegate(ext_delegate)
ext_combo.addItems(self.host.file_extensions())
ext_combo.addItems(self._extensions)
# Build inputs
inputs_layout = QtWidgets.QFormLayout(inputs_widget)
@ -336,7 +339,7 @@ class SaveAsDialog(QtWidgets.QDialog):
def get_existing_comments(self):
matcher = CommentMatcher(self.anatomy, self.template_key, self.data)
host_extensions = set(self.host.file_extensions())
host_extensions = set(self._extensions)
comments = set()
if os.path.isdir(self.root):
for fname in os.listdir(self.root):
@ -392,7 +395,7 @@ class SaveAsDialog(QtWidgets.QDialog):
return anatomy_filled[self.template_key]["file"]
def refresh(self):
extensions = self.host.file_extensions()
extensions = list(self._extensions)
extension = self.data["ext"]
if extension is None:
# Define saving file extension

View file

@ -14,7 +14,22 @@ from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget
from openpype.tools.utils.tasks_widget import TasksWidget
from .files_widget import FilesWidget
from .lib import TempPublishFiles, file_size_to_string
def file_size_to_string(file_size):
size = 0
size_ending_mapping = {
"KB": 1024 ** 1,
"MB": 1024 ** 2,
"GB": 1024 ** 3
}
ending = "B"
for _ending, _size in size_ending_mapping.items():
if file_size < _size:
break
size = file_size / _size
ending = _ending
return "{:.2f} {}".format(size, ending)
class SidePanelWidget(QtWidgets.QWidget):
@ -44,67 +59,25 @@ class SidePanelWidget(QtWidgets.QWidget):
btn_note_save, 0, alignment=QtCore.Qt.AlignRight
)
publish_temp_widget = QtWidgets.QWidget(self)
publish_temp_info_label = QtWidgets.QLabel(
self.published_workfile_message.format(
file_size_to_string(0)
),
publish_temp_widget
)
publish_temp_info_label.setWordWrap(True)
btn_clear_temp = QtWidgets.QPushButton(
"Clear temp", publish_temp_widget
)
publish_temp_layout = QtWidgets.QVBoxLayout(publish_temp_widget)
publish_temp_layout.setContentsMargins(0, 0, 0, 0)
publish_temp_layout.addWidget(publish_temp_info_label, 0)
publish_temp_layout.addWidget(
btn_clear_temp, 0, alignment=QtCore.Qt.AlignRight
)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(details_label, 0)
main_layout.addWidget(details_input, 1)
main_layout.addWidget(artist_note_widget, 1)
main_layout.addWidget(publish_temp_widget, 0)
note_input.textChanged.connect(self._on_note_change)
btn_note_save.clicked.connect(self._on_save_click)
btn_clear_temp.clicked.connect(self._on_clear_temp_click)
self._details_input = details_input
self._artist_note_widget = artist_note_widget
self._note_input = note_input
self._btn_note_save = btn_note_save
self._publish_temp_info_label = publish_temp_info_label
self._publish_temp_widget = publish_temp_widget
self._orig_note = ""
self._workfile_doc = None
publish_temp_widget.setVisible(False)
def set_published_visible(self, published_visible):
self._artist_note_widget.setVisible(not published_visible)
self._publish_temp_widget.setVisible(published_visible)
if published_visible:
self.refresh_publish_temp_sizes()
def refresh_publish_temp_sizes(self):
temp_publish_files = TempPublishFiles()
text = self.published_workfile_message.format(
file_size_to_string(temp_publish_files.size)
)
self._publish_temp_info_label.setText(text)
def _on_clear_temp_click(self):
temp_publish_files = TempPublishFiles()
temp_publish_files.clear()
self.refresh_publish_temp_sizes()
def _on_note_change(self):
text = self._note_input.toPlainText()
@ -225,9 +198,6 @@ class Window(QtWidgets.QMainWindow):
files_widget.file_selected.connect(self.on_file_select)
files_widget.workfile_created.connect(self.on_workfile_create)
files_widget.file_opened.connect(self._on_file_opened)
files_widget.publish_file_viewed.connect(
self._on_publish_file_viewed
)
files_widget.published_visible_changed.connect(
self._on_published_change
)
@ -292,9 +262,6 @@ class Window(QtWidgets.QMainWindow):
def _on_file_opened(self):
self.close()
def _on_publish_file_viewed(self):
self.side_panel.refresh_publish_temp_sizes()
def _on_published_change(self, visible):
self.side_panel.set_published_visible(visible)

View file

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

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
version = "3.9.2-nightly.3" # OpenPype
version = "3.9.2" # OpenPype
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team <info@openpype.io>"]
license = "MIT License"
@ -136,3 +136,19 @@ hash = "de63a8bf7f6c45ff59ecafeba13123f710c2cbc1783ec9e0b938e980d4f5c37f"
[openpype.thirdparty.oiio.darwin]
url = "https://distribute.openpype.io/thirdparty/oiio-2.2.0-darwin.tgz"
hash = "sha256:..."
[tool.pyright]
include = [
"igniter",
"openpype",
"repos",
"vendor"
]
exclude = [
"**/node_modules",
"**/__pycache__"
]
ignore = ["website", "docs", ".git"]
reportMissingImports = true
reportMissingTypeStubs = false

View file

@ -123,6 +123,10 @@ To get working connection to Google Drive there are some necessary steps:
- add new site back in OpenPype Settings, name as you want, provider needs to be 'gdrive'
- distribute credentials file via shared mounted disk location
:::note
If you are using regular personal GDrive for testing don't forget adding `/My Drive` as the prefix in root configuration. Business accounts and share drives don't need this.
:::
### SFTP
SFTP provider is used to connect to SFTP server. Currently authentication with `user:password` or `user:ssh key` is implemented.