diff --git a/CHANGELOG.md b/CHANGELOG.md
index f767bc71d5..88623cee3d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/openpype/hosts/blender/__init__.py b/openpype/hosts/blender/__init__.py
index 3081d3c9ba..0f27882c7e 100644
--- a/openpype/hosts/blender/__init__.py
+++ b/openpype/hosts/blender/__init__.py
@@ -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
diff --git a/openpype/hosts/hiero/__init__.py b/openpype/hosts/hiero/__init__.py
index 2d674b3fa7..d2ac82391b 100644
--- a/openpype/hosts/hiero/__init__.py
+++ b/openpype/hosts/hiero/__init__.py
@@ -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)
diff --git a/openpype/hosts/houdini/__init__.py b/openpype/hosts/houdini/__init__.py
index 8c12d13c81..a3ee38db8d 100644
--- a/openpype/hosts/houdini/__init__.py
+++ b/openpype/hosts/houdini/__init__.py
@@ -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)
diff --git a/openpype/hosts/maya/__init__.py b/openpype/hosts/maya/__init__.py
index b7d26a7818..c1c82c62e5 100644
--- a/openpype/hosts/maya/__init__.py
+++ b/openpype/hosts/maya/__init__.py
@@ -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)
diff --git a/openpype/hosts/nuke/__init__.py b/openpype/hosts/nuke/__init__.py
index 60b37ce1dd..134a6621c4 100644
--- a/openpype/hosts/nuke/__init__.py
+++ b/openpype/hosts/nuke/__init__.py
@@ -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)
diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py
index c22488f728..e05c6aecbd 100644
--- a/openpype/hosts/nuke/api/lib.py
+++ b/openpype/hosts/nuke/api/lib.py
@@ -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,
diff --git a/openpype/hosts/nuke/plugins/load/load_effects.py b/openpype/hosts/nuke/plugins/load/load_effects.py
index 1ed32996e1..56c5acbb0a 100644
--- a/openpype/hosts/nuke/plugins/load/load_effects.py
+++ b/openpype/hosts/nuke/plugins/load/load_effects.py
@@ -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
diff --git a/openpype/hosts/nuke/plugins/load/load_effects_ip.py b/openpype/hosts/nuke/plugins/load/load_effects_ip.py
index 383776111f..0bc5f5a514 100644
--- a/openpype/hosts/nuke/plugins/load/load_effects_ip.py
+++ b/openpype/hosts/nuke/plugins/load/load_effects_ip.py
@@ -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
diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py
index df52a22364..46134afcf0 100644
--- a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py
+++ b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py
@@ -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
diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data.py b/openpype/hosts/nuke/plugins/publish/extract_review_data.py
new file mode 100644
index 0000000000..38a8140cff
--- /dev/null
+++ b/openpype/hosts/nuke/plugins/publish/extract_review_data.py
@@ -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
diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py
index 31a8ff18ee..22b371d8e9 100644
--- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py
+++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py
@@ -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((
diff --git a/openpype/hosts/nuke/plugins/publish/precollect_writes.py b/openpype/hosts/nuke/plugins/publish/precollect_writes.py
index 85e98db7ed..4826b2788f 100644
--- a/openpype/hosts/nuke/plugins/publish/precollect_writes.py
+++ b/openpype/hosts/nuke/plugins/publish/precollect_writes.py
@@ -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):
diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py
index 9cbfb61550..5e8d13592c 100644
--- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py
+++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py
@@ -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
diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py
new file mode 100644
index 0000000000..0af9a9a400
--- /dev/null
+++ b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py
@@ -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)
+ ))
diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py
index 139dabadee..d4fd1dff4b 100644
--- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py
+++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py
@@ -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"
diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py b/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py
index 7ea0587b8f..d3a04cc69f 100644
--- a/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py
+++ b/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py
@@ -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()
diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py
index d6bd11620d..9b5800c33f 100644
--- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py
@@ -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,
diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py
index fad4d14ea0..5755619292 100644
--- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py
+++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py
@@ -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,
diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py
index cff7cd32cb..c11d5b9c68 100644
--- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py
+++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py
@@ -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"]
}
diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json
index 4ff1d3b54d..31211231c6 100644
--- a/openpype/settings/defaults/project_settings/deadline.json
+++ b/openpype/settings/defaults/project_settings/deadline.json
@@ -35,6 +35,7 @@
"use_published": true,
"priority": 50,
"chunk_size": 10,
+ "concurrent_tasks": 1,
"primary_pool": "",
"secondary_pool": "",
"group": "",
diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json
index d3de28e8db..67da9deabb 100644
--- a/openpype/settings/defaults/project_settings/ftrack.json
+++ b/openpype/settings/defaults/project_settings/ftrack.json
@@ -407,7 +407,8 @@
"vrayproxy": "cache",
"redshiftproxy": "cache",
"usd": "usd"
- }
+ },
+ "keep_first_subset_name_for_review": true
}
}
}
\ No newline at end of file
diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json
index 2fb6a372e4..44d7f2d9d0 100644
--- a/openpype/settings/defaults/project_settings/nuke.json
+++ b/openpype/settings/defaults/project_settings/nuke.json
@@ -106,6 +106,9 @@
]
}
},
+ "ExtractReviewData": {
+ "enabled": false
+ },
"ExtractReviewDataLut": {
"enabled": false
},
diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json
index 46beeb85b9..88b5a598cd 100644
--- a/openpype/settings/defaults/project_settings/tvpaint.json
+++ b/openpype/settings/defaults/project_settings/tvpaint.json
@@ -1,6 +1,10 @@
{
"stop_timer_on_application_exit": false,
"publish": {
+ "CollectRenderScene": {
+ "enabled": false,
+ "render_layer": "Main"
+ },
"ExtractSequence": {
"review_bg": [
255,
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json
index e6097a2b14..ea1173313b 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json
@@ -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",
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json
index cb59e9d67e..fb384882c6 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json
@@ -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
}
]
}
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json
index 97462a8b62..20fe5b0855 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json
@@ -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 'render_layer' or 'variant' in subset name template with custom value.
- value of 'render_pass' is always \"beauty\"."
+ },
+ {
+ "type": "text",
+ "key": "render_layer",
+ "label": "Render Layer"
+ }
+ ]
+ },
{
"type": "dict",
"collapsible": true,
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json
index 673e12d54b..27e8957786 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json
@@ -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,
diff --git a/openpype/style/style.css b/openpype/style/style.css
index df83600973..b5f6962eee 100644
--- a/openpype/style/style.css
+++ b/openpype/style/style.css
@@ -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};
diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py
index d2b8a76952..56af7752da 100644
--- a/openpype/tools/workfiles/files_widget.py
+++ b/openpype/tools/workfiles/files_widget.py
@@ -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
<",
+ 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())
diff --git a/openpype/tools/workfiles/lib.py b/openpype/tools/workfiles/lib.py
deleted file mode 100644
index 21a7485b7b..0000000000
--- a/openpype/tools/workfiles/lib.py
+++ /dev/null
@@ -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: 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)
diff --git a/openpype/tools/workfiles/save_as_dialog.py b/openpype/tools/workfiles/save_as_dialog.py
index e616a325cc..f5ae393d0f 100644
--- a/openpype/tools/workfiles/save_as_dialog.py
+++ b/openpype/tools/workfiles/save_as_dialog.py
@@ -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
diff --git a/openpype/tools/workfiles/window.py b/openpype/tools/workfiles/window.py
index 8654a18036..73e63d30b5 100644
--- a/openpype/tools/workfiles/window.py
+++ b/openpype/tools/workfiles/window.py
@@ -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)
diff --git a/openpype/version.py b/openpype/version.py
index 6d55672aca..3d759096c8 100644
--- a/openpype/version.py
+++ b/openpype/version.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
-__version__ = "3.9.2-nightly.3"
+__version__ = "3.9.2"
diff --git a/pyproject.toml b/pyproject.toml
index 479cd731fe..1084382d9a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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 "]
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
\ No newline at end of file
diff --git a/website/docs/module_site_sync.md b/website/docs/module_site_sync.md
index 78f482352e..2e9cf01102 100644
--- a/website/docs/module_site_sync.md
+++ b/website/docs/module_site_sync.md
@@ -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.