Merge branch 'develop' into feature/1235-hiero-unify-otio-workflow-from-resolve

This commit is contained in:
Jakub Jezek 2021-04-13 14:51:11 +02:00
commit 87ad6fed45
No known key found for this signature in database
GPG key ID: C4B96E101D2A47F3
65 changed files with 1189 additions and 857 deletions

View file

@ -6,7 +6,7 @@ class PrePython2Vendor(PreLaunchHook):
"""Prepend python 2 dependencies for py2 hosts."""
# WARNING This hook will probably be deprecated in OpenPype 3 - kept for test
order = 10
app_groups = ["hiero", "nuke", "nukex"]
app_groups = ["hiero", "nuke", "nukex", "unreal"]
def execute(self):
# Prepare vendor dir path

View file

@ -51,8 +51,37 @@ def set_start_end_frames():
"name": asset_name
})
bpy.context.scene.frame_start = asset_doc["data"]["frameStart"]
bpy.context.scene.frame_end = asset_doc["data"]["frameEnd"]
scene = bpy.context.scene
# Default scene settings
frameStart = scene.frame_start
frameEnd = scene.frame_end
fps = scene.render.fps
resolution_x = scene.render.resolution_x
resolution_y = scene.render.resolution_y
# Check if settings are set
data = asset_doc.get("data")
if not data:
return
if data.get("frameStart"):
frameStart = data.get("frameStart")
if data.get("frameEnd"):
frameEnd = data.get("frameEnd")
if data.get("fps"):
fps = data.get("fps")
if data.get("resolutionWidth"):
resolution_x = data.get("resolutionWidth")
if data.get("resolutionHeight"):
resolution_y = data.get("resolutionHeight")
scene.frame_start = frameStart
scene.frame_end = frameEnd
scene.render.fps = fps
scene.render.resolution_x = resolution_x
scene.render.resolution_y = resolution_y
def on_new(arg1, arg2):

View file

@ -292,6 +292,9 @@ class UnrealLayoutLoader(plugin.AssetLoader):
icon = "code-fork"
color = "orange"
animation_creator_name = "CreateAnimation"
setdress_creator_name = "CreateSetDress"
def _remove_objects(self, objects):
for obj in list(objects):
if obj.type == 'ARMATURE':
@ -368,7 +371,7 @@ class UnrealLayoutLoader(plugin.AssetLoader):
location.get('z')
)
obj.rotation_euler = (
rotation.get('x'),
rotation.get('x') + math.pi / 2,
-rotation.get('y'),
-rotation.get('z')
)

View file

@ -0,0 +1,35 @@
from typing import List
import pyblish.api
import openpype.hosts.blender.api.action
class ValidateObjectIsInObjectMode(pyblish.api.InstancePlugin):
"""Validate that the current object is in Object Mode."""
order = pyblish.api.ValidatorOrder - 0.01
hosts = ["blender"]
families = ["model", "rig"]
category = "geometry"
label = "Object is in Object Mode"
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]
optional = True
@classmethod
def get_invalid(cls, instance) -> List:
invalid = []
for obj in [obj for obj in instance]:
try:
if obj.type == 'MESH' or obj.type == 'ARMATURE':
# Check if the object is in object mode.
if not obj.mode == 'OBJECT':
invalid.append(obj)
except Exception:
continue
return invalid
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError(
f"Object found in instance is not in Object Mode: {invalid}")

View file

@ -11,7 +11,9 @@ from .api.pipeline import (
update_container,
publish,
launch_workfiles_app,
maintained_selection
maintained_selection,
remove_instance,
list_instances
)
from .api.lib import (
@ -73,6 +75,8 @@ __all__ = [
"publish",
"launch_workfiles_app",
"maintained_selection",
"remove_instance",
"list_instances",
# utils
"setup",

View file

@ -12,7 +12,8 @@ from avalon.tools import (
creator,
loader,
sceneinventory,
libraryloader
libraryloader,
subsetmanager
)
@ -64,8 +65,9 @@ class OpenPypeMenu(QtWidgets.QWidget):
publish_btn = QtWidgets.QPushButton("Publish ...", self)
load_btn = QtWidgets.QPushButton("Load ...", self)
inventory_btn = QtWidgets.QPushButton("Inventory ...", self)
subsetm_btn = QtWidgets.QPushButton("Subset Manager ...", self)
libload_btn = QtWidgets.QPushButton("Library ...", self)
# rename_btn = QtWidgets.QPushButton("Rename ...", self)
# rename_btn = QtWidgets.QPushButton("Rename", self)
# set_colorspace_btn = QtWidgets.QPushButton(
# "Set colorspace from presets", self
# )
@ -81,6 +83,7 @@ class OpenPypeMenu(QtWidgets.QWidget):
layout.addWidget(publish_btn)
layout.addWidget(load_btn)
layout.addWidget(inventory_btn)
layout.addWidget(subsetm_btn)
layout.addWidget(Spacer(15, self))
@ -102,6 +105,7 @@ class OpenPypeMenu(QtWidgets.QWidget):
publish_btn.clicked.connect(self.on_publish_clicked)
load_btn.clicked.connect(self.on_load_clicked)
inventory_btn.clicked.connect(self.on_inventory_clicked)
subsetm_btn.clicked.connect(self.on_subsetm_clicked)
libload_btn.clicked.connect(self.on_libload_clicked)
# rename_btn.clicked.connect(self.on_rename_clicked)
# set_colorspace_btn.clicked.connect(self.on_set_colorspace_clicked)
@ -127,6 +131,10 @@ class OpenPypeMenu(QtWidgets.QWidget):
print("Clicked Inventory")
sceneinventory.show()
def on_subsetm_clicked(self):
print("Clicked Subset Manager")
subsetmanager.show()
def on_libload_clicked(self):
print("Clicked Library")
libraryloader.show()

View file

@ -258,3 +258,51 @@ def on_pyblish_instance_toggled(instance, old_value, new_value):
# Whether instances should be passthrough based on new value
timeline_item = instance.data["item"]
set_publish_attribute(timeline_item, new_value)
def remove_instance(instance):
"""Remove instance marker from track item."""
instance_id = instance.get("uuid")
selected_timeline_items = lib.get_current_timeline_items(
filter=True, selecting_color=lib.publish_clip_color)
found_ti = None
for timeline_item_data in selected_timeline_items:
timeline_item = timeline_item_data["clip"]["item"]
# get openpype tag data
tag_data = lib.get_timeline_item_pype_tag(timeline_item)
_ti_id = tag_data.get("uuid")
if _ti_id == instance_id:
found_ti = timeline_item
break
if found_ti is None:
return
# removing instance by marker color
print(f"Removing instance: {found_ti.GetName()}")
found_ti.DeleteMarkersByColor(lib.pype_marker_color)
def list_instances():
"""List all created instances from current workfile."""
listed_instances = []
selected_timeline_items = lib.get_current_timeline_items(
filter=True, selecting_color=lib.publish_clip_color)
for timeline_item_data in selected_timeline_items:
timeline_item = timeline_item_data["clip"]["item"]
ti_name = timeline_item.GetName().split(".")[0]
# get openpype tag data
tag_data = lib.get_timeline_item_pype_tag(timeline_item)
if tag_data:
asset = tag_data.get("asset")
subset = tag_data.get("subset")
tag_data["label"] = f"{ti_name} [{asset}-{subset}]"
listed_instances.append(tag_data)
return listed_instances

View file

@ -1,4 +1,5 @@
import re
import uuid
from avalon import api
import openpype.api as pype
from openpype.hosts import resolve
@ -697,13 +698,13 @@ class PublishClip:
Populating the tag data into internal variable self.tag_data
"""
# define vertical sync attributes
master_layer = True
hero_track = True
self.review_layer = ""
if self.vertical_sync:
# check if track name is not in driving layer
if self.track_name not in self.driving_layer:
# if it is not then define vertical sync as None
master_layer = False
hero_track = False
# increasing steps by index of rename iteration
self.count_steps *= self.rename_index
@ -717,7 +718,7 @@ class PublishClip:
self.tag_data[_k] = _v["value"]
# driving layer is set as positive match
if master_layer or self.vertical_sync:
if hero_track or self.vertical_sync:
# mark review layer
if self.review_track and (
self.review_track not in self.review_track_default):
@ -751,35 +752,39 @@ class PublishClip:
hierarchy_formating_data
)
tag_hierarchy_data.update({"masterLayer": True})
if master_layer and self.vertical_sync:
# tag_hierarchy_data.update({"masterLayer": True})
tag_hierarchy_data.update({"heroTrack": True})
if hero_track and self.vertical_sync:
self.vertical_clip_match.update({
(self.clip_in, self.clip_out): tag_hierarchy_data
})
if not master_layer and self.vertical_sync:
if not hero_track and self.vertical_sync:
# driving layer is set as negative match
for (_in, _out), master_data in self.vertical_clip_match.items():
master_data.update({"masterLayer": False})
for (_in, _out), hero_data in self.vertical_clip_match.items():
hero_data.update({"heroTrack": False})
if _in == self.clip_in and _out == self.clip_out:
data_subset = master_data["subset"]
# add track index in case duplicity of names in master data
data_subset = hero_data["subset"]
# add track index in case duplicity of names in hero data
if self.subset in data_subset:
master_data["subset"] = self.subset + str(
hero_data["subset"] = self.subset + str(
self.track_index)
# in case track name and subset name is the same then add
if self.subset_name == self.track_name:
master_data["subset"] = self.subset
hero_data["subset"] = self.subset
# assing data to return hierarchy data to tag
tag_hierarchy_data = master_data
tag_hierarchy_data = hero_data
# add data to return data dict
self.tag_data.update(tag_hierarchy_data)
if master_layer and self.review_layer:
# add uuid to tag data
self.tag_data["uuid"] = str(uuid.uuid4())
# add review track only to hero track
if hero_track and self.review_layer:
self.tag_data.update({"reviewTrack": self.review_layer})
def _solve_tag_hierarchy_data(self, hierarchy_formating_data):
""" Solve tag data from hierarchy data and templates. """
# fill up clip name and hierarchy keys

View file

@ -117,7 +117,7 @@ class CreateShotClip(resolve.Creator):
"vSyncTrack": {
"value": gui_tracks, # noqa
"type": "QComboBox",
"label": "Master track",
"label": "Hero track",
"target": "ui",
"toolTip": "Select driving track name which should be mastering all others", # noqa
"order": 1}

View file

@ -5,11 +5,11 @@ from openpype.hosts import resolve
from pprint import pformat
class CollectInstances(pyblish.api.ContextPlugin):
class PrecollectInstances(pyblish.api.ContextPlugin):
"""Collect all Track items selection."""
order = pyblish.api.CollectorOrder - 0.59
label = "Collect Instances"
label = "Precollect Instances"
hosts = ["resolve"]
def process(self, context):
@ -26,7 +26,7 @@ class CollectInstances(pyblish.api.ContextPlugin):
data = dict()
timeline_item = timeline_item_data["clip"]["item"]
# get openpype tag data
# get pype tag data
tag_data = resolve.get_timeline_item_pype_tag(timeline_item)
self.log.debug(f"__ tag_data: {pformat(tag_data)}")
@ -102,10 +102,10 @@ class CollectInstances(pyblish.api.ContextPlugin):
})
def create_shot_instance(self, context, timeline_item, **data):
master_layer = data.get("masterLayer")
hero_track = data.get("heroTrack")
hierarchy_data = data.get("hierarchyData")
if not master_layer:
if not hero_track:
return
if not hierarchy_data:

View file

@ -9,10 +9,10 @@ from openpype.hosts.resolve.otio import davinci_export
reload(davinci_export)
class CollectWorkfile(pyblish.api.ContextPlugin):
"""Inject the current working file into context"""
class PrecollectWorkfile(pyblish.api.ContextPlugin):
"""Precollect the current working file into context"""
label = "Collect Workfile"
label = "Precollect Workfile"
order = pyblish.api.CollectorOrder - 0.6
def process(self, context):
@ -21,8 +21,6 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
subset = "workfile"
project = resolve.get_current_project()
fps = project.GetSetting("timelineFrameRate")
active_timeline = resolve.get_current_timeline()
video_tracks = resolve.get_video_track_names()
# adding otio timeline to context

View file

@ -58,9 +58,8 @@ def _close_window(event):
def _export_button(event):
pm = resolve.GetProjectManager()
project = pm.GetCurrentProject()
fps = project.GetSetting("timelineFrameRate")
timeline = project.GetCurrentTimeline()
otio_timeline = otio_export.create_otio_timeline(timeline, fps)
otio_timeline = otio_export.create_otio_timeline(project)
otio_path = os.path.join(
itm["exportfilebttn"].Text,
timeline.GetName() + ".otio")

View file

@ -18,7 +18,7 @@ class CollectInstances(pyblish.api.ContextPlugin):
))
for instance_data in workfile_instances:
instance_data["fps"] = context.data["fps"]
instance_data["fps"] = context.data["sceneFps"]
# Store workfile instance data to instance data
instance_data["originData"] = copy.deepcopy(instance_data)
@ -32,6 +32,11 @@ class CollectInstances(pyblish.api.ContextPlugin):
subset_name = instance_data["subset"]
name = instance_data.get("name", subset_name)
instance_data["name"] = name
instance_data["label"] = "{} [{}-{}]".format(
name,
context.data["sceneFrameStart"],
context.data["sceneFrameEnd"]
)
active = instance_data.get("active", True)
instance_data["active"] = active
@ -73,8 +78,8 @@ class CollectInstances(pyblish.api.ContextPlugin):
if instance is None:
continue
instance.data["frameStart"] = context.data["frameStart"]
instance.data["frameEnd"] = context.data["frameEnd"]
instance.data["frameStart"] = context.data["sceneFrameStart"]
instance.data["frameEnd"] = context.data["sceneFrameEnd"]
self.log.debug("Created instance: {}\n{}".format(
instance, json.dumps(instance.data, indent=4)

View file

@ -127,11 +127,11 @@ class CollectWorkfileData(pyblish.api.ContextPlugin):
"currentFile": workfile_path,
"sceneWidth": width,
"sceneHeight": height,
"pixelAspect": pixel_apsect,
"frameStart": frame_start,
"frameEnd": frame_end,
"fps": frame_rate,
"fieldOrder": field_order
"scenePixelAspect": pixel_apsect,
"sceneFrameStart": frame_start,
"sceneFrameEnd": frame_end,
"sceneFps": frame_rate,
"sceneFieldOrder": field_order
}
self.log.debug(
"Scene data: {}".format(json.dumps(scene_data, indent=4))

View file

@ -0,0 +1,36 @@
import json
import pyblish.api
class ValidateProjectSettings(pyblish.api.ContextPlugin):
"""Validate project settings against database.
"""
label = "Validate Project Settings"
order = pyblish.api.ValidatorOrder
optional = True
def process(self, context):
scene_data = {
"frameStart": context.data.get("sceneFrameStart"),
"frameEnd": context.data.get("sceneFrameEnd"),
"fps": context.data.get("sceneFps"),
"resolutionWidth": context.data.get("sceneWidth"),
"resolutionHeight": context.data.get("sceneHeight"),
"pixelAspect": context.data.get("scenePixelAspect")
}
invalid = {}
for k in scene_data.keys():
expected_value = context.data["assetEntity"]["data"][k]
if scene_data[k] != expected_value:
invalid[k] = {
"current": scene_data[k], "expected": expected_value
}
if invalid:
raise AssertionError(
"Project settings does not match database:\n{}".format(
json.dumps(invalid, sort_keys=True, indent=4)
)
)

View file

View file

@ -23,8 +23,8 @@ class UnrealPrelaunchHook(PreLaunchHook):
def execute(self):
asset_name = self.data["asset_name"]
task_name = self.data["task_name"]
workdir = self.env["AVALON_WORKDIR"]
engine_version = self.app_name.split("_")[-1]
workdir = self.launch_context.env["AVALON_WORKDIR"]
engine_version = self.app_name.split("_")[-1].replace("-", ".")
unreal_project_name = f"{asset_name}_{task_name}"
# Unreal is sensitive about project names longer then 20 chars
@ -81,8 +81,8 @@ class UnrealPrelaunchHook(PreLaunchHook):
# Set "AVALON_UNREAL_PLUGIN" to current process environment for
# execution of `create_unreal_project`
env_key = "AVALON_UNREAL_PLUGIN"
if self.env.get(env_key):
os.environ[env_key] = self.env[env_key]
if self.launch_context.env.get(env_key):
os.environ[env_key] = self.launch_context.env[env_key]
unreal_lib.create_unreal_project(
unreal_project_name,

View file

@ -104,7 +104,8 @@ from .plugin_tools import (
from .local_settings import (
IniSettingRegistry,
JSONSettingRegistry,
PypeSettingsRegistry,
OpenPypeSecureRegistry,
OpenPypeSettingsRegistry,
get_local_site_id,
change_openpype_mongo_url
)
@ -217,7 +218,8 @@ __all__ = [
"IniSettingRegistry",
"JSONSettingRegistry",
"PypeSettingsRegistry",
"OpenPypeSecureRegistry",
"OpenPypeSettingsRegistry",
"get_local_site_id",
"change_openpype_mongo_url",

View file

@ -1123,6 +1123,7 @@ class BuildWorkfile:
return output
@with_avalon
def get_creator_by_name(creator_name, case_sensitive=False):
"""Find creator plugin by name.

View file

@ -5,6 +5,7 @@ from datetime import datetime
from abc import ABCMeta, abstractmethod
import json
# TODO Use pype igniter logic instead of using duplicated code
# disable lru cache in Python 2
try:
from functools import lru_cache
@ -25,11 +26,115 @@ except ImportError:
import platform
import appdirs
import six
import appdirs
from .import validate_mongo_connection
_PLACEHOLDER = object()
class OpenPypeSecureRegistry:
"""Store information using keyring.
Registry should be used for private data that should be available only for
user.
All passed registry names will have added prefix `OpenPype/` to easier
identify which data were created by OpenPype.
Args:
name(str): Name of registry used as identifier for data.
"""
def __init__(self, name):
try:
import keyring
except Exception:
raise NotImplementedError(
"Python module `keyring` is not available."
)
# hack for cx_freeze and Windows keyring backend
if platform.system().lower() == "windows":
from keyring.backends import Windows
keyring.set_keyring(Windows.WinVaultKeyring())
# Force "OpenPype" prefix
self._name = "/".join(("OpenPype", name))
def set_item(self, name, value):
# type: (str, str) -> None
"""Set sensitive item into system's keyring.
This uses `Keyring module`_ to save sensitive stuff into system's
keyring.
Args:
name (str): Name of the item.
value (str): Value of the item.
.. _Keyring module:
https://github.com/jaraco/keyring
"""
import keyring
keyring.set_password(self._name, name, value)
@lru_cache(maxsize=32)
def get_item(self, name, default=_PLACEHOLDER):
"""Get value of sensitive item from system's keyring.
See also `Keyring module`_
Args:
name (str): Name of the item.
default (Any): Default value if item is not available.
Returns:
value (str): Value of the item.
Raises:
ValueError: If item doesn't exist and default is not defined.
.. _Keyring module:
https://github.com/jaraco/keyring
"""
import keyring
value = keyring.get_password(self._name, name)
if value is not None:
return value
if default is not _PLACEHOLDER:
return default
# NOTE Should raise `KeyError`
raise ValueError(
"Item {}:{} does not exist in keyring.".format(self._name, name)
)
def delete_item(self, name):
# type: (str) -> None
"""Delete value stored in system's keyring.
See also `Keyring module`_
Args:
name (str): Name of the item to be deleted.
.. _Keyring module:
https://github.com/jaraco/keyring
"""
import keyring
self.get_item.cache_clear()
keyring.delete_password(self._name, name)
@six.add_metaclass(ABCMeta)
class ASettingRegistry():
@ -48,13 +153,6 @@ class ASettingRegistry():
# type: (str) -> ASettingRegistry
super(ASettingRegistry, self).__init__()
if six.PY3:
import keyring
# hack for cx_freeze and Windows keyring backend
if platform.system() == "Windows":
from keyring.backends import Windows
keyring.set_keyring(Windows.WinVaultKeyring())
self._name = name
self._items = {}
@ -120,7 +218,7 @@ class ASettingRegistry():
"""Delete item from settings.
Note:
see :meth:`pype.lib.local_settings.ARegistrySettings.delete_item`
see :meth:`openpype.lib.user_settings.ARegistrySettings.delete_item`
"""
pass
@ -129,78 +227,6 @@ class ASettingRegistry():
del self._items[name]
self._delete_item(name)
def set_secure_item(self, name, value):
# type: (str, str) -> None
"""Set sensitive item into system's keyring.
This uses `Keyring module`_ to save sensitive stuff into system's
keyring.
Args:
name (str): Name of the item.
value (str): Value of the item.
.. _Keyring module:
https://github.com/jaraco/keyring
"""
if six.PY2:
raise NotImplementedError(
"Keyring not available on Python 2 hosts")
import keyring
keyring.set_password(self._name, name, value)
@lru_cache(maxsize=32)
def get_secure_item(self, name):
# type: (str) -> str
"""Get value of sensitive item from system's keyring.
See also `Keyring module`_
Args:
name (str): Name of the item.
Returns:
value (str): Value of the item.
Raises:
ValueError: If item doesn't exist.
.. _Keyring module:
https://github.com/jaraco/keyring
"""
if six.PY2:
raise NotImplementedError(
"Keyring not available on Python 2 hosts")
import keyring
value = keyring.get_password(self._name, name)
if not value:
raise ValueError(
"Item {}:{} does not exist in keyring.".format(
self._name, name))
return value
def delete_secure_item(self, name):
# type: (str) -> None
"""Delete value stored in system's keyring.
See also `Keyring module`_
Args:
name (str): Name of the item to be deleted.
.. _Keyring module:
https://github.com/jaraco/keyring
"""
if six.PY2:
raise NotImplementedError(
"Keyring not available on Python 2 hosts")
import keyring
self.get_secure_item.cache_clear()
keyring.delete_password(self._name, name)
class IniSettingRegistry(ASettingRegistry):
"""Class using :mod:`configparser`.
@ -218,7 +244,7 @@ class IniSettingRegistry(ASettingRegistry):
if not os.path.exists(self._registry_file):
with open(self._registry_file, mode="w") as cfg:
print("# Settings registry", cfg)
print("# Generated by Pype {}".format(version), cfg)
print("# Generated by OpenPype {}".format(version), cfg)
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
print("# {}".format(now), cfg)
@ -352,7 +378,7 @@ class IniSettingRegistry(ASettingRegistry):
"""Delete item from default section.
Note:
See :meth:`~pype.lib.IniSettingsRegistry.delete_item_from_section`
See :meth:`~openpype.lib.IniSettingsRegistry.delete_item_from_section`
"""
self.delete_item_from_section("MAIN", name)
@ -369,7 +395,7 @@ class JSONSettingRegistry(ASettingRegistry):
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
header = {
"__metadata__": {
"pype-version": os.getenv("OPENPYPE_VERSION", "N/A"),
"openpype-version": os.getenv("OPENPYPE_VERSION", "N/A"),
"generated": now
},
"registry": {}
@ -387,7 +413,7 @@ class JSONSettingRegistry(ASettingRegistry):
"""Get item value from registry json.
Note:
See :meth:`pype.lib.JSONSettingRegistry.get_item`
See :meth:`openpype.lib.JSONSettingRegistry.get_item`
"""
with open(self._registry_file, mode="r") as cfg:
@ -420,7 +446,7 @@ class JSONSettingRegistry(ASettingRegistry):
"""Set item value to registry json.
Note:
See :meth:`pype.lib.JSONSettingRegistry.set_item`
See :meth:`openpype.lib.JSONSettingRegistry.set_item`
"""
with open(self._registry_file, "r+") as cfg:
@ -452,8 +478,8 @@ class JSONSettingRegistry(ASettingRegistry):
json.dump(data, cfg, indent=4)
class PypeSettingsRegistry(JSONSettingRegistry):
"""Class handling Pype general settings registry.
class OpenPypeSettingsRegistry(JSONSettingRegistry):
"""Class handling OpenPype general settings registry.
Attributes:
vendor (str): Name used for path construction.
@ -461,21 +487,23 @@ class PypeSettingsRegistry(JSONSettingRegistry):
"""
def __init__(self):
def __init__(self, name=None):
self.vendor = "pypeclub"
self.product = "pype"
self.product = "openpype"
if not name:
name = "openpype_settings"
path = appdirs.user_data_dir(self.product, self.vendor)
super(PypeSettingsRegistry, self).__init__("pype_settings", path)
super(OpenPypeSettingsRegistry, self).__init__(name, path)
def _create_local_site_id(registry=None):
"""Create a local site identifier."""
from uuid import uuid4
from coolname import generate_slug
if registry is None:
registry = PypeSettingsRegistry()
registry = OpenPypeSettingsRegistry()
new_id = str(uuid4())
new_id = generate_slug(3)
print("Created local site id \"{}\"".format(new_id))
@ -489,7 +517,7 @@ def get_local_site_id():
Identifier is created if does not exists yet.
"""
registry = PypeSettingsRegistry()
registry = OpenPypeSettingsRegistry()
try:
return registry.get_item("localId")
except ValueError:
@ -504,5 +532,9 @@ def change_openpype_mongo_url(new_mongo_url):
"""
validate_mongo_connection(new_mongo_url)
registry = PypeSettingsRegistry()
registry.set_secure_item("pypeMongo", new_mongo_url)
key = "openPypeMongo"
registry = OpenPypeSecureRegistry("mongodb")
existing_value = registry.get_item(key, None)
if existing_value is not None:
registry.delete_item(key)
registry.set_item(key, new_mongo_url)

View file

@ -1,13 +1,16 @@
import os
import re
import time
import requests
import json
import datetime
import requests
from .constants import (
CLOCKIFY_ENDPOINT, ADMIN_PERMISSION_NAMES, CREDENTIALS_JSON_PATH
CLOCKIFY_ENDPOINT,
ADMIN_PERMISSION_NAMES
)
from openpype.lib.local_settings import OpenPypeSecureRegistry
def time_check(obj):
if obj.request_counter < 10:
@ -31,6 +34,8 @@ class ClockifyAPI:
self.request_counter = 0
self.request_time = time.time()
self.secure_registry = OpenPypeSecureRegistry("clockify")
@property
def headers(self):
return {"X-Api-Key": self.api_key}
@ -129,22 +134,10 @@ class ClockifyAPI:
return False
def get_api_key(self):
api_key = None
try:
file = open(CREDENTIALS_JSON_PATH, 'r')
api_key = json.load(file).get('api_key', None)
if api_key == '':
api_key = None
except Exception:
file = open(CREDENTIALS_JSON_PATH, 'w')
file.close()
return api_key
return self.secure_registry.get_item("api_key", None)
def save_api_key(self, api_key):
data = {'api_key': api_key}
file = open(CREDENTIALS_JSON_PATH, 'w')
file.write(json.dumps(data))
file.close()
self.secure_registry.set_item("api_key", api_key)
def get_workspaces(self):
action_url = 'workspaces/'

View file

@ -1,17 +1,12 @@
import os
import appdirs
CLOCKIFY_FTRACK_SERVER_PATH = os.path.join(
os.path.dirname(__file__), "ftrack", "server"
os.path.dirname(os.path.abspath(__file__)), "ftrack", "server"
)
CLOCKIFY_FTRACK_USER_PATH = os.path.join(
os.path.dirname(__file__), "ftrack", "user"
os.path.dirname(os.path.abspath(__file__)), "ftrack", "user"
)
CREDENTIALS_JSON_PATH = os.path.normpath(os.path.join(
appdirs.user_data_dir("pype-app", "pype"),
"clockify.json"
))
ADMIN_PERMISSION_NAMES = ["WORKSPACE_OWN", "WORKSPACE_ADMIN"]
CLOCKIFY_ENDPOINT = "https://api.clockify.me/api/"

View file

@ -210,3 +210,7 @@ class FtrackModule(
def tray_exit(self):
return self.tray_module.stop_action_server()
def set_credentials_to_env(self, username, api_key):
os.environ["FTRACK_API_USER"] = username or ""
os.environ["FTRACK_API_KEY"] = api_key or ""

View file

@ -9,7 +9,7 @@ class PrePython2Support(PreLaunchHook):
Path to vendor modules is added to the beggining of PYTHONPATH.
"""
# There will be needed more granular filtering in future
app_groups = ["maya", "nuke", "nukex", "hiero", "nukestudio"]
app_groups = ["maya", "nuke", "nukex", "hiero", "nukestudio", "unreal"]
def execute(self):
# Prepare vendor dir path

View file

@ -891,6 +891,33 @@ class SyncEntitiesFactory:
self.entities_dict[parent_id]["children"].remove(id)
def _query_custom_attributes(self, session, conf_ids, entity_ids):
output = []
# Prepare values to query
attributes_joined = join_query_keys(conf_ids)
attributes_len = len(conf_ids)
chunk_size = int(5000 / attributes_len)
for idx in range(0, len(entity_ids), chunk_size):
entity_ids_joined = join_query_keys(
entity_ids[idx:idx + chunk_size]
)
call_expr = [{
"action": "query",
"expression": (
"select value, entity_id from ContextCustomAttributeValue "
"where entity_id in ({}) and configuration_id in ({})"
).format(entity_ids_joined, attributes_joined)
}]
if hasattr(session, "call"):
[result] = session.call(call_expr)
else:
[result] = session._call(call_expr)
for item in result["data"]:
output.append(item)
return output
def set_cutom_attributes(self):
self.log.debug("* Preparing custom attributes")
# Get custom attributes and values
@ -1000,31 +1027,13 @@ class SyncEntitiesFactory:
copy.deepcopy(prepared_avalon_attr_ca_id)
)
# TODO query custom attributes by entity_id
entity_ids_joined = ", ".join([
"\"{}\"".format(id) for id in sync_ids
])
attributes_joined = ", ".join([
"\"{}\"".format(attr_id) for attr_id in attribute_key_by_id.keys()
])
cust_attr_query = (
"select value, configuration_id, entity_id"
" from ContextCustomAttributeValue"
" where entity_id in ({}) and configuration_id in ({})"
items = self._query_custom_attributes(
self.session,
list(attribute_key_by_id.keys()),
sync_ids
)
call_expr = [{
"action": "query",
"expression": cust_attr_query.format(
entity_ids_joined, attributes_joined
)
}]
if hasattr(self.session, "call"):
[values] = self.session.call(call_expr)
else:
[values] = self.session._call(call_expr)
for item in values["data"]:
for item in items:
entity_id = item["entity_id"]
attr_id = item["configuration_id"]
key = attribute_key_by_id[attr_id]
@ -1106,28 +1115,14 @@ class SyncEntitiesFactory:
for key, val in prepare_dict_avalon.items():
entity_dict["avalon_attrs"][key] = val
# Prepare values to query
entity_ids_joined = ", ".join([
"\"{}\"".format(id) for id in sync_ids
])
attributes_joined = ", ".join([
"\"{}\"".format(attr_id) for attr_id in attribute_key_by_id.keys()
])
avalon_hier = []
call_expr = [{
"action": "query",
"expression": (
"select value, entity_id, configuration_id"
" from ContextCustomAttributeValue"
" where entity_id in ({}) and configuration_id in ({})"
).format(entity_ids_joined, attributes_joined)
}]
if hasattr(self.session, "call"):
[values] = self.session.call(call_expr)
else:
[values] = self.session._call(call_expr)
items = self._query_custom_attributes(
self.session,
list(attribute_key_by_id.keys()),
sync_ids
)
for item in values["data"]:
avalon_hier = []
for item in items:
value = item["value"]
# WARNING It is not possible to propage enumerate hierachical
# attributes with multiselection 100% right. Unseting all values
@ -1256,19 +1251,21 @@ class SyncEntitiesFactory:
if not msg or not items:
continue
self.report_items["warning"][msg] = items
tasks = {}
for task_type in task_types:
task_type_name = task_type["name"]
# Set short name to empty string
# QUESTION Maybe better would be to lower and remove spaces
# from task type name.
tasks[task_type_name] = {
"short_name": ""
}
current_project_anatomy_data = get_anatomy_settings(
project_name, exclude_locals=True
)
anatomy_tasks = current_project_anatomy_data["tasks"]
tasks = {}
default_type_data = {
"short_name": ""
}
for task_type in task_types:
task_type_name = task_type["name"]
tasks[task_type_name] = copy.deepcopy(
anatomy_tasks.get(task_type_name)
or default_type_data
)
project_config = {
"tasks": tasks,

View file

@ -1,23 +1,16 @@
import os
import json
import ftrack_api
import appdirs
import getpass
try:
from urllib.parse import urlparse
except ImportError:
from urlparse import urlparse
CONFIG_PATH = os.path.normpath(appdirs.user_data_dir("pype-app", "pype"))
CREDENTIALS_FILE_NAME = "ftrack_cred.json"
CREDENTIALS_PATH = os.path.join(CONFIG_PATH, CREDENTIALS_FILE_NAME)
CREDENTIALS_FOLDER = os.path.dirname(CREDENTIALS_PATH)
from openpype.lib import OpenPypeSecureRegistry
if not os.path.isdir(CREDENTIALS_FOLDER):
os.makedirs(CREDENTIALS_FOLDER)
USER_GETTER = None
USERNAME_KEY = "username"
API_KEY_KEY = "api_key"
def get_ftrack_hostname(ftrack_server=None):
@ -30,112 +23,73 @@ def get_ftrack_hostname(ftrack_server=None):
return urlparse(ftrack_server).hostname
def get_user():
if USER_GETTER:
return USER_GETTER()
return getpass.getuser()
def _get_ftrack_secure_key(hostname, key):
"""Secure item key for entered hostname."""
return "/".join(("ftrack", hostname, key))
def get_credentials(ftrack_server=None, user=None):
credentials = {}
if not os.path.exists(CREDENTIALS_PATH):
with open(CREDENTIALS_PATH, "w") as file:
file.write(json.dumps(credentials))
file.close()
return credentials
with open(CREDENTIALS_PATH, "r") as file:
content = file.read()
def get_credentials(ftrack_server=None):
hostname = get_ftrack_hostname(ftrack_server)
if not user:
user = get_user()
username_name = _get_ftrack_secure_key(hostname, USERNAME_KEY)
api_key_name = _get_ftrack_secure_key(hostname, API_KEY_KEY)
content_json = json.loads(content or "{}")
credentials = content_json.get(hostname, {}).get(user) or {}
username_registry = OpenPypeSecureRegistry(username_name)
api_key_registry = OpenPypeSecureRegistry(api_key_name)
return credentials
def save_credentials(ft_user, ft_api_key, ftrack_server=None, user=None):
hostname = get_ftrack_hostname(ftrack_server)
if not user:
user = get_user()
with open(CREDENTIALS_PATH, "r") as file:
content = file.read()
content_json = json.loads(content or "{}")
if hostname not in content_json:
content_json[hostname] = {}
content_json[hostname][user] = {
"username": ft_user,
"api_key": ft_api_key
return {
USERNAME_KEY: username_registry.get_item(USERNAME_KEY, None),
API_KEY_KEY: api_key_registry.get_item(API_KEY_KEY, None)
}
# Deprecated keys
if "username" in content_json:
content_json.pop("username")
if "apiKey" in content_json:
content_json.pop("apiKey")
with open(CREDENTIALS_PATH, "w") as file:
file.write(json.dumps(content_json, indent=4))
def clear_credentials(ft_user=None, ftrack_server=None, user=None):
if not ft_user:
ft_user = os.environ.get("FTRACK_API_USER")
if not ft_user:
return
def save_credentials(username, api_key, ftrack_server=None):
hostname = get_ftrack_hostname(ftrack_server)
if not user:
user = get_user()
username_name = _get_ftrack_secure_key(hostname, USERNAME_KEY)
api_key_name = _get_ftrack_secure_key(hostname, API_KEY_KEY)
with open(CREDENTIALS_PATH, "r") as file:
content = file.read()
# Clear credentials
clear_credentials(ftrack_server)
content_json = json.loads(content or "{}")
if hostname not in content_json:
content_json[hostname] = {}
username_registry = OpenPypeSecureRegistry(username_name)
api_key_registry = OpenPypeSecureRegistry(api_key_name)
content_json[hostname].pop(user, None)
with open(CREDENTIALS_PATH, "w") as file:
file.write(json.dumps(content_json))
username_registry.set_item(USERNAME_KEY, username)
api_key_registry.set_item(API_KEY_KEY, api_key)
def set_env(ft_user=None, ft_api_key=None):
os.environ["FTRACK_API_USER"] = ft_user or ""
os.environ["FTRACK_API_KEY"] = ft_api_key or ""
def clear_credentials(ftrack_server=None):
hostname = get_ftrack_hostname(ftrack_server)
username_name = _get_ftrack_secure_key(hostname, USERNAME_KEY)
api_key_name = _get_ftrack_secure_key(hostname, API_KEY_KEY)
username_registry = OpenPypeSecureRegistry(username_name)
api_key_registry = OpenPypeSecureRegistry(api_key_name)
current_username = username_registry.get_item(USERNAME_KEY, None)
current_api_key = api_key_registry.get_item(API_KEY_KEY, None)
if current_username is not None:
username_registry.delete_item(USERNAME_KEY)
if current_api_key is not None:
api_key_registry.delete_item(API_KEY_KEY)
def get_env_credentials():
return (
os.environ.get("FTRACK_API_USER"),
os.environ.get("FTRACK_API_KEY")
)
def check_credentials(ft_user, ft_api_key, ftrack_server=None):
def check_credentials(username, api_key, ftrack_server=None):
if not ftrack_server:
ftrack_server = os.environ["FTRACK_SERVER"]
if not ft_user or not ft_api_key:
if not username or not api_key:
return False
try:
session = ftrack_api.Session(
server_url=ftrack_server,
api_key=ft_api_key,
api_user=ft_user
api_key=api_key,
api_user=username
)
session.close()
except Exception:
return False
return True

View file

@ -1,6 +1,7 @@
import os
from openpype.api import get_system_settings
def get_ftrack_settings():
return get_system_settings()["modules"]["ftrack"]
@ -10,7 +11,6 @@ def get_ftrack_url_from_settings():
def get_ftrack_event_mongo_info():
ftrack_settings = get_ftrack_settings()
database_name = os.environ["OPENPYPE_DATABASE_NAME"]
collection_name = "ftrack_events"
return database_name, collection_name

View file

@ -30,7 +30,7 @@ class FtrackTrayWrapper:
self.bool_action_thread_running = False
self.bool_timer_event = False
self.widget_login = login_dialog.CredentialsDialog()
self.widget_login = login_dialog.CredentialsDialog(module)
self.widget_login.login_changed.connect(self.on_login_change)
self.widget_login.logout_signal.connect(self.on_logout)
@ -56,7 +56,7 @@ class FtrackTrayWrapper:
validation = credentials.check_credentials(ft_user, ft_api_key)
if validation:
self.widget_login.set_credentials(ft_user, ft_api_key)
credentials.set_env(ft_user, ft_api_key)
self.module.set_credentials_to_env(ft_user, ft_api_key)
log.info("Connected to Ftrack successfully")
self.on_login_change()
@ -337,7 +337,7 @@ class FtrackTrayWrapper:
def changed_user(self):
self.stop_action_server()
credentials.set_env()
self.module.set_credentials_to_env(None, None)
self.validate()
def start_timer_manager(self, data):

View file

@ -14,11 +14,13 @@ class CredentialsDialog(QtWidgets.QDialog):
login_changed = QtCore.Signal()
logout_signal = QtCore.Signal()
def __init__(self, parent=None):
def __init__(self, module, parent=None):
super(CredentialsDialog, self).__init__(parent)
self.setWindowTitle("OpenPype - Ftrack Login")
self._module = module
self._login_server_thread = None
self._is_logged = False
self._in_advance_mode = False
@ -268,7 +270,7 @@ class CredentialsDialog(QtWidgets.QDialog):
verification = credentials.check_credentials(username, api_key)
if verification:
credentials.save_credentials(username, api_key, False)
credentials.set_env(username, api_key)
self._module.set_credentials_to_env(username, api_key)
self.set_credentials(username, api_key)
self.login_changed.emit()
return verification

View file

@ -40,8 +40,7 @@ class IdleManager(PypeModule, ITrayService):
name = "idle_manager"
def initialize(self, module_settings):
idle_man_settings = module_settings[self.name]
self.enabled = idle_man_settings["enabled"]
self.enabled = True
self.time_callbacks = collections.defaultdict(list)
self.idle_thread = None
@ -50,7 +49,8 @@ class IdleManager(PypeModule, ITrayService):
return
def tray_start(self):
self.start_thread()
if self.time_callbacks:
self.start_thread()
def tray_exit(self):
self.stop_thread()

View file

@ -45,11 +45,13 @@ class TimersManager(PypeModule, ITrayService, IIdleManager, IWebServerRoutes):
timers_settings = modules_settings[self.name]
self.enabled = timers_settings["enabled"]
auto_stop = timers_settings["auto_stop"]
# When timer will stop if idle manager is running (minutes)
full_time = int(timers_settings["full_time"] * 60)
# How many minutes before the timer is stopped will popup the message
message_time = int(timers_settings["message_time"] * 60)
self.auto_stop = auto_stop
self.time_show_message = full_time - message_time
self.time_stop_timer = full_time
@ -160,6 +162,9 @@ class TimersManager(PypeModule, ITrayService, IIdleManager, IWebServerRoutes):
def callbacks_by_idle_time(self):
"""Implementation of IIdleManager interface."""
# Time when message is shown
if not self.auto_stop:
return {}
callbacks = collections.defaultdict(list)
callbacks[self.time_show_message].append(lambda: self.time_callback(0))

View file

@ -40,7 +40,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin):
continue
# exclude if not masterLayer True
if not instance.data.get("masterLayer"):
if not instance.data.get("heroTrack"):
continue
# get asset build data if any available
@ -50,7 +50,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin):
# suppose that all instances are Shots
shot_data['entity_type'] = 'Shot'
shot_data['tasks'] = instance.data.get("tasks") or []
shot_data['tasks'] = instance.data.get("tasks") or {}
shot_data["comments"] = instance.data.get("comments", [])
shot_data['custom_attributes'] = {

View file

@ -0,0 +1,10 @@
{
"publish": {
"ValidateMissingLayers": {
"enabled": true,
"optional": true,
"active": true
}
},
"filters": {}
}

View file

@ -126,6 +126,7 @@
},
"timers_manager": {
"enabled": true,
"auto_stop": true,
"full_time": 15.0,
"message_time": 0.5
},
@ -165,8 +166,5 @@
},
"standalonepublish_tool": {
"enabled": true
},
"idle_manager": {
"enabled": true
}
}

View file

@ -82,6 +82,10 @@
"type": "schema",
"name": "schema_project_harmony"
},
{
"type": "schema",
"name": "schema_project_tvpaint"
},
{
"type": "schema",
"name": "schema_project_celaction"

View file

@ -0,0 +1,32 @@
{
"type": "dict",
"collapsible": true,
"key": "tvpaint",
"label": "TVPaint",
"is_file": true,
"children": [
{
"type": "dict",
"collapsible": true,
"key": "publish",
"label": "Publish plugins",
"is_file": true,
"children": [
{
"type": "schema_template",
"name": "template_publish_plugin",
"template_data": [
{
"key": "ValidateMissingLayers",
"label": "ValidateMissingLayers"
}
]
}
]
},
{
"type": "schema",
"name": "schema_publish_gui_filter"
}
]
}

View file

@ -42,6 +42,11 @@
"key": "enabled",
"label": "Enabled"
},
{
"type": "boolean",
"key": "auto_stop",
"label": "Auto stop timer"
},
{
"type": "number",
"decimal": 2,
@ -176,20 +181,6 @@
"label": "Enabled"
}
]
},
{
"type": "dict",
"key": "idle_manager",
"label": "Idle Manager",
"collapsible": true,
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
}
]
}
]
}

View file

@ -5,28 +5,16 @@ class LocalGeneralWidgets(QtWidgets.QWidget):
def __init__(self, parent):
super(LocalGeneralWidgets, self).__init__(parent)
local_site_name_input = QtWidgets.QLineEdit(self)
layout = QtWidgets.QFormLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addRow("Local site label", local_site_name_input)
self.local_site_name_input = local_site_name_input
def update_local_settings(self, value):
site_label = ""
if value:
site_label = value.get("site_label", site_label)
self.local_site_name_input.setText(site_label)
return
# RETURNING EARLY TO HIDE WIDGET WITHOUT CONTENT
def settings_value(self):
# Add changed
# If these have changed then
output = {}
local_site_name = self.local_site_name_input.text()
if local_site_name:
output["site_label"] = local_site_name
# Do not return output yet since we don't have mechanism to save or
# load these data through api calls
# TEMPORARILY EMPTY AS THERE IS NOTHING TO PUT HERE
return output

View file

@ -80,6 +80,7 @@ class LocalSettingsWidget(QtWidgets.QWidget):
general_widget = LocalGeneralWidgets(general_content)
general_layout.addWidget(general_widget)
general_expand_widget.hide()
self.main_layout.addWidget(general_expand_widget)
@ -126,9 +127,9 @@ class LocalSettingsWidget(QtWidgets.QWidget):
self.system_settings.reset()
self.project_settings.reset()
self.general_widget.update_local_settings(
value.get(LOCAL_GENERAL_KEY)
)
# self.general_widget.update_local_settings(
# value.get(LOCAL_GENERAL_KEY)
# )
self.app_widget.update_local_settings(
value.get(LOCAL_APPS_KEY)
)
@ -138,9 +139,9 @@ class LocalSettingsWidget(QtWidgets.QWidget):
def settings_value(self):
output = {}
general_value = self.general_widget.settings_value()
if general_value:
output[LOCAL_GENERAL_KEY] = general_value
# general_value = self.general_widget.settings_value()
# if general_value:
# output[LOCAL_GENERAL_KEY] = general_value
app_value = self.app_widget.settings_value()
if app_value:

View file

@ -363,7 +363,7 @@ class PypeInfoWidget(QtWidgets.QWidget):
"version_value": "OpenPype version:",
"executable": "OpenPype executable:",
"pype_root": "OpenPype location:",
"mongo_url": "OpenPype Mongo URL:"
"mongo_url": "OpenPype Mongo URL:"
}
# Prepare keys order
keys_order = ["version_value", "executable", "pype_root", "mongo_url"]