Merge branch 'develop' into feature/895-add-option-to-define-paht-to-workfile-template

This commit is contained in:
Jakub Jezek 2021-05-27 12:13:16 +02:00
commit d2ec91ec0e
No known key found for this signature in database
GPG key ID: D8548FBF690B100A
59 changed files with 2804 additions and 3167 deletions

View file

@ -203,6 +203,12 @@ class OpenPypeVersion(semver.VersionInfo):
openpype_version.staging = True
return openpype_version
def __hash__(self):
if self.path:
return hash(self.path)
else:
return hash(str(self))
class BootstrapRepos:
"""Class for bootstrapping local OpenPype installation.
@ -650,6 +656,9 @@ class BootstrapRepos:
v for v in openpype_versions if v.path.suffix != ".zip"
]
# remove duplicates
openpype_versions = list(set(openpype_versions))
return openpype_versions
def process_entered_location(self, location: str) -> Union[Path, None]:

View file

@ -0,0 +1,34 @@
import os
from openpype.lib import PreLaunchHook
class LaunchWithTerminal(PreLaunchHook):
"""Mac specific pre arguments for application.
Mac applications should be launched using "open" argument which is internal
callbacks to open executable. We also add argument "-a" to tell it's
application open. This is used only for executables ending with ".app". It
is expected that these executables lead to app packages.
"""
order = 1000
platforms = ["darwin"]
def execute(self):
executable = str(self.launch_context.executable)
# Skip executables not ending with ".app" or that are not folder
if not executable.endswith(".app") or not os.path.isdir(executable):
return
# Check if first argument match executable path
# - Few applications are not executed directly but through OpenPype
# process (Photoshop, AfterEffects, Harmony, ...). These should not
# use `open`.
if self.launch_context.launch_args[0] != executable:
return
# Tell `open` to pass arguments if there are any
if len(self.launch_context.launch_args) > 1:
self.launch_context.launch_args.insert(1, "--args")
# Prepend open arguments
self.launch_context.launch_args.insert(0, ["open", "-a"])

View file

@ -1,25 +0,0 @@
import bpy
from avalon import api, blender
import openpype.hosts.blender.api.plugin
class CreateSetDress(openpype.hosts.blender.api.plugin.Creator):
"""A grouped package of loaded content"""
name = "setdressMain"
label = "Set Dress"
family = "setdress"
icon = "cubes"
defaults = ["Main", "Anim"]
def process(self):
asset = self.data["asset"]
subset = self.data["subset"]
name = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
collection = bpy.data.collections.new(name=name)
bpy.context.scene.collection.children.link(collection)
self.data['task'] = api.Session.get('AVALON_TASK')
blender.lib.imprint(collection, self.data)
return collection

View file

@ -25,9 +25,6 @@ class BlendLayoutLoader(plugin.AssetLoader):
icon = "code-fork"
color = "orange"
animation_creator_name = "CreateAnimation"
setdress_creator_name = "CreateSetDress"
def _remove(self, objects, obj_container):
for obj in list(objects):
if obj.type == 'ARMATURE':
@ -293,7 +290,6 @@ class UnrealLayoutLoader(plugin.AssetLoader):
color = "orange"
animation_creator_name = "CreateAnimation"
setdress_creator_name = "CreateSetDress"
def _remove_objects(self, objects):
for obj in list(objects):
@ -383,7 +379,7 @@ class UnrealLayoutLoader(plugin.AssetLoader):
def _process(
self, libpath, layout_container, container_name, representation,
actions, parent
actions, parent_collection
):
with open(libpath, "r") as fp:
data = json.load(fp)
@ -392,6 +388,11 @@ class UnrealLayoutLoader(plugin.AssetLoader):
layout_collection = bpy.data.collections.new(container_name)
scene.collection.children.link(layout_collection)
parent = parent_collection
if parent is None:
parent = scene.collection
all_loaders = api.discover(api.Loader)
avalon_container = bpy.data.collections.get(
@ -516,23 +517,9 @@ class UnrealLayoutLoader(plugin.AssetLoader):
container_metadata["libpath"] = libpath
container_metadata["lib_container"] = lib_container
# Create a setdress subset to contain all the animation for all
# the rigs in the layout
creator_plugin = get_creator_by_name(self.setdress_creator_name)
if not creator_plugin:
raise ValueError("Creator plugin \"{}\" was not found.".format(
self.setdress_creator_name
))
parent = api.create(
creator_plugin,
name="animation",
asset=api.Session["AVALON_ASSET"],
options={"useSelection": True},
data={"dependencies": str(context["representation"]["_id"])})
layout_collection = self._process(
libpath, layout_container, container_name,
str(context["representation"]["_id"]), None, parent)
str(context["representation"]["_id"]), None, None)
container_metadata["obj_container"] = layout_collection

View file

@ -107,6 +107,9 @@ class BlendRigLoader(plugin.AssetLoader):
if action is not None:
local_obj.animation_data.action = action
elif local_obj.animation_data.action is not None:
plugin.prepare_data(
local_obj.animation_data.action, collection_name)
# Set link the drivers to the local object
if local_obj.data.animation_data:

View file

@ -1,61 +0,0 @@
import os
import json
import openpype.api
import pyblish.api
import bpy
class ExtractSetDress(openpype.api.Extractor):
"""Extract setdress."""
label = "Extract SetDress"
hosts = ["blender"]
families = ["setdress"]
optional = True
order = pyblish.api.ExtractorOrder + 0.1
def process(self, instance):
stagingdir = self.staging_dir(instance)
json_data = []
for i in instance.context:
collection = i.data.get("name")
container = None
for obj in bpy.data.collections[collection].objects:
if obj.type == "ARMATURE":
container_name = obj.get("avalon").get("container_name")
container = bpy.data.collections[container_name]
if container:
json_dict = {
"subset": i.data.get("subset"),
"container": container.name,
}
json_dict["instance_name"] = container.get("avalon").get(
"instance_name"
)
json_data.append(json_dict)
if "representations" not in instance.data:
instance.data["representations"] = []
json_filename = f"{instance.name}.json"
json_path = os.path.join(stagingdir, json_filename)
with open(json_path, "w+") as file:
json.dump(json_data, fp=file, indent=2)
json_representation = {
"name": "json",
"ext": "json",
"files": json_filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(json_representation)
self.log.info(
"Extracted instance '{}' to: {}".format(instance.name,
json_representation)
)

View file

@ -1,4 +1,5 @@
import os
import json
import openpype.api
@ -121,6 +122,25 @@ class ExtractAnimationFBX(openpype.api.Extractor):
pair[1].user_clear()
bpy.data.actions.remove(pair[1])
json_filename = f"{instance.name}.json"
json_path = os.path.join(stagingdir, json_filename)
json_dict = {}
collection = instance.data.get("name")
container = None
for obj in bpy.data.collections[collection].objects:
if obj.type == "ARMATURE":
container_name = obj.get("avalon").get("container_name")
container = bpy.data.collections[container_name]
if container:
json_dict = {
"instance_name": container.get("avalon").get("instance_name")
}
with open(json_path, "w+") as file:
json.dump(json_dict, fp=file, indent=2)
if "representations" not in instance.data:
instance.data["representations"] = []
@ -130,7 +150,15 @@ class ExtractAnimationFBX(openpype.api.Extractor):
'files': fbx_filename,
"stagingDir": stagingdir,
}
json_representation = {
'name': 'json',
'ext': 'json',
'files': json_filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(fbx_representation)
instance.data["representations"].append(json_representation)
self.log.info("Extracted instance '{}' to: {}".format(
instance.name, fbx_representation))

View file

@ -214,7 +214,9 @@ def get_track_items(
# add all if no track_type is defined
return_list.append(track_item)
return return_list
# return output list but make sure all items are TrackItems
return [_i for _i in return_list
if type(_i) == hiero.core.TrackItem]
def get_track_item_pype_tag(track_item):

View file

@ -52,10 +52,11 @@ class ExtractClipEffects(openpype.api.Extractor):
instance.data["representations"] = list()
transfer_data = [
"handleStart", "handleEnd", "sourceIn", "sourceOut",
"frameStart", "frameEnd", "sourceInH", "sourceOutH",
"clipIn", "clipOut", "clipInH", "clipOutH", "asset", "track",
"version"
"handleStart", "handleEnd",
"sourceStart", "sourceStartH", "sourceEnd", "sourceEndH",
"frameStart", "frameEnd",
"clipIn", "clipOut", "clipInH", "clipOutH",
"asset", "version"
]
# pass data to version

View file

@ -5,7 +5,7 @@ import pyblish.api
class PreCollectClipEffects(pyblish.api.InstancePlugin):
"""Collect soft effects instances."""
order = pyblish.api.CollectorOrder - 0.508
order = pyblish.api.CollectorOrder - 0.579
label = "Pre-collect Clip Effects Instances"
families = ["clip"]
@ -24,7 +24,8 @@ class PreCollectClipEffects(pyblish.api.InstancePlugin):
self.clip_in_h = self.clip_in - self.handle_start
self.clip_out_h = self.clip_out + self.handle_end
track = instance.data["trackItem"]
track_item = instance.data["item"]
track = track_item.parent()
track_index = track.trackIndex()
tracks_effect_items = instance.context.data.get("tracksEffectItems")
clip_effect_items = instance.data.get("clipEffectItems")
@ -112,7 +113,12 @@ class PreCollectClipEffects(pyblish.api.InstancePlugin):
node = sitem.node()
node_serialized = self.node_serialisation(node)
node_name = sitem.name()
node_class = re.sub(r"\d+", "", node_name)
if "_" in node_name:
node_class = re.sub(r"(?:_)[_0-9]+", "", node_name) # more numbers
else:
node_class = re.sub(r"\d+", "", node_name) # one number
# collect timelineIn/Out
effect_t_in = int(sitem.timelineIn())
effect_t_out = int(sitem.timelineOut())
@ -121,6 +127,7 @@ class PreCollectClipEffects(pyblish.api.InstancePlugin):
return
self.log.debug("node_name: `{}`".format(node_name))
self.log.debug("node_class: `{}`".format(node_class))
return {node_name: {
"class": node_class,

View file

@ -2,6 +2,9 @@ import pyblish
import openpype
from openpype.hosts.hiero import api as phiero
from openpype.hosts.hiero.otio import hiero_export
import hiero
from compiler.ast import flatten
# # developer reload modules
from pprint import pformat
@ -14,18 +17,40 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
label = "Precollect Instances"
hosts = ["hiero"]
audio_track_items = []
def process(self, context):
otio_timeline = context.data["otioTimeline"]
self.otio_timeline = context.data["otioTimeline"]
selected_timeline_items = phiero.get_track_items(
selected=True, check_enabled=True, check_tagged=True)
selected=True, check_tagged=True, check_enabled=True)
# only return enabled track items
if not selected_timeline_items:
selected_timeline_items = phiero.get_track_items(
check_enabled=True, check_tagged=True)
self.log.info(
"Processing enabled track items: {}".format(
selected_timeline_items))
# add all tracks subtreck effect items to context
all_tracks = hiero.ui.activeSequence().videoTracks()
tracks_effect_items = self.collect_sub_track_items(all_tracks)
context.data["tracksEffectItems"] = tracks_effect_items
# process all sellected timeline track items
for track_item in selected_timeline_items:
data = {}
clip_name = track_item.name()
source_clip = track_item.source()
# get clips subtracks and anotations
annotations = self.clip_annotations(source_clip)
subtracks = self.clip_subtrack(track_item)
self.log.debug("Annotations: {}".format(annotations))
self.log.debug(">> Subtracks: {}".format(subtracks))
# get openpype tag data
tag_data = phiero.get_track_item_pype_data(track_item)
@ -76,12 +101,15 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
"item": track_item,
"families": families,
"publish": tag_data["publish"],
"fps": context.data["fps"]
"fps": context.data["fps"],
# clip's effect
"clipEffectItems": subtracks,
"clipAnnotations": annotations
})
# otio clip data
otio_data = self.get_otio_clip_instance_data(
otio_timeline, track_item) or {}
otio_data = self.get_otio_clip_instance_data(track_item) or {}
self.log.debug("__ otio_data: {}".format(pformat(otio_data)))
data.update(otio_data)
self.log.debug("__ data: {}".format(pformat(data)))
@ -185,6 +213,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
item = data.get("item")
clip_name = item.name()
# test if any audio clips
if not self.test_any_audio(item):
return
asset = data["asset"]
subset = "audioMain"
@ -215,7 +247,28 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
self.log.debug(
"_ instance.data: {}".format(pformat(instance.data)))
def get_otio_clip_instance_data(self, otio_timeline, track_item):
def test_any_audio(self, track_item):
# collect all audio tracks to class variable
if not self.audio_track_items:
for otio_clip in self.otio_timeline.each_clip():
if otio_clip.parent().kind != "Audio":
continue
self.audio_track_items.append(otio_clip)
# get track item timeline range
timeline_range = self.create_otio_time_range_from_timeline_item_data(
track_item)
# loop trough audio track items and search for overlaping clip
for otio_audio in self.audio_track_items:
parent_range = otio_audio.range_in_parent()
# if any overaling clip found then return True
if openpype.lib.is_overlapping_otio_ranges(
parent_range, timeline_range, strict=False):
return True
def get_otio_clip_instance_data(self, track_item):
"""
Return otio objects for timeline, track and clip
@ -231,7 +284,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
ti_track_name = track_item.parent().name()
timeline_range = self.create_otio_time_range_from_timeline_item_data(
track_item)
for otio_clip in otio_timeline.each_clip():
for otio_clip in self.otio_timeline.each_clip():
track_name = otio_clip.parent().name
parent_range = otio_clip.range_in_parent()
if ti_track_name not in track_name:
@ -258,3 +311,76 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
return hiero_export.create_otio_time_range(
frame_start, frame_duration, fps)
@staticmethod
def collect_sub_track_items(tracks):
"""
Returns dictionary with track index as key and list of subtracks
"""
# collect all subtrack items
sub_track_items = {}
for track in tracks:
items = track.items()
# skip if no clips on track > need track with effect only
if items:
continue
# skip all disabled tracks
if not track.isEnabled():
continue
track_index = track.trackIndex()
_sub_track_items = flatten(track.subTrackItems())
# continue only if any subtrack items are collected
if len(_sub_track_items) < 1:
continue
enabled_sti = []
# loop all found subtrack items and check if they are enabled
for _sti in _sub_track_items:
# checking if not enabled
if not _sti.isEnabled():
continue
if isinstance(_sti, hiero.core.Annotation):
continue
# collect the subtrack item
enabled_sti.append(_sti)
# continue only if any subtrack items are collected
if len(enabled_sti) < 1:
continue
# add collection of subtrackitems to dict
sub_track_items[track_index] = enabled_sti
return sub_track_items
@staticmethod
def clip_annotations(clip):
"""
Returns list of Clip's hiero.core.Annotation
"""
annotations = []
subTrackItems = flatten(clip.subTrackItems())
annotations += [item for item in subTrackItems if isinstance(
item, hiero.core.Annotation)]
return annotations
@staticmethod
def clip_subtrack(clip):
"""
Returns list of Clip's hiero.core.SubTrackItem
"""
subtracks = []
subTrackItems = flatten(clip.parent().subTrackItems())
for item in subTrackItems:
# avoid all anotation
if isinstance(item, hiero.core.Annotation):
continue
# # avoid all not anaibled
if not item.isEnabled():
continue
subtracks.append(item)
return subtracks

View file

@ -75,10 +75,26 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
"activeProject": project,
"otioTimeline": otio_timeline,
"currentFile": curent_file,
"fps": fps,
"colorspace": self.get_colorspace(project),
"fps": fps
}
context.data.update(context_data)
self.log.info("Creating instance: {}".format(instance))
self.log.debug("__ instance.data: {}".format(pformat(instance.data)))
self.log.debug("__ context_data: {}".format(pformat(context_data)))
def get_colorspace(self, project):
# get workfile's colorspace properties
return {
"useOCIOEnvironmentOverride": project.useOCIOEnvironmentOverride(),
"lutSetting16Bit": project.lutSetting16Bit(),
"lutSetting8Bit": project.lutSetting8Bit(),
"lutSettingFloat": project.lutSettingFloat(),
"lutSettingLog": project.lutSettingLog(),
"lutSettingViewer": project.lutSettingViewer(),
"lutSettingWorkingSpace": project.lutSettingWorkingSpace(),
"lutUseOCIOForExport": project.lutUseOCIOForExport(),
"ocioConfigName": project.ocioConfigName(),
"ocioConfigPath": project.ocioConfigPath()
}

View file

@ -1060,7 +1060,7 @@ class WorkfileSettings(object):
# replace reset resolution from avalon core to pype's
self.reset_frame_range_handles()
# add colorspace menu item
# self.set_colorspace()
self.set_colorspace()
def set_favorites(self):
work_dir = os.getenv("AVALON_WORKDIR")

View file

@ -4,18 +4,19 @@ import json
from collections import OrderedDict
class LoadLuts(api.Loader):
class LoadEffects(api.Loader):
"""Loading colorspace soft effect exported from nukestudio"""
representations = ["lutJson"]
families = ["lut"]
representations = ["effectJson"]
families = ["effect"]
label = "Load Luts - nodes"
label = "Load Effects - nodes"
order = 0
icon = "cc"
color = style.colors.light
ignore_attr = ["useLifetime"]
def load(self, context, name, namespace, data):
"""
Loading function to get the soft effects to particular read node
@ -66,15 +67,15 @@ class LoadLuts(api.Loader):
for key, value in json.load(f).iteritems()}
# get correct order of nodes by positions on track and subtrack
nodes_order = self.reorder_nodes(json_f["effects"])
nodes_order = self.reorder_nodes(json_f)
# adding nodes to node graph
# just in case we are in group lets jump out of it
nuke.endGroup()
GN = nuke.createNode("Group")
GN["name"].setValue(object_name)
GN = nuke.createNode(
"Group",
"name {}_1".format(object_name))
# adding content to the group node
with GN:
@ -186,7 +187,7 @@ class LoadLuts(api.Loader):
for key, value in json.load(f).iteritems()}
# get correct order of nodes by positions on track and subtrack
nodes_order = self.reorder_nodes(json_f["effects"])
nodes_order = self.reorder_nodes(json_f)
# adding nodes to node graph
# just in case we are in group lets jump out of it
@ -266,7 +267,11 @@ class LoadLuts(api.Loader):
None: if nothing found
"""
search_name = "{0}_{1}".format(asset, subset)
node = [n for n in nuke.allNodes() if search_name in n["name"].value()]
node = [
n for n in nuke.allNodes(filter="Read")
if search_name in n["file"].value()
]
if len(node) > 0:
rn = node[0]
else:
@ -286,8 +291,10 @@ class LoadLuts(api.Loader):
def reorder_nodes(self, data):
new_order = OrderedDict()
trackNums = [v["trackIndex"] for k, v in data.items()]
subTrackNums = [v["subTrackIndex"] for k, v in data.items()]
trackNums = [v["trackIndex"] for k, v in data.items()
if isinstance(v, dict)]
subTrackNums = [v["subTrackIndex"] for k, v in data.items()
if isinstance(v, dict)]
for trackIndex in range(
min(trackNums), max(trackNums) + 1):
@ -300,6 +307,7 @@ class LoadLuts(api.Loader):
def get_item(self, data, trackIndex, subTrackIndex):
return {key: val for key, val in data.items()
if isinstance(val, dict)
if subTrackIndex == val["subTrackIndex"]
if trackIndex == val["trackIndex"]}

View file

@ -5,13 +5,13 @@ from collections import OrderedDict
from openpype.hosts.nuke.api import lib
class LoadLutsInputProcess(api.Loader):
class LoadEffectsInputProcess(api.Loader):
"""Loading colorspace soft effect exported from nukestudio"""
representations = ["lutJson"]
families = ["lut"]
representations = ["effectJson"]
families = ["effect"]
label = "Load Luts - Input Process"
label = "Load Effects - Input Process"
order = 0
icon = "eye"
color = style.colors.alert
@ -67,15 +67,15 @@ class LoadLutsInputProcess(api.Loader):
for key, value in json.load(f).iteritems()}
# get correct order of nodes by positions on track and subtrack
nodes_order = self.reorder_nodes(json_f["effects"])
nodes_order = self.reorder_nodes(json_f)
# adding nodes to node graph
# just in case we are in group lets jump out of it
nuke.endGroup()
GN = nuke.createNode("Group")
GN["name"].setValue(object_name)
GN = nuke.createNode(
"Group",
"name {}_1".format(object_name))
# adding content to the group node
with GN:
@ -190,7 +190,7 @@ class LoadLutsInputProcess(api.Loader):
for key, value in json.load(f).iteritems()}
# get correct order of nodes by positions on track and subtrack
nodes_order = self.reorder_nodes(json_f["effects"])
nodes_order = self.reorder_nodes(json_f)
# adding nodes to node graph
# just in case we are in group lets jump out of it
@ -304,8 +304,10 @@ class LoadLutsInputProcess(api.Loader):
def reorder_nodes(self, data):
new_order = OrderedDict()
trackNums = [v["trackIndex"] for k, v in data.items()]
subTrackNums = [v["subTrackIndex"] for k, v in data.items()]
trackNums = [v["trackIndex"] for k, v in data.items()
if isinstance(v, dict)]
subTrackNums = [v["subTrackIndex"] for k, v in data.items()
if isinstance(v, dict)]
for trackIndex in range(
min(trackNums), max(trackNums) + 1):
@ -318,6 +320,7 @@ class LoadLutsInputProcess(api.Loader):
def get_item(self, data, trackIndex, subTrackIndex):
return {key: val for key, val in data.items()
if isinstance(val, dict)
if subTrackIndex == val["subTrackIndex"]
if trackIndex == val["trackIndex"]}

View file

@ -1,6 +1,7 @@
from openpype.hosts.nuke.api.lib import (
on_script_load,
check_inventory_versions
check_inventory_versions,
WorkfileSettings
)
import nuke
@ -9,8 +10,14 @@ from openpype.api import Logger
log = Logger().get_logger(__name__)
nuke.addOnScriptSave(on_script_load)
# fix ffmpeg settings on script
nuke.addOnScriptLoad(on_script_load)
# set checker for last versions on loaded containers
nuke.addOnScriptLoad(check_inventory_versions)
nuke.addOnScriptSave(check_inventory_versions)
# # set apply all workfile settings on script load and save
nuke.addOnScriptLoad(WorkfileSettings().set_context_settings)
log.info('Automatic syncing of write file knob to script version')

View file

@ -19,6 +19,10 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
def on_instance_toggle(instance, old_value, new_value):
# Review may not have real instance in wokrfile metadata
if not instance.data.get("uuid"):
return
instance_id = instance.data["uuid"]
found_idx = None
current_instances = pipeline.list_instances()

View file

@ -1,19 +0,0 @@
from avalon.tvpaint import pipeline
from openpype.hosts.tvpaint.api import plugin
class CreateReview(plugin.Creator):
"""Review for global review of all layers."""
name = "review"
label = "Review"
family = "review"
icon = "cube"
defaults = ["Main"]
def process(self):
instances = pipeline.list_instances()
for instance in instances:
if instance["family"] == self.family:
self.log.info("Review family is already Created.")
return
super(CreateReview, self).process()

View file

@ -17,6 +17,20 @@ class CollectInstances(pyblish.api.ContextPlugin):
json.dumps(workfile_instances, indent=4)
))
# 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":
review_instance_exist = True
break
# Fake review instance if review was not found in metadata families
if not review_instance_exist:
workfile_instances.append(
self._create_review_instance_data(context)
)
for instance_data in workfile_instances:
instance_data["fps"] = context.data["sceneFps"]
@ -90,6 +104,16 @@ class CollectInstances(pyblish.api.ContextPlugin):
instance, json.dumps(instance.data, indent=4)
))
def _create_review_instance_data(self, context):
"""Fake review instance data."""
return {
"family": "review",
"asset": context.data["workfile_context"]["asset"],
# Dummy subset name
"subset": "reviewMain"
}
def create_render_layer_instance(self, context, instance_data):
name = instance_data["name"]
# Change label

View file

@ -1,4 +1,5 @@
import os
import json
from avalon import api, pipeline
from avalon.unreal import lib
@ -61,10 +62,16 @@ class AnimationFBXLoader(api.Loader):
task = unreal.AssetImportTask()
task.options = unreal.FbxImportUI()
# If there are no options, the process cannot be automated
if options:
libpath = self.fname.replace("fbx", "json")
with open(libpath, "r") as fp:
data = json.load(fp)
instance_name = data.get("instance_name")
if instance_name:
automated = True
actor_name = 'PersistentLevel.' + options.get('instance_name')
actor_name = 'PersistentLevel.' + instance_name
actor = unreal.EditorLevelLibrary.get_actor_reference(actor_name)
skeleton = actor.skeletal_mesh_component.skeletal_mesh.skeleton
task.options.set_editor_property('skeleton', skeleton)
@ -81,16 +88,31 @@ class AnimationFBXLoader(api.Loader):
# set import options here
task.options.set_editor_property(
'automated_import_should_detect_type', True)
'automated_import_should_detect_type', False)
task.options.set_editor_property(
'original_import_type', unreal.FBXImportType.FBXIT_ANIMATION)
'original_import_type', unreal.FBXImportType.FBXIT_SKELETAL_MESH)
task.options.set_editor_property(
'mesh_type_to_import', unreal.FBXImportType.FBXIT_ANIMATION)
task.options.set_editor_property('import_mesh', False)
task.options.set_editor_property('import_animations', True)
task.options.set_editor_property('override_full_name', True)
task.options.skeletal_mesh_import_data.set_editor_property(
'import_content_type',
unreal.FBXImportContentType.FBXICT_SKINNING_WEIGHTS
task.options.anim_sequence_import_data.set_editor_property(
'animation_length',
unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME
)
task.options.anim_sequence_import_data.set_editor_property(
'import_meshes_in_bone_hierarchy', False)
task.options.anim_sequence_import_data.set_editor_property(
'use_default_sample_rate', True)
task.options.anim_sequence_import_data.set_editor_property(
'import_custom_attribute', True)
task.options.anim_sequence_import_data.set_editor_property(
'import_bone_tracks', True)
task.options.anim_sequence_import_data.set_editor_property(
'remove_redundant_keys', True)
task.options.anim_sequence_import_data.set_editor_property(
'convert_scene', True)
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])

View file

@ -1,127 +0,0 @@
import json
from avalon import api
import unreal
class AnimationCollectionLoader(api.Loader):
"""Load Unreal SkeletalMesh from FBX"""
families = ["setdress"]
representations = ["json"]
label = "Load Animation Collection"
icon = "cube"
color = "orange"
def load(self, context, name, namespace, options):
from avalon import api, pipeline
from avalon.unreal import lib
from avalon.unreal import pipeline as unreal_pipeline
import unreal
# Create directory for asset and avalon container
root = "/Game/Avalon/Assets"
asset = context.get('asset').get('name')
suffix = "_CON"
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_dir, container_name = tools.create_unique_asset_name(
"{}/{}".format(root, asset), suffix="")
container_name += suffix
unreal.EditorAssetLibrary.make_directory(asset_dir)
libpath = self.fname
with open(libpath, "r") as fp:
data = json.load(fp)
all_loaders = api.discover(api.Loader)
for element in data:
reference = element.get('_id')
loaders = api.loaders_from_representation(all_loaders, reference)
loader = None
for l in loaders:
if l.__name__ == "AnimationFBXLoader":
loader = l
break
if not loader:
continue
instance_name = element.get('instance_name')
api.load(
loader,
reference,
namespace=instance_name,
options=element
)
# Create Asset Container
lib.create_avalon_container(
container=container_name, path=asset_dir)
data = {
"schema": "openpype:container-2.0",
"id": pipeline.AVALON_CONTAINER_ID,
"asset": asset,
"namespace": asset_dir,
"container_name": container_name,
"loader": str(self.__class__.__name__),
"representation": context["representation"]["_id"],
"parent": context["representation"]["parent"],
"family": context["representation"]["context"]["family"]
}
unreal_pipeline.imprint(
"{}/{}".format(asset_dir, container_name), data)
asset_content = unreal.EditorAssetLibrary.list_assets(
asset_dir, recursive=True, include_folder=True
)
return asset_content
def update(self, container, representation):
from avalon import api, io
from avalon.unreal import pipeline
source_path = api.get_representation_path(representation)
with open(source_path, "r") as fp:
data = json.load(fp)
animation_containers = [
i for i in pipeline.ls() if
i.get('asset') == container.get('asset') and
i.get('family') == 'animation']
for element in data:
new_version = io.find_one({"_id": io.ObjectId(element.get('_id'))})
new_version_number = new_version.get('context').get('version')
anim_container = None
for i in animation_containers:
if i.get('container_name') == (element.get('subset') + "_CON"):
anim_container = i
break
if not anim_container:
continue
api.update(anim_container, new_version_number)
container_path = "{}/{}".format(container["namespace"],
container["objectName"])
# update metadata
pipeline.imprint(
container_path,
{
"representation": str(representation["_id"]),
"parent": str(representation["parent"])
})
def remove(self, container):
unreal.EditorAssetLibrary.delete_directory(container["namespace"])

View file

@ -440,7 +440,20 @@ class EnvironmentTool:
class ApplicationExecutable:
"""Representation of executable loaded from settings."""
def __init__(self, executable):
# On MacOS check if exists path to executable when ends with `.app`
# - it is common that path will lead to "/Applications/Blender" but
# real path is "/Applications/Blender.app"
if (
platform.system().lower() == "darwin"
and not os.path.exists(executable)
):
_executable = executable + ".app"
if os.path.exists(_executable):
executable = _executable
self.executable_path = executable
def __str__(self):
@ -1177,17 +1190,23 @@ def prepare_context_environments(data):
try:
workdir = get_workdir_with_workdir_data(workdir_data, anatomy)
if not os.path.exists(workdir):
log.debug(
"Creating workdir folder: \"{}\"".format(workdir)
)
os.makedirs(workdir)
except Exception as exc:
raise ApplicationLaunchFailed(
"Error in anatomy.format: {}".format(str(exc))
)
if not os.path.exists(workdir):
log.debug(
"Creating workdir folder: \"{}\"".format(workdir)
)
try:
os.makedirs(workdir)
except Exception as exc:
raise ApplicationLaunchFailed(
"Couldn't create workdir because: {}".format(str(exc))
)
context_env = {
"AVALON_PROJECT": project_doc["name"],
"AVALON_ASSET": asset_doc["name"],

303
openpype/lib/delivery.py Normal file
View file

@ -0,0 +1,303 @@
"""Functions useful for delivery action or loader"""
import os
import shutil
import clique
import collections
def collect_frames(files):
"""
Returns dict of source path and its frame, if from sequence
Uses clique as most precise solution
Args:
files(list): list of source paths
Returns:
(dict): {'/asset/subset_v001.0001.png': '0001', ....}
"""
collections, remainder = clique.assemble(files)
sources_and_frames = {}
if collections:
for collection in collections:
src_head = collection.head
src_tail = collection.tail
for index in collection.indexes:
src_frame = collection.format("{padding}") % index
src_file_name = "{}{}{}".format(src_head, src_frame,
src_tail)
sources_and_frames[src_file_name] = src_frame
else:
sources_and_frames[remainder.pop()] = None
return sources_and_frames
def sizeof_fmt(num, suffix='B'):
"""Returns formatted string with size in appropriate unit"""
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
if abs(num) < 1024.0:
return "%3.1f%s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f%s%s" % (num, 'Yi', suffix)
def path_from_represenation(representation, anatomy):
from avalon import pipeline # safer importing
try:
template = representation["data"]["template"]
except KeyError:
return None
try:
context = representation["context"]
context["root"] = anatomy.roots
path = pipeline.format_template_with_optional_keys(
context, template
)
except KeyError:
# Template references unavailable data
return None
return os.path.normpath(path)
def copy_file(src_path, dst_path):
"""Hardlink file if possible(to save space), copy if not"""
from avalon.vendor import filelink # safer importing
if os.path.exists(dst_path):
return
try:
filelink.create(
src_path,
dst_path,
filelink.HARDLINK
)
except OSError:
shutil.copyfile(src_path, dst_path)
def get_format_dict(anatomy, location_path):
"""Returns replaced root values from user provider value.
Args:
anatomy (Anatomy)
location_path (str): user provided value
Returns:
(dict): prepared for formatting of a template
"""
format_dict = {}
if location_path:
location_path = location_path.replace("\\", "/")
root_names = anatomy.root_names_from_templates(
anatomy.templates["delivery"]
)
if root_names is None:
format_dict["root"] = location_path
else:
format_dict["root"] = {}
for name in root_names:
format_dict["root"][name] = location_path
return format_dict
def check_destination_path(repre_id,
anatomy, anatomy_data,
datetime_data, template_name):
""" Try to create destination path based on 'template_name'.
In the case that path cannot be filled, template contains unmatched
keys, provide error message to filter out repre later.
Args:
anatomy (Anatomy)
anatomy_data (dict): context to fill anatomy
datetime_data (dict): values with actual date
template_name (str): to pick correct delivery template
Returns:
(collections.defauldict): {"TYPE_OF_ERROR":"ERROR_DETAIL"}
"""
anatomy_data.update(datetime_data)
anatomy_filled = anatomy.format_all(anatomy_data)
dest_path = anatomy_filled["delivery"][template_name]
report_items = collections.defaultdict(list)
sub_msg = None
if not dest_path.solved:
msg = (
"Missing keys in Representation's context"
" for anatomy template \"{}\"."
).format(template_name)
if dest_path.missing_keys:
keys = ", ".join(dest_path.missing_keys)
sub_msg = (
"Representation: {}<br>- Missing keys: \"{}\"<br>"
).format(repre_id, keys)
if dest_path.invalid_types:
items = []
for key, value in dest_path.invalid_types.items():
items.append("\"{}\" {}".format(key, str(value)))
keys = ", ".join(items)
sub_msg = (
"Representation: {}<br>"
"- Invalid value DataType: \"{}\"<br>"
).format(repre_id, keys)
report_items[msg].append(sub_msg)
return report_items
def process_single_file(
src_path, repre, anatomy, template_name, anatomy_data, format_dict,
report_items, log
):
"""Copy single file to calculated path based on template
Args:
src_path(str): path of source representation file
_repre (dict): full repre, used only in process_sequence, here only
as to share same signature
anatomy (Anatomy)
template_name (string): user selected delivery template name
anatomy_data (dict): data from repre to fill anatomy with
format_dict (dict): root dictionary with names and values
report_items (collections.defaultdict): to return error messages
log (Logger): for log printing
Returns:
(collections.defaultdict , int)
"""
if not os.path.exists(src_path):
msg = "{} doesn't exist for {}".format(src_path,
repre["_id"])
report_items["Source file was not found"].append(msg)
return report_items, 0
anatomy_filled = anatomy.format(anatomy_data)
if format_dict:
template_result = anatomy_filled["delivery"][template_name]
delivery_path = template_result.rootless.format(**format_dict)
else:
delivery_path = anatomy_filled["delivery"][template_name]
# context.representation could be .psd
delivery_path = delivery_path.replace("..", ".")
delivery_folder = os.path.dirname(delivery_path)
if not os.path.exists(delivery_folder):
os.makedirs(delivery_folder)
log.debug("Copying single: {} -> {}".format(src_path, delivery_path))
copy_file(src_path, delivery_path)
return report_items, 1
def process_sequence(
src_path, repre, anatomy, template_name, anatomy_data, format_dict,
report_items, log
):
""" For Pype2(mainly - works in 3 too) where representation might not
contain files.
Uses listing physical files (not 'files' on repre as a)might not be
present, b)might not be reliable for representation and copying them.
TODO Should be refactored when files are sufficient to drive all
representations.
Args:
src_path(str): path of source representation file
repre (dict): full representation
anatomy (Anatomy)
template_name (string): user selected delivery template name
anatomy_data (dict): data from repre to fill anatomy with
format_dict (dict): root dictionary with names and values
report_items (collections.defaultdict): to return error messages
log (Logger): for log printing
Returns:
(collections.defaultdict , int)
"""
if not os.path.exists(src_path):
msg = "{} doesn't exist for {}".format(src_path,
repre["_id"])
report_items["Source file was not found"].append(msg)
return report_items, 0
dir_path, file_name = os.path.split(str(src_path))
context = repre["context"]
ext = context.get("ext", context.get("representation"))
if not ext:
msg = "Source extension not found, cannot find collection"
report_items[msg].append(src_path)
log.warning("{} <{}>".format(msg, context))
return report_items, 0
ext = "." + ext
# context.representation could be .psd
ext = ext.replace("..", ".")
src_collections, remainder = clique.assemble(os.listdir(dir_path))
src_collection = None
for col in src_collections:
if col.tail != ext:
continue
src_collection = col
break
if src_collection is None:
msg = "Source collection of files was not found"
report_items[msg].append(src_path)
log.warning("{} <{}>".format(msg, src_path))
return report_items, 0
frame_indicator = "@####@"
anatomy_data["frame"] = frame_indicator
anatomy_filled = anatomy.format(anatomy_data)
if format_dict:
template_result = anatomy_filled["delivery"][template_name]
delivery_path = template_result.rootless.format(**format_dict)
else:
delivery_path = anatomy_filled["delivery"][template_name]
delivery_folder = os.path.dirname(delivery_path)
dst_head, dst_tail = delivery_path.split(frame_indicator)
dst_padding = src_collection.padding
dst_collection = clique.Collection(
head=dst_head,
tail=dst_tail,
padding=dst_padding
)
if not os.path.exists(delivery_folder):
os.makedirs(delivery_folder)
src_head = src_collection.head
src_tail = src_collection.tail
uploaded = 0
for index in src_collection.indexes:
src_padding = src_collection.format("{padding}") % index
src_file_name = "{}{}{}".format(src_head, src_padding, src_tail)
src = os.path.normpath(
os.path.join(dir_path, src_file_name)
)
dst_padding = dst_collection.format("{padding}") % index
dst = "{}{}{}".format(dst_head, dst_padding, dst_tail)
log.debug("Copying single: {} -> {}".format(src, dst))
copy_file(src, dst)
uploaded += 1
return report_items, uploaded

View file

@ -1101,9 +1101,6 @@ class SyncToAvalonEvent(BaseEvent):
# Parents, Hierarchy
ent_path_items = [ent["name"] for ent in ftrack_ent["link"]]
parents = ent_path_items[1:len(ent_path_items)-1:]
hierarchy = ""
if len(parents) > 0:
hierarchy = os.path.sep.join(parents)
# TODO logging
self.log.debug(
@ -1132,7 +1129,6 @@ class SyncToAvalonEvent(BaseEvent):
"ftrackId": ftrack_ent["id"],
"entityType": ftrack_ent.entity_type,
"parents": parents,
"hierarchy": hierarchy,
"tasks": {},
"visualParent": vis_par
}
@ -1975,14 +1971,9 @@ class SyncToAvalonEvent(BaseEvent):
if cur_par == parents:
continue
hierarchy = ""
if len(parents) > 0:
hierarchy = os.path.sep.join(parents)
if "data" not in self.updates[mongo_id]:
self.updates[mongo_id]["data"] = {}
self.updates[mongo_id]["data"]["parents"] = parents
self.updates[mongo_id]["data"]["hierarchy"] = hierarchy
# Skip custom attributes if didn't change
if not hier_cust_attrs_ids:

View file

@ -11,23 +11,28 @@ from avalon.api import AvalonMongoDB
class DeleteAssetSubset(BaseAction):
'''Edit meta data action.'''
#: Action identifier.
# Action identifier.
identifier = "delete.asset.subset"
#: Action label.
# Action label.
label = "Delete Asset/Subsets"
#: Action description.
# Action description.
description = "Removes from Avalon with all childs and asset from Ftrack"
icon = statics_icon("ftrack", "action_icons", "DeleteAsset.svg")
settings_key = "delete_asset_subset"
#: Db connection
dbcon = AvalonMongoDB()
# Db connection
dbcon = None
splitter = {"type": "label", "value": "---"}
action_data_by_id = {}
asset_prefix = "asset:"
subset_prefix = "subset:"
def __init__(self, *args, **kwargs):
self.dbcon = AvalonMongoDB()
super(DeleteAssetSubset, self).__init__(*args, **kwargs)
def discover(self, session, entities, event):
""" Validation """
task_ids = []
@ -446,7 +451,14 @@ class DeleteAssetSubset(BaseAction):
if len(assets_to_delete) > 0:
map_av_ftrack_id = spec_data["without_ftrack_id"]
# Prepare data when deleting whole avalon asset
avalon_assets = self.dbcon.find({"type": "asset"})
avalon_assets = self.dbcon.find(
{"type": "asset"},
{
"_id": 1,
"data.visualParent": 1,
"data.ftrackId": 1
}
)
avalon_assets_by_parent = collections.defaultdict(list)
for asset in avalon_assets:
asset_id = asset["_id"]
@ -537,11 +549,13 @@ class DeleteAssetSubset(BaseAction):
ftrack_proc_txt, ", ".join(ftrack_ids_to_delete)
))
ftrack_ents_to_delete = (
entities_by_link_len = (
self._filter_entities_to_delete(ftrack_ids_to_delete, session)
)
for entity in ftrack_ents_to_delete:
session.delete(entity)
for link_len in sorted(entities_by_link_len.keys(), reverse=True):
for entity in entities_by_link_len[link_len]:
session.delete(entity)
try:
session.commit()
except Exception:
@ -600,29 +614,11 @@ class DeleteAssetSubset(BaseAction):
joined_ids_to_delete
)
).all()
filtered = to_delete_entities[:]
while True:
changed = False
_filtered = filtered[:]
for entity in filtered:
entity_id = entity["id"]
entities_by_link_len = collections.defaultdict(list)
for entity in to_delete_entities:
entities_by_link_len[len(entity["link"])].append(entity)
for _entity in tuple(_filtered):
if entity_id == _entity["id"]:
continue
for _link in _entity["link"]:
if entity_id == _link["id"] and _entity in _filtered:
_filtered.remove(_entity)
changed = True
break
filtered = _filtered
if not changed:
break
return filtered
return entities_by_link_len
def report_handle(self, report_messages, project_name, event):
if not report_messages:

View file

@ -1,18 +1,20 @@
import os
import copy
import json
import shutil
import collections
import clique
from bson.objectid import ObjectId
from avalon import pipeline
from avalon.vendor import filelink
from openpype.api import Anatomy, config
from openpype.modules.ftrack.lib import BaseAction, statics_icon
from openpype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY
from openpype.lib.delivery import (
path_from_represenation,
get_format_dict,
check_destination_path,
process_single_file,
process_sequence
)
from avalon.api import AvalonMongoDB
@ -450,18 +452,7 @@ class Delivery(BaseAction):
anatomy = Anatomy(project_name)
format_dict = {}
if location_path:
location_path = location_path.replace("\\", "/")
root_names = anatomy.root_names_from_templates(
anatomy.templates["delivery"]
)
if root_names is None:
format_dict["root"] = location_path
else:
format_dict["root"] = {}
for name in root_names:
format_dict["root"][name] = location_path
format_dict = get_format_dict(anatomy, location_path)
datetime_data = config.get_datetime_data()
for repre in repres_to_deliver:
@ -471,41 +462,15 @@ class Delivery(BaseAction):
debug_msg += " with published path {}.".format(source_path)
self.log.debug(debug_msg)
# Get destination repre path
anatomy_data = copy.deepcopy(repre["context"])
anatomy_data.update(datetime_data)
anatomy_filled = anatomy.format_all(anatomy_data)
test_path = anatomy_filled["delivery"][anatomy_name]
repre_report_items = check_destination_path(repre["_id"],
anatomy,
anatomy_data,
datetime_data,
anatomy_name)
if not test_path.solved:
msg = (
"Missing keys in Representation's context"
" for anatomy template \"{}\"."
).format(anatomy_name)
if test_path.missing_keys:
keys = ", ".join(test_path.missing_keys)
sub_msg = (
"Representation: {}<br>- Missing keys: \"{}\"<br>"
).format(str(repre["_id"]), keys)
if test_path.invalid_types:
items = []
for key, value in test_path.invalid_types.items():
items.append("\"{}\" {}".format(key, str(value)))
keys = ", ".join(items)
sub_msg = (
"Representation: {}<br>"
"- Invalid value DataType: \"{}\"<br>"
).format(str(repre["_id"]), keys)
report_items[msg].append(sub_msg)
self.log.warning(
"{} Representation: \"{}\" Filled: <{}>".format(
msg, str(repre["_id"]), str(test_path)
)
)
if repre_report_items:
report_items.update(repre_report_items)
continue
# Get source repre path
@ -514,151 +479,28 @@ class Delivery(BaseAction):
if frame:
repre["context"]["frame"] = len(str(frame)) * "#"
repre_path = self.path_from_represenation(repre, anatomy)
repre_path = path_from_represenation(repre, anatomy)
# TODO add backup solution where root of path from component
# is repalced with root
# is replaced with root
args = (
repre_path,
repre,
anatomy,
anatomy_name,
anatomy_data,
format_dict,
report_items
report_items,
self.log
)
if not frame:
self.process_single_file(*args)
process_single_file(*args)
else:
self.process_sequence(*args)
process_sequence(*args)
return self.report(report_items)
def process_single_file(
self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict,
report_items
):
anatomy_filled = anatomy.format(anatomy_data)
if format_dict:
template_result = anatomy_filled["delivery"][anatomy_name]
delivery_path = template_result.rootless.format(**format_dict)
else:
delivery_path = anatomy_filled["delivery"][anatomy_name]
delivery_folder = os.path.dirname(delivery_path)
if not os.path.exists(delivery_folder):
os.makedirs(delivery_folder)
self.copy_file(repre_path, delivery_path)
def process_sequence(
self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict,
report_items
):
dir_path, file_name = os.path.split(str(repre_path))
base_name, ext = os.path.splitext(file_name)
file_name_items = None
if "#" in base_name:
file_name_items = [part for part in base_name.split("#") if part]
elif "%" in base_name:
file_name_items = base_name.split("%")
if not file_name_items:
msg = "Source file was not found"
report_items[msg].append(repre_path)
self.log.warning("{} <{}>".format(msg, repre_path))
return
src_collections, remainder = clique.assemble(os.listdir(dir_path))
src_collection = None
for col in src_collections:
if col.tail != ext:
continue
# skip if collection don't have same basename
if not col.head.startswith(file_name_items[0]):
continue
src_collection = col
break
if src_collection is None:
# TODO log error!
msg = "Source collection of files was not found"
report_items[msg].append(repre_path)
self.log.warning("{} <{}>".format(msg, repre_path))
return
frame_indicator = "@####@"
anatomy_data["frame"] = frame_indicator
anatomy_filled = anatomy.format(anatomy_data)
if format_dict:
template_result = anatomy_filled["delivery"][anatomy_name]
delivery_path = template_result.rootless.format(**format_dict)
else:
delivery_path = anatomy_filled["delivery"][anatomy_name]
delivery_folder = os.path.dirname(delivery_path)
dst_head, dst_tail = delivery_path.split(frame_indicator)
dst_padding = src_collection.padding
dst_collection = clique.Collection(
head=dst_head,
tail=dst_tail,
padding=dst_padding
)
if not os.path.exists(delivery_folder):
os.makedirs(delivery_folder)
src_head = src_collection.head
src_tail = src_collection.tail
for index in src_collection.indexes:
src_padding = src_collection.format("{padding}") % index
src_file_name = "{}{}{}".format(src_head, src_padding, src_tail)
src = os.path.normpath(
os.path.join(dir_path, src_file_name)
)
dst_padding = dst_collection.format("{padding}") % index
dst = "{}{}{}".format(dst_head, dst_padding, dst_tail)
self.copy_file(src, dst)
def path_from_represenation(self, representation, anatomy):
try:
template = representation["data"]["template"]
except KeyError:
return None
try:
context = representation["context"]
context["root"] = anatomy.roots
path = pipeline.format_template_with_optional_keys(
context, template
)
except KeyError:
# Template references unavailable data
return None
return os.path.normpath(path)
def copy_file(self, src_path, dst_path):
if os.path.exists(dst_path):
return
try:
filelink.create(
src_path,
dst_path,
filelink.HARDLINK
)
except OSError:
shutil.copyfile(src_path, dst_path)
def report(self, report_items):
"""Returns dict with final status of delivery (succes, fail etc.)."""
items = []
title = "Delivery report"
for msg, _items in report_items.items():

View file

@ -1237,12 +1237,8 @@ class SyncEntitiesFactory:
ent_path_items = [ent["name"] for ent in entity["link"]]
parents = ent_path_items[1:len(ent_path_items) - 1:]
hierarchy = ""
if len(parents) > 0:
hierarchy = os.path.sep.join(parents)
data["parents"] = parents
data["hierarchy"] = hierarchy
data["tasks"] = self.entities_dict[id].pop("tasks", {})
self.entities_dict[id]["final_entity"]["data"] = data
self.entities_dict[id]["final_entity"]["type"] = "asset"
@ -2169,8 +2165,6 @@ class SyncEntitiesFactory:
hierarchy = "/".join(parents)
self.entities_dict[ftrack_id][
"final_entity"]["data"]["parents"] = parents
self.entities_dict[ftrack_id][
"final_entity"]["data"]["hierarchy"] = hierarchy
_parents.append(self.entities_dict[ftrack_id]["name"])
for child_id in self.entities_dict[ftrack_id]["children"]:
@ -2181,7 +2175,6 @@ class SyncEntitiesFactory:
if "data" not in self.updates[mongo_id]:
self.updates[mongo_id]["data"] = {}
self.updates[mongo_id]["data"]["parents"] = parents
self.updates[mongo_id]["data"]["hierarchy"] = hierarchy
def prepare_project_changes(self):
ftrack_ent_dict = self.entities_dict[self.ft_project_id]

View file

@ -16,6 +16,18 @@ class LoginServerHandler(BaseHTTPRequestHandler):
self.login_callback = login_callback
BaseHTTPRequestHandler.__init__(self, *args, **kw)
def log_message(self, format_str, *args):
"""Override method of BaseHTTPRequestHandler.
Goal is to use `print` instead of `sys.stderr.write`
"""
# Change
print("%s - - [%s] %s\n" % (
self.client_address[0],
self.log_date_time_string(),
format_str % args
))
def do_GET(self):
'''Override to handle requests ourselves.'''
parsed_path = parse.urlparse(self.path)

View file

@ -0,0 +1,318 @@
from collections import defaultdict
import copy
from Qt import QtWidgets, QtCore, QtGui
from avalon import api, style
from avalon.api import AvalonMongoDB
from openpype.api import Anatomy, config
from openpype import resources
from openpype.lib.delivery import (
sizeof_fmt,
path_from_represenation,
get_format_dict,
check_destination_path,
process_single_file,
process_sequence,
collect_frames
)
class Delivery(api.SubsetLoader):
"""Export selected versions to folder structure from Template"""
is_multiple_contexts_compatible = True
sequence_splitter = "__sequence_splitter__"
representations = ["*"]
families = ["*"]
tool_names = ["library_loader"]
label = "Deliver Versions"
order = 35
icon = "upload"
color = "#d8d8d8"
def message(self, text):
msgBox = QtWidgets.QMessageBox()
msgBox.setText(text)
msgBox.setStyleSheet(style.load_stylesheet())
msgBox.setWindowFlags(
msgBox.windowFlags() | QtCore.Qt.FramelessWindowHint
)
msgBox.exec_()
def load(self, contexts, name=None, namespace=None, options=None):
try:
dialog = DeliveryOptionsDialog(contexts, self.log)
dialog.exec_()
except Exception:
self.log.error("Failed to deliver versions.", exc_info=True)
class DeliveryOptionsDialog(QtWidgets.QDialog):
"""Dialog to select template where to deliver selected representations."""
def __init__(self, contexts, log=None, parent=None):
super(DeliveryOptionsDialog, self).__init__(parent=parent)
project = contexts[0]["project"]["name"]
self.anatomy = Anatomy(project)
self._representations = None
self.log = log
self.currently_uploaded = 0
self.dbcon = AvalonMongoDB()
self.dbcon.Session["AVALON_PROJECT"] = project
self.dbcon.install()
self._set_representations(contexts)
self.setWindowTitle("OpenPype - Deliver versions")
icon = QtGui.QIcon(resources.pype_icon_filepath())
self.setWindowIcon(icon)
self.setWindowFlags(
QtCore.Qt.WindowCloseButtonHint |
QtCore.Qt.WindowMinimizeButtonHint
)
self.setStyleSheet(style.load_stylesheet())
dropdown = QtWidgets.QComboBox()
self.templates = self._get_templates(self.anatomy)
for name, _ in self.templates.items():
dropdown.addItem(name)
template_label = QtWidgets.QLabel()
template_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
template_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
root_line_edit = QtWidgets.QLineEdit()
repre_checkboxes_layout = QtWidgets.QFormLayout()
repre_checkboxes_layout.setContentsMargins(10, 5, 5, 10)
self._representation_checkboxes = {}
for repre in self._get_representation_names():
checkbox = QtWidgets.QCheckBox()
checkbox.setChecked(False)
self._representation_checkboxes[repre] = checkbox
checkbox.stateChanged.connect(self._update_selected_label)
repre_checkboxes_layout.addRow(repre, checkbox)
selected_label = QtWidgets.QLabel()
input_widget = QtWidgets.QWidget(self)
input_layout = QtWidgets.QFormLayout(input_widget)
input_layout.setContentsMargins(10, 15, 5, 5)
input_layout.addRow("Selected representations", selected_label)
input_layout.addRow("Delivery template", dropdown)
input_layout.addRow("Template value", template_label)
input_layout.addRow("Root", root_line_edit)
input_layout.addRow("Representations", repre_checkboxes_layout)
btn_delivery = QtWidgets.QPushButton("Deliver")
btn_delivery.setEnabled(bool(dropdown.currentText()))
progress_bar = QtWidgets.QProgressBar(self)
progress_bar.setMinimum = 0
progress_bar.setMaximum = 100
progress_bar.setVisible(False)
text_area = QtWidgets.QTextEdit()
text_area.setReadOnly(True)
text_area.setVisible(False)
text_area.setMinimumHeight(100)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(input_widget)
layout.addStretch(1)
layout.addWidget(btn_delivery)
layout.addWidget(progress_bar)
layout.addWidget(text_area)
self.selected_label = selected_label
self.template_label = template_label
self.dropdown = dropdown
self.root_line_edit = root_line_edit
self.progress_bar = progress_bar
self.text_area = text_area
self.btn_delivery = btn_delivery
self.files_selected, self.size_selected = \
self._get_counts(self._get_selected_repres())
self._update_selected_label()
self._update_template_value()
btn_delivery.clicked.connect(self.deliver)
dropdown.currentIndexChanged.connect(self._update_template_value)
def deliver(self):
"""Main method to loop through all selected representations"""
self.progress_bar.setVisible(True)
self.btn_delivery.setEnabled(False)
QtWidgets.QApplication.processEvents()
report_items = defaultdict(list)
selected_repres = self._get_selected_repres()
datetime_data = config.get_datetime_data()
template_name = self.dropdown.currentText()
format_dict = get_format_dict(self.anatomy, self.root_line_edit.text())
for repre in self._representations:
if repre["name"] not in selected_repres:
continue
repre_path = path_from_represenation(repre, self.anatomy)
anatomy_data = copy.deepcopy(repre["context"])
new_report_items = check_destination_path(str(repre["_id"]),
self.anatomy,
anatomy_data,
datetime_data,
template_name)
report_items.update(new_report_items)
if new_report_items:
continue
args = [
repre_path,
repre,
self.anatomy,
template_name,
anatomy_data,
format_dict,
report_items,
self.log
]
if repre.get("files"):
src_paths = []
for repre_file in repre["files"]:
src_path = self.anatomy.fill_root(repre_file["path"])
src_paths.append(src_path)
sources_and_frames = collect_frames(src_paths)
for src_path, frame in sources_and_frames.items():
args[0] = src_path
if frame:
anatomy_data["frame"] = frame
new_report_items, uploaded = process_single_file(*args)
report_items.update(new_report_items)
self._update_progress(uploaded)
else: # fallback for Pype2 and representations without files
frame = repre['context'].get('frame')
if frame:
repre["context"]["frame"] = len(str(frame)) * "#"
if not frame:
new_report_items, uploaded = process_single_file(*args)
else:
new_report_items, uploaded = process_sequence(*args)
report_items.update(new_report_items)
self._update_progress(uploaded)
self.text_area.setText(self._format_report(report_items))
self.text_area.setVisible(True)
def _get_representation_names(self):
"""Get set of representation names for checkbox filtering."""
return set([repre["name"] for repre in self._representations])
def _get_templates(self, anatomy):
"""Adds list of delivery templates from Anatomy to dropdown."""
templates = {}
for template_name, value in anatomy.templates["delivery"].items():
if not isinstance(value, str) or not value.startswith('{root'):
continue
templates[template_name] = value
return templates
def _set_representations(self, contexts):
version_ids = [context["version"]["_id"] for context in contexts]
repres = list(self.dbcon.find({
"type": "representation",
"parent": {"$in": version_ids}
}))
self._representations = repres
def _get_counts(self, selected_repres=None):
"""Returns tuple of number of selected files and their size."""
files_selected = 0
size_selected = 0
for repre in self._representations:
if repre["name"] in selected_repres:
files = repre.get("files", [])
if not files: # for repre without files, cannot divide by 0
files_selected += 1
size_selected += 0
else:
for repre_file in files:
files_selected += 1
size_selected += repre_file["size"]
return files_selected, size_selected
def _prepare_label(self):
"""Provides text with no of selected files and their size."""
label = "{} files, size {}".format(self.files_selected,
sizeof_fmt(self.size_selected))
return label
def _get_selected_repres(self):
"""Returns list of representation names filtered from checkboxes."""
selected_repres = []
for repre_name, chckbox in self._representation_checkboxes.items():
if chckbox.isChecked():
selected_repres.append(repre_name)
return selected_repres
def _update_selected_label(self):
"""Updates label with list of number of selected files."""
selected_repres = self._get_selected_repres()
self.files_selected, self.size_selected = \
self._get_counts(selected_repres)
self.selected_label.setText(self._prepare_label())
def _update_template_value(self, _index=None):
"""Sets template value to label after selection in dropdown."""
name = self.dropdown.currentText()
template_value = self.templates.get(name)
if template_value:
self.btn_delivery.setEnabled(True)
self.template_label.setText(template_value)
def _update_progress(self, uploaded):
"""Update progress bar after each repre copied."""
self.currently_uploaded += uploaded
ratio = self.currently_uploaded / self.files_selected
self.progress_bar.setValue(ratio * self.progress_bar.maximum())
def _format_report(self, report_items):
"""Format final result and error details as html."""
msg = "Delivery finished"
if not report_items:
msg += " successfully"
else:
msg += " with errors"
txt = "<h2>{}</h2>".format(msg)
for header, data in report_items.items():
txt += "<h3>{}</h3>".format(header)
for item in data:
txt += "{}<br>".format(item)
return txt

View file

@ -39,7 +39,6 @@ class CollectResourcesPath(pyblish.api.InstancePlugin):
"rig",
"plate",
"look",
"lut",
"yetiRig",
"yeticache",
"nukenodes",
@ -52,7 +51,8 @@ class CollectResourcesPath(pyblish.api.InstancePlugin):
"fbx",
"textures",
"action",
"background"
"background",
"effect"
]
def process(self, instance):

View file

@ -40,12 +40,15 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
# get sequence
otio_timeline = context.data["otioTimeline"]
# temp file
audio_temp_fpath = self.create_temp_file("audio")
# get all audio inputs from otio timeline
audio_inputs = self.get_audio_track_items(otio_timeline)
if not audio_inputs:
return
# temp file
audio_temp_fpath = self.create_temp_file("audio")
# create empty audio with longest duration
empty = self.create_empty(audio_inputs)
@ -53,14 +56,14 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
audio_inputs.insert(0, empty)
# create cmd
cmd = self.ffmpeg_path + " "
cmd = '"{}"'.format(self.ffmpeg_path) + " "
cmd += self.create_cmd(audio_inputs)
cmd += audio_temp_fpath
cmd += "\"{}\"".format(audio_temp_fpath)
# run subprocess
self.log.debug("Executing: {}".format(cmd))
openpype.api.run_subprocess(
cmd, shell=True, logger=self.log
cmd, logger=self.log
)
# remove empty
@ -97,17 +100,17 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
audio_fpath = self.create_temp_file(name)
cmd = " ".join([
self.ffmpeg_path,
'"{}"'.format(self.ffmpeg_path),
"-ss {}".format(start_sec),
"-t {}".format(duration_sec),
"-i {}".format(audio_file),
"-i \"{}\"".format(audio_file),
audio_fpath
])
# run subprocess
self.log.debug("Executing: {}".format(cmd))
openpype.api.run_subprocess(
cmd, shell=True, logger=self.log
cmd, logger=self.log
)
else:
audio_fpath = recycling_file.pop()
@ -218,11 +221,11 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
# create empty cmd
cmd = " ".join([
self.ffmpeg_path,
'"{}"'.format(self.ffmpeg_path),
"-f lavfi",
"-i anullsrc=channel_layout=stereo:sample_rate=48000",
"-t {}".format(max_duration_sec),
empty_fpath
"\"{}\"".format(empty_fpath)
])
# generate empty with ffmpeg
@ -230,7 +233,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
self.log.debug("Executing: {}".format(cmd))
openpype.api.run_subprocess(
cmd, shell=True, logger=self.log
cmd, logger=self.log
)
# return dict with output

View file

@ -209,7 +209,7 @@ class ExtractOTIOReview(openpype.api.Extractor):
"frameStart": start,
"frameEnd": end,
"stagingDir": self.staging_dir,
"tags": ["review", "ftrackreview", "delete"]
"tags": ["review", "delete"]
}
collection = clique.Collection(
@ -313,7 +313,7 @@ class ExtractOTIOReview(openpype.api.Extractor):
out_frame_start += end_offset
# start command list
command = [ffmpeg_path]
command = ['"{}"'.format(ffmpeg_path)]
if sequence:
input_dir, collection = sequence
@ -326,7 +326,7 @@ class ExtractOTIOReview(openpype.api.Extractor):
# form command for rendering gap files
command.extend([
"-start_number {}".format(in_frame_start),
"-i {}".format(input_path)
"-i \"{}\"".format(input_path)
])
elif video:
@ -341,7 +341,7 @@ class ExtractOTIOReview(openpype.api.Extractor):
command.extend([
"-ss {}".format(sec_start),
"-t {}".format(sec_duration),
"-i {}".format(video_path)
"-i \"{}\"".format(video_path)
])
elif gap:
@ -360,11 +360,13 @@ class ExtractOTIOReview(openpype.api.Extractor):
# add output attributes
command.extend([
"-start_number {}".format(out_frame_start),
output_path
"\"{}\"".format(output_path)
])
# execute
self.log.debug("Executing: {}".format(" ".join(command)))
output = openpype.api.run_subprocess(" ".join(command), shell=True)
output = openpype.api.run_subprocess(
" ".join(command), logger=self.log
)
self.log.debug("Output: {}".format(output))
def _generate_used_frames(self, duration, end_offset=None):

View file

@ -48,6 +48,8 @@ class ExtractReview(pyblish.api.InstancePlugin):
video_exts = ["mov", "mp4"]
supported_exts = image_exts + video_exts
alpha_exts = ["exr", "png", "dpx"]
# FFmpeg tools paths
ffmpeg_path = get_ffmpeg_tool_path("ffmpeg")
@ -296,6 +298,13 @@ class ExtractReview(pyblish.api.InstancePlugin):
):
with_audio = False
input_is_sequence = self.input_is_sequence(repre)
input_allow_bg = False
if input_is_sequence and repre["files"]:
ext = os.path.splitext(repre["files"][0])[1].replace(".", "")
if ext in self.alpha_exts:
input_allow_bg = True
return {
"fps": float(instance.data["fps"]),
"frame_start": frame_start,
@ -310,7 +319,8 @@ class ExtractReview(pyblish.api.InstancePlugin):
"resolution_width": instance.data.get("resolutionWidth"),
"resolution_height": instance.data.get("resolutionHeight"),
"origin_repre": repre,
"input_is_sequence": self.input_is_sequence(repre),
"input_is_sequence": input_is_sequence,
"input_allow_bg": input_allow_bg,
"with_audio": with_audio,
"without_handles": without_handles,
"handles_are_set": handles_are_set
@ -470,6 +480,39 @@ class ExtractReview(pyblish.api.InstancePlugin):
lut_filters = self.lut_filters(new_repre, instance, ffmpeg_input_args)
ffmpeg_video_filters.extend(lut_filters)
bg_alpha = 0
bg_color = output_def.get("bg_color")
if bg_color:
bg_red, bg_green, bg_blue, bg_alpha = bg_color
if bg_alpha > 0:
if not temp_data["input_allow_bg"]:
self.log.info((
"Output definition has defined BG color input was"
" resolved as does not support adding BG."
))
else:
bg_color_hex = "#{0:0>2X}{1:0>2X}{2:0>2X}".format(
bg_red, bg_green, bg_blue
)
bg_color_alpha = float(bg_alpha) / 255
bg_color_str = "{}@{}".format(bg_color_hex, bg_color_alpha)
self.log.info("Applying BG color {}".format(bg_color_str))
color_args = [
"split=2[bg][fg]",
"[bg]drawbox=c={}:replace=1:t=fill[bg]".format(
bg_color_str
),
"[bg][fg]overlay=format=auto"
]
# Prepend bg color change before all video filters
# NOTE at the time of creation it is required as video filters
# from settings may affect color of BG
# e.g. `eq` can remove alpha from input
for arg in reversed(color_args):
ffmpeg_video_filters.insert(0, arg)
# Add argument to override output file
ffmpeg_output_args.append("-y")
@ -547,10 +590,12 @@ class ExtractReview(pyblish.api.InstancePlugin):
all_args.append("\"{}\"".format(self.ffmpeg_path))
all_args.extend(input_args)
if video_filters:
all_args.append("-filter:v {}".format(",".join(video_filters)))
all_args.append("-filter:v")
all_args.append("\"{}\"".format(",".join(video_filters)))
if audio_filters:
all_args.append("-filter:a {}".format(",".join(audio_filters)))
all_args.append("-filter:a")
all_args.append("\"{}\"".format(",".join(audio_filters)))
all_args.extend(output_args)

View file

@ -78,7 +78,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
"rig",
"plate",
"look",
"lut",
"audio",
"yetiRig",
"yeticache",
@ -97,7 +96,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
"editorial",
"background",
"camerarig",
"redshiftproxy"
"redshiftproxy",
"effect"
]
exclude_families = ["clip"]
db_representation_context_keys = [

View file

@ -37,11 +37,11 @@
"ftrackreview"
],
"ffmpeg_args": {
"video_filters": [
"eq=gamma=2.2"
],
"video_filters": [],
"audio_filters": [],
"input": [],
"input": [
"-apply_trc gamma22"
],
"output": [
"-pix_fmt yuv420p",
"-crf 18",
@ -57,6 +57,12 @@
},
"width": 0,
"height": 0,
"bg_color": [
0,
0,
0,
0
],
"letter_box": {
"enabled": false,
"ratio": 0.0,

View file

@ -193,6 +193,15 @@
"minimum": 0,
"maximum": 100000
},
{
"type": "label",
"label": "Background color is used only when input have transparency and Alpha is higher than 0."
},
{
"type": "color",
"label": "Background color",
"key": "bg_color"
},
{
"key": "letter_box",
"label": "Letter box",
@ -280,24 +289,14 @@
"minimum": 0
},
{
"type": "schema_template",
"name": "template_rgba_color",
"template_data": [
{
"label": "Font Color",
"name": "font_color"
}
]
"type": "color",
"key": "font_color",
"label": "Font Color"
},
{
"type": "schema_template",
"name": "template_rgba_color",
"template_data": [
{
"label": "Background Color",
"name": "bg_color"
}
]
"type": "color",
"key": "bg_color",
"label": "Background Color"
},
{
"type": "number",

View file

@ -1,33 +0,0 @@
[
{
"type": "list-strict",
"key": "{name}",
"label": "{label}",
"object_types": [
{
"label": "R",
"type": "number",
"minimum": 0,
"maximum": 255
},
{
"label": "G",
"type": "number",
"minimum": 0,
"maximum": 255
},
{
"label": "B",
"type": "number",
"minimum": 0,
"maximum": 255
},
{
"label": "A",
"type": "number",
"minimum": 0,
"maximum": 255
}
]
}
]

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.0.0-rc.5"
__version__ = "3.0.0-rc.6"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
version = "3.0.0-rc.5"
version = "3.0.0-rc.6"
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team <info@openpype.io>"]
license = "MIT License"

@ -1 +1 @@
Subproject commit cfd4191e364b47de7364096f45d9d9d9a901692a
Subproject commit 0d9a228fdb2eb08fe6caa30f25fe2a34fead1a03

123
start.py
View file

@ -6,10 +6,11 @@ Bootstrapping process of OpenPype is as follows:
`OPENPYPE_PATH` is checked for existence - either one from environment or
from user settings. Precedence takes the one set by environment.
On this path we try to find OpenPype in directories version string in their names.
For example: `openpype-v3.0.1-foo` is valid name, or even `foo_3.0.2` - as long
as version can be determined from its name _AND_ file `openpype/openpype/version.py`
can be found inside, it is considered OpenPype installation.
On this path we try to find OpenPype in directories version string in their
names. For example: `openpype-v3.0.1-foo` is valid name, or
even `foo_3.0.2` - as long as version can be determined from its name
_AND_ file `openpype/openpype/version.py` can be found inside, it is
considered OpenPype installation.
If no OpenPype repositories are found in `OPENPYPE_PATH` (user data dir)
then **Igniter** (OpenPype setup tool) will launch its GUI.
@ -20,19 +21,19 @@ appdata dir in user home and extract it there. Version will be determined by
version specified in OpenPype module.
If OpenPype repository directories are found in default install location
(user data dir) or in `OPENPYPE_PATH`, it will get list of those dirs there and
use latest one or the one specified with optional `--use-version` command
line argument. If the one specified doesn't exist then latest available
version will be used. All repositories in that dir will be added
(user data dir) or in `OPENPYPE_PATH`, it will get list of those dirs
there and use latest one or the one specified with optional `--use-version`
command line argument. If the one specified doesn't exist then latest
available version will be used. All repositories in that dir will be added
to `sys.path` and `PYTHONPATH`.
If OpenPype is live (not frozen) then current version of OpenPype module will be
used. All directories under `repos` will be added to `sys.path` and
If OpenPype is live (not frozen) then current version of OpenPype module
will be used. All directories under `repos` will be added to `sys.path` and
`PYTHONPATH`.
OpenPype depends on connection to `MongoDB`_. You can specify MongoDB connection
string via `OPENPYPE_MONGO` set in environment or it can be set in user
settings or via **Igniter** GUI.
OpenPype depends on connection to `MongoDB`_. You can specify MongoDB
connection string via `OPENPYPE_MONGO` set in environment or it can be set
in user settings or via **Igniter** GUI.
So, bootstrapping OpenPype looks like this::
@ -282,7 +283,8 @@ def _process_arguments() -> tuple:
print(" --use-version=3.0.0")
sys.exit(1)
m = re.search(r"--use-version=(?P<version>\d+\.\d+\.\d*.+?)", arg)
m = re.search(
r"--use-version=(?P<version>\d+\.\d+\.\d+(?:\S*)?)", arg)
if m and m.group('version'):
use_version = m.group('version')
sys.argv.remove(arg)
@ -414,6 +416,7 @@ def _find_frozen_openpype(use_version: str = None,
(if requested).
"""
version_path = None
openpype_version = None
openpype_versions = bootstrap.find_openpype(include_zips=True,
staging=use_staging)
@ -433,7 +436,6 @@ def _find_frozen_openpype(use_version: str = None,
if local_version == openpype_versions[-1]:
os.environ["OPENPYPE_TRYOUT"] = "1"
openpype_versions = []
else:
print("!!! Warning: cannot determine current running version.")
@ -480,17 +482,25 @@ def _find_frozen_openpype(use_version: str = None,
return version_path
# get path of version specified in `--use-version`
version_path = BootstrapRepos.get_version_path_from_list(
use_version, openpype_versions)
local_version = bootstrap.get_version(OPENPYPE_ROOT)
if use_version and use_version != local_version:
# force the one user has selected
openpype_version = None
openpype_versions = bootstrap.find_openpype(include_zips=True,
staging=use_staging)
v: OpenPypeVersion
found = [v for v in openpype_versions if str(v) == use_version]
if found:
openpype_version = sorted(found)[-1]
if not openpype_version:
print(f"!!! requested version {use_version} was not found.")
if openpype_versions:
print(" - found: ")
for v in sorted(openpype_versions):
print(f" - {v}: {v.path}")
if not version_path:
if use_version is not None and openpype_version:
print(("!!! Specified version was not found, using "
"latest available"))
# specified version was not found so use latest detected.
version_path = openpype_version.path
print(f">>> Using version [ {openpype_version} ]")
print(f" From {version_path}")
print(f" - local version {local_version}")
sys.exit(1)
# test if latest detected is installed (in user data dir)
is_inside = False
@ -521,7 +531,7 @@ def _find_frozen_openpype(use_version: str = None,
openpype_version.path = version_path
_initialize_environment(openpype_version)
return version_path
return openpype_version.path
def _bootstrap_from_code(use_version):
@ -536,36 +546,53 @@ def _bootstrap_from_code(use_version):
"""
# run through repos and add them to `sys.path` and `PYTHONPATH`
# set root
_openpype_root = OPENPYPE_ROOT
if getattr(sys, 'frozen', False):
local_version = bootstrap.get_version(Path(OPENPYPE_ROOT))
local_version = bootstrap.get_version(Path(_openpype_root))
print(f" - running version: {local_version}")
assert local_version
else:
# get current version of OpenPype
local_version = bootstrap.get_local_live_version()
os.environ["OPENPYPE_VERSION"] = local_version
if use_version and use_version != local_version:
version_to_use = None
openpype_versions = bootstrap.find_openpype(include_zips=True)
version_path = BootstrapRepos.get_version_path_from_list(
use_version, openpype_versions)
if version_path:
# use specified
bootstrap.add_paths_from_directory(version_path)
os.environ["OPENPYPE_VERSION"] = use_version
else:
version_path = OPENPYPE_ROOT
v: OpenPypeVersion
found = [v for v in openpype_versions if str(v) == use_version]
if found:
version_to_use = sorted(found)[-1]
repos = os.listdir(os.path.join(OPENPYPE_ROOT, "repos"))
repos = [os.path.join(OPENPYPE_ROOT, "repos", repo) for repo in repos]
if version_to_use:
# use specified
if version_to_use.path.is_file():
version_to_use.path = bootstrap.extract_openpype(
version_to_use)
bootstrap.add_paths_from_directory(version_to_use.path)
os.environ["OPENPYPE_VERSION"] = use_version
version_path = version_to_use.path
os.environ["OPENPYPE_REPOS_ROOT"] = (version_path / "openpype").as_posix() # noqa: E501
_openpype_root = version_to_use.path.as_posix()
else:
print(f"!!! requested version {use_version} was not found.")
if openpype_versions:
print(" - found: ")
for v in sorted(openpype_versions):
print(f" - {v}: {v.path}")
print(f" - local version {local_version}")
sys.exit(1)
else:
os.environ["OPENPYPE_VERSION"] = local_version
version_path = Path(_openpype_root)
os.environ["OPENPYPE_REPOS_ROOT"] = _openpype_root
repos = os.listdir(os.path.join(_openpype_root, "repos"))
repos = [os.path.join(_openpype_root, "repos", repo) for repo in repos]
# add self to python paths
repos.insert(0, OPENPYPE_ROOT)
repos.insert(0, _openpype_root)
for repo in repos:
sys.path.insert(0, repo)
# Set OPENPYPE_REPOS_ROOT to code root
os.environ["OPENPYPE_REPOS_ROOT"] = OPENPYPE_ROOT
# add venv 'site-packages' to PYTHONPATH
python_path = os.getenv("PYTHONPATH", "")
split_paths = python_path.split(os.pathsep)
@ -580,11 +607,11 @@ def _bootstrap_from_code(use_version):
# point to same hierarchy from code and from frozen OpenPype
additional_paths = [
# add OpenPype tools
os.path.join(OPENPYPE_ROOT, "openpype", "tools"),
os.path.join(_openpype_root, "openpype", "tools"),
# add common OpenPype vendor
# (common for multiple Python interpreter versions)
os.path.join(
OPENPYPE_ROOT,
_openpype_root,
"openpype",
"vendor",
"python",
@ -597,7 +624,7 @@ def _bootstrap_from_code(use_version):
os.environ["PYTHONPATH"] = os.pathsep.join(split_paths)
return Path(version_path)
return version_path
def boot():
@ -624,6 +651,10 @@ def boot():
use_version, use_staging = _process_arguments()
if os.getenv("OPENPYPE_VERSION"):
use_staging = "staging" in os.getenv("OPENPYPE_VERSION")
use_version = os.getenv("OPENPYPE_VERSION")
# ------------------------------------------------------------------------
# Determine mongodb connection
# ------------------------------------------------------------------------

View file

@ -175,9 +175,9 @@ if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) {
Write-Host ">>> " -NoNewline -ForegroundColor green
Write-Host "Cleaning cache files ... " -NoNewline
Get-ChildItem $openpype_root -Filter "*.pyc" -Force -Recurse | Remove-Item -Force
Get-ChildItem $openpype_root -Filter "*.pyo" -Force -Recurse | Remove-Item -Force
Get-ChildItem $openpype_root -Filter "__pycache__" -Force -Recurse | Remove-Item -Force -Recurse
Get-ChildItem $openpype_root -Filter "*.pyc" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force
Get-ChildItem $openpype_root -Filter "*.pyo" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force
Get-ChildItem $openpype_root -Filter "__pycache__" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force -Recurse
Write-Host "OK" -ForegroundColor green
Write-Host ">>> " -NoNewline -ForegroundColor green

View file

@ -122,7 +122,8 @@ clean_pyc () {
local path
path=$openpype_root
echo -e "${BIGreen}>>>${RST} Cleaning pyc at [ ${BIWhite}$path${RST} ] ... \c"
find "$path" -regex '^.*\(__pycache__\|\.py[co]\)$' -delete
find "$path" -path ./build -prune -o -regex '^.*\(__pycache__\|\.py[co]\)$' -delete
echo -e "${BIGreen}DONE${RST}"
}

View file

@ -76,16 +76,20 @@ print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))
Set-Location -Path $current_dir
Exit-WithCode 1
}
# We are supporting python 3.6 and up
if(($matches[1] -lt 3) -or ($matches[2] -lt 7)) {
# We are supporting python 3.7 only
if (($matches[1] -lt 3) -or ($matches[2] -lt 7)) {
Write-Host "FAILED Version [ $p ] is old and unsupported" -ForegroundColor red
Set-Location -Path $current_dir
Exit-WithCode 1
} elseif (($matches[1] -eq 3) -and ($matches[2] -gt 7)) {
Write-Host "WARNING Version [ $p ] is unsupported, use at your own risk." -ForegroundColor yellow
Write-Host "*** " -NoNewline -ForegroundColor yellow
Write-Host "OpenPype supports only Python 3.7" -ForegroundColor white
} else {
Write-Host "OK [ $p ]" -ForegroundColor green
}
Write-Host "OK [ $p ]" -ForegroundColor green
}
$current_dir = Get-Location
$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$openpype_root = (Get-Item $script_dir).parent.FullName

View file

@ -126,7 +126,7 @@ clean_pyc () {
local path
path=$openpype_root
echo -e "${BIGreen}>>>${RST} Cleaning pyc at [ ${BIWhite}$path${RST} ] ... \c"
find "$path" -regex '^.*\(__pycache__\|\.py[co]\)$' -delete
find "$path" -path ./build -prune -o -regex '^.*\(__pycache__\|\.py[co]\)$' -delete
echo -e "${BIGreen}DONE${RST}"
}

View file

@ -98,9 +98,9 @@ if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) {
Write-Host ">>> " -NoNewline -ForegroundColor green
Write-Host "Cleaning cache files ... " -NoNewline
Get-ChildItem $openpype_root -Filter "*.pyc" -Force -Recurse | Remove-Item -Force
Get-ChildItem $openpype_root -Filter "*.pyo" -Force -Recurse | Remove-Item -Force
Get-ChildItem $openpype_root -Filter "__pycache__" -Force -Recurse | Remove-Item -Force -Recurse
Get-ChildItem $openpype_root -Filter "*.pyc" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force
Get-ChildItem $openpype_root -Filter "*.pyo" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force
Get-ChildItem $openpype_root -Filter "__pycache__" -Force -Recurse| Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force -Recurse
Write-Host "OK" -ForegroundColor green
Write-Host ">>> " -NoNewline -ForegroundColor green

View file

@ -89,23 +89,6 @@ detect_python () {
fi
}
##############################################################################
# Clean pyc files in specified directory
# Globals:
# None
# Arguments:
# Optional path to clean
# Returns:
# None
###############################################################################
clean_pyc () {
local path
path=$openpype_root
echo -e "${BIGreen}>>>${RST} Cleaning pyc at [ ${BIWhite}$path${RST} ] ... \c"
find "$path" -regex '^.*\(__pycache__\|\.py[co]\)$' -delete
echo -e "${BIGreen}DONE${RST}"
}
##############################################################################
# Return absolute path
# Globals:

View file

@ -10,9 +10,51 @@
PS> .\run_project_manager.ps1
#>
$art = @"
. . .. . ..
_oOOP3OPP3Op_. .
.PPpo~. .. ~2p. .. .... . .
.Ppo . .pPO3Op.. . O:. . . .
.3Pp . oP3'. 'P33. . 4 .. . . . .. . . .
.~OP 3PO. .Op3 : . .. _____ _____ _____
.P3O . oP3oP3O3P' . . . . / /./ /./ /
O3:. O3p~ . .:. . ./____/./____/ /____/
'P . 3p3. oP3~. ..P:. . . .. . . .. . . .
. ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . .
. '_ .. . . _OP3.. . .https://openpype.io.. .
~P3.OPPPO3OP~ . .. .
. ' '. . .. . . . .. .
"@
Write-Host $art -ForegroundColor DarkGreen
$current_dir = Get-Location
$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$openpype_root = (Get-Item $script_dir).parent.FullName
$env:_INSIDE_OPENPYPE_TOOL = "1"
# make sure Poetry is in PATH
if (-not (Test-Path 'env:POETRY_HOME')) {
$env:POETRY_HOME = "$openpype_root\.poetry"
}
$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin"
Set-Location -Path $openpype_root
Write-Host ">>> " -NoNewline -ForegroundColor Green
Write-Host "Reading Poetry ... " -NoNewline
if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) {
Write-Host "NOT FOUND" -ForegroundColor Yellow
Write-Host "*** " -NoNewline -ForegroundColor Yellow
Write-Host "We need to install Poetry create virtual env first ..."
& "$openpype_root\tools\create_env.ps1"
} else {
Write-Host "OK" -ForegroundColor Green
}
& poetry run python "$($openpype_root)\start.py" projectmanager
Set-Location -Path $current_dir

103
tools/run_projectmanager.sh Executable file
View file

@ -0,0 +1,103 @@
#!/usr/bin/env bash
# Run OpenPype Settings GUI
art () {
cat <<-EOF
. . .. . ..
_oOOP3OPP3Op_. .
.PPpo~· ·· ~2p. ·· ···· · ·
·Ppo · .pPO3Op.· · O:· · · ·
.3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · ·
·~OP 3PO· .Op3 : · ·· _____ _____ _____
·P3O · oP3oP3O3P' · · · · / /·/ /·/ /
O3:· O3p~ · ·:· · ·/____/·/____/ /____/
'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · ·
· ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · ·
· '_ .. · . _OP3·· · ·https://openpype.io·· ·
~P3·OPPPO3OP~ · ·· ·
· ' '· · ·· · · · ·· ·
EOF
}
# Colors for terminal
RST='\033[0m' # Text Reset
# Regular Colors
Black='\033[0;30m' # Black
Red='\033[0;31m' # Red
Green='\033[0;32m' # Green
Yellow='\033[0;33m' # Yellow
Blue='\033[0;34m' # Blue
Purple='\033[0;35m' # Purple
Cyan='\033[0;36m' # Cyan
White='\033[0;37m' # White
# Bold
BBlack='\033[1;30m' # Black
BRed='\033[1;31m' # Red
BGreen='\033[1;32m' # Green
BYellow='\033[1;33m' # Yellow
BBlue='\033[1;34m' # Blue
BPurple='\033[1;35m' # Purple
BCyan='\033[1;36m' # Cyan
BWhite='\033[1;37m' # White
# Bold High Intensity
BIBlack='\033[1;90m' # Black
BIRed='\033[1;91m' # Red
BIGreen='\033[1;92m' # Green
BIYellow='\033[1;93m' # Yellow
BIBlue='\033[1;94m' # Blue
BIPurple='\033[1;95m' # Purple
BICyan='\033[1;96m' # Cyan
BIWhite='\033[1;97m' # White
##############################################################################
# Return absolute path
# Globals:
# None
# Arguments:
# Path to resolve
# Returns:
# None
###############################################################################
realpath () {
echo $(cd $(dirname "$1"); pwd)/$(basename "$1")
}
# Main
main () {
# Directories
openpype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}")))
_inside_openpype_tool="1"
# make sure Poetry is in PATH
if [[ -z $POETRY_HOME ]]; then
export POETRY_HOME="$openpype_root/.poetry"
fi
export PATH="$POETRY_HOME/bin:$PATH"
pushd "$openpype_root" > /dev/null || return > /dev/null
echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c"
if [ -f "$POETRY_HOME/bin/poetry" ]; then
echo -e "${BIGreen}OK${RST}"
else
echo -e "${BIYellow}NOT FOUND${RST}"
echo -e "${BIYellow}***${RST} We need to install Poetry and virtual env ..."
. "$openpype_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; }
fi
echo -e "${BIGreen}>>>${RST} Generating zip from current sources ..."
poetry run python "$openpype_root/start.py" projectmanager
}
main

View file

@ -94,8 +94,8 @@ if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) {
Write-Host ">>> " -NoNewline -ForegroundColor green
Write-Host "Cleaning cache files ... " -NoNewline
Get-ChildItem $openpype_root -Filter "*.pyc" -Force -Recurse | Remove-Item -Force
Get-ChildItem $openpype_root -Filter "__pycache__" -Force -Recurse | Remove-Item -Force -Recurse
Get-ChildItem $openpype_root -Filter "*.pyc" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force
Get-ChildItem $openpype_root -Filter "__pycache__" -Force -Recurse | Where-Object { $_.FullName -inotmatch 'build' } | Remove-Item -Force -Recurse
Write-Host "OK" -ForegroundColor green
Write-Host ">>> " -NoNewline -ForegroundColor green

View file

@ -70,7 +70,7 @@ clean_pyc () {
local path
path=$openpype_root
echo -e "${BIGreen}>>>${RST} Cleaning pyc at [ ${BIWhite}$path${RST} ] ... \c"
find "$path" -regex '^.*\(__pycache__\|\.py[co]\)$' -delete
find "$path" -path ./build -prune -o -regex '^.*\(__pycache__\|\.py[co]\)$' -delete
echo -e "${BIGreen}DONE${RST}"
}

View file

@ -41,10 +41,26 @@ version to run for the artist, until a higher version is detected in the update
#### Manual Updates
If for some reason you don't want to use the automatic updates, you can distribute your
zips manually. Your artist will then have to unpack them to the correct place on their disk.
zips manually. Your artist will then have to put them to the correct place on their disk.
Zips will be automatically unzipped there.
The default locations are:
- Windows: `C:\Users\%USERNAME%\AppData\Local\pypeclub\openpype`
- Linux: ` `
- Mac: ` `
- Windows: `%LOCALAPPDATA%\pypeclub\openpype`
- Linux: `~/.local/share/pypeclub/openpype`
- Mac: `~/Library/Application Support/pypeclub/openpype`
### Staging vs. Production
You can have version of OpenPype with experimental features you want to try somewhere but you
don't want to disrupt your production. You can tag version as **staging** simply by appending `+staging`
to its name.
So if you have OpenPype version like `OpenPype-v3.0.0.zip` just name it `OpenPype-v3.0.0+staging.zip`.
When both these versions are present, production one will always take precedence over staging.
You can run OpenPype with `--use-staging` argument to add use staging versions.
:::note
Running staging version is identified by orange **P** icon in system tray.
:::

View file

@ -4,137 +4,154 @@ title: OpenPype Commands Reference
sidebar_label: OpenPype Commands
---
:::info
You can substitute `openpype_console` with `poetry run python start.py` if you want to run it
directly from sources.
:::
## `tray`
:::note
Running OpenPype without any commands will default to `tray`.
:::
To launch Tray:
```sh
pype tray
## Common arguments
`--use-version` to specify explicit version to use:
```shell
openpype_console --use-version=3.0.0-foo+bar
```
### `--debug`
`--use-staging` - to use staging versions of OpenPype.
For more information [see here](admin_use#run-openpype).
## Commands
| Command | Description | Arguments |
| --- | --- |: --- :|
| tray | Launch OpenPype Tray. | [📑](#tray-arguments)
| eventserver | This should be ideally used by system service (such as systemd or upstart on linux and window service). | [📑](#eventserver-arguments) |
| launch | Launch application in Pype environment. | [📑](#launch-arguments) |
| publish | Pype takes JSON from provided path and use it to publish data in it. | [📑](#publish-arguments) |
| extractenvironments | Extract environment variables for entered context to a json file. | [📑](#extractenvironments-arguments) |
| run | Execute given python script within OpenPype environment. | [📑](#run-arguments) |
| projectmanager | Launch Project Manager UI | [📑](#projectmanager-arguments) |
| settings | Open Settings UI | [📑](#settings-arguments) |
| standalonepublisher | Open Standalone Publisher UI | [📑](#standalonepublisher-arguments) |
---
### `tray` arguments {#tray-arguments}
| Argument | Description |
| --- | --- |
| `--debug` | print verbose information useful for debugging (works with `openpype_console`) |
To launch Tray with debugging information:
```sh
pype tray --debug
```shell
openpype_console tray --debug
```
---
### `launch` arguments {#eventserver-arguments}
You have to set either proper environment variables to provide URL and credentials or use
option to specify them. If you use `--store_credentials` provided credentials will be stored for later use.
--------------------
## `eventserver`
This command launches ftrack event server.
This should be ideally used by system service (such us systemd or upstart
on linux and window service).
You have to set either proper environment variables to provide URL and
credentials or use option to specify them. If you use `--store_credentials`
provided credentials will be stored for later use.
| Argument | Description |
| --- | --- |
| `--debug` | print debug info |
| `--ftrack-url` | URL to ftrack server (can be set with `FTRACK_SERVER`) |
| `--ftrack-user` |user name to log in to ftrack (can be set with `FTRACK_API_USER`) |
| `--ftrack-api-key` | ftrack api key (can be set with `FTRACK_API_KEY`) |
| `--ftrack-events-path` | path to event server plugins (can be set with `FTRACK_EVENTS_PATH`) |
| `--no-stored-credentials` | will use credential specified with options above |
| `--store-credentials` | will store credentials to file for later use |
| `--legacy` | run event server without mongo storing |
| `--clockify-api-key` | Clockify API key (can be set with `CLOCKIFY_API_KEY`) |
| `--clockify-workspace` | Clockify workspace (can be set with `CLOCKIFY_WORKSPACE`) |
To run ftrack event server:
```sh
pype eventserver --ftrack-url=<url> --ftrack-user=<user> --ftrack-api-key=<key> --ftrack-events-path=<path> --no-stored-credentials --store-credentials
```shell
openpype_console eventserver --ftrack-url=<url> --ftrack-user=<user> --ftrack-api-key=<key> --ftrack-events-path=<path> --no-stored-credentials --store-credentials
```
---
### `launch` arguments {#launch-arguments}
### `--debug`
- print debug info
### `--ftrack-url`
- URL to ftrack server
### `--ftrack-user`
- user name to log in to ftrack
### `--ftrack-api-key`
- ftrack api key
### `--ftrack-events-path`
- path to event server plugins
### `--no-stored-credentials`
- will use credential specified with options above
### `--store-credentials`
- will store credentials to file for later use
--------------------
## `launch`
Launch application in Pype environment.
### `--app`
Application name - this should be the same as it's [defining toml](admin_hosts#launchers) file (without .toml)
### `--project`
Project name
### `--asset`
Asset name
### `--task`
Task name
### `--tools`
*Optional: Additional tools environment files to add*
### `--user`
*Optional: User on behalf to run*
### `--ftrack-server` / `-fs`
*Optional: Ftrack server URL*
### `--ftrack-user` / `-fu`
*Optional: Ftrack user*
### `--ftrack-key` / `-fk`
*Optional: Ftrack API key*
| Argument | Description |
| --- | --- |
| `--app` | Application name - this should be the key for application from Settings. |
| `--project` | Project name (default taken from `AVALON_PROJECT` if set) |
| `--asset` | Asset name (default taken from `AVALON_ASSET` if set) |
| `--task` | Task name (default taken from `AVALON_TASK` is set) |
| `--tools` | *Optional: Additional tools to add* |
| `--user` | *Optional: User on behalf to run* |
| `--ftrack-server` / `-fs` | *Optional: Ftrack server URL* |
| `--ftrack-user` / `-fu` | *Optional: Ftrack user* |
| `--ftrack-key` / `-fk` | *Optional: Ftrack API key* |
For example to run Python interactive console in Pype context:
```sh
```shell
pype launch --app python --project my_project --asset my_asset --task my_task
```
--------------------
---
### `publish` arguments {#publish-arguments}
| Argument | Description |
| --- | --- |
| `--debug` | print more verbose infomation |
## `publish`
Pype takes JSON from provided path and use it to publish data in it.
```sh
```shell
pype publish <PATH_TO_JSON>
```
### `--debug`
- print more verbose infomation
--------------------
## `extractenvironments`
Extract environment variables for entered context to a json file.
---
### `extractenvironments` arguments {#extractenvironments-arguments}
Entered output filepath will be created if does not exists.
All context options must be passed otherwise only openpype's global environments will be extracted.
Context options are `project`, `asset`, `task`, `app`
Context options are "project", "asset", "task", "app"
| Argument | Description |
| --- | --- |
| `output_json_path` | Absolute path to the exported json file |
| `--project` | Project name |
| `--asset` | Asset name |
| `--task` | Task name |
| `--app` | Application name |
### `output_json_path`
- Absolute path to the exported json file
```shell
openpype_console /home/openpype/env.json --project Foo --asset Bar --task modeling --app maya-2019
```
### `--project`
- Project name
---
### `run` arguments {#run-arguments}
### `--asset`
- Asset name
| Argument | Description |
| `--script` | run specified python script |
### `--task`
- Task name
Note that additional arguments are passed to the script.
### `--app`
- Application name
```shell
openpype_console run --script /foo/bar/baz.py arg1 arg2
```
---
### `projectmanager` arguments {#projectmanager-arguments}
`projectmanager` has no command-line arguments.
```shell
openpype_console projectmanager
```
---
### `settings` arguments {#settings-arguments}
| Argument | Description |
| `-d` / `--dev` | Run settings in developer mode. |
```shell
openpypeconsole settings
```
---
### `standalonepublisher` arguments {#standalonepublisher-arguments}
`standalonepublisher` has no command-line arguments.
```shell
openpype_console standalonepublisher
```

View file

@ -32,6 +32,60 @@ Once artist enters the Mongo URL address, OpenPype will remember the connection
next launch, so it is a one time process.From that moment OpenPype will do it's best to
always keep up to date with the latest studio updates.
If the launch was successfull, the artist should see a green OpenPype logo in their
If the launch was successful, the artist should see a green OpenPype logo in their
tray menu. Keep in mind that on Windows this icon might be hidden by default, in which case,
the artist can simply drag the icon down to the tray.
the artist can simply drag the icon down to the tray.
You can use following command line arguments:
`--use-version` - to specify version you want to run explicitly, like:
```shell
openpype_console --use-version=3.0.1
```
`--use-staging` - to specify you prefer staging version. In that case it will be used
(if found) instead of production one.
### Details
When you run OpenPype from executable, few check are made:
#### Check for mongoDB database connection
MongoDB connection string is in format:
```shell
mongodb[+srv]://[username:password@]host1[:port1][,...hostN[:portN]][/[defaultauthdb][?options]
```
More on that in [MongoDB documentation](https://docs.mongodb.com/manual/reference/connection-string/).
Example connection strings are `mongodb://local-unprotected-server:2707` or
`mongodb+srv://user:superpassword:some.mongodb-hosted-on.net:27072`.
When you start OpenPype first time, Igniter UI will show up and ask you for this string. It will then
save it in secure way to your systems keyring - on Windows it is **Credential Manager**, on MacOS it will use its
**Keychain**, on Linux it can be **GNOME Keyring** or other software, depending on your distribution.
This can be also set beforehand with environment variable `OPENPYPE_MONGO`. If set it takes precedence
over the one set in keyring.
#### Check for OpenPype version path
When connection to MongoDB is made, OpenPype will get various settings from there - one among them is
directory location where OpenPype versions are stored. If this directory exists OpenPype tries to
find the latest version there and if succeed it will copy it to user system and use it.
This path can be set is OpenPype settings, but also with environment variable `OPENPYPE_PATH` or with
`openPypePath` in json file located application directory depending on your system.
- Windows: `%LOCALAPPDATA%\pypeclub\openpype`
- Linux: `~/.local/share/pypeclub/openpype`
- Mac: `~/Library/Application Support/pypeclub/openpype`
### Runtime provided environment variables
OpenPype is providing following environment variables for its subprocesses that can be used
in various places, like scripting, etc.
- `OPENPYPE_ROOT` - this will point to currently running code.
- `OPENPYPE_VERSION` - string of current version - like `3.0.0-foo+bar`
- `OPENPYPE_REPOS_ROOT` - this is path where all components can be find (like Avalon Core and OpenPype)
- `OPENPYPE_DATABASE_NAME` - database name in MongoDB used by OpenPype
- `OPENPYPE_EXECUTABLE` - path to executable used to run OpenPype - when run from sources it will point
to **python** stored in virtual environment. If run from frozen code, it will point to either `openpype_gui` or
`openpype_console`.

View file

@ -177,6 +177,22 @@ Library loader is extended [loader](#loader) which allows to load published subs
</div>
</div>
### Delivery Action ###
Library Loader contains functionality to export any selected asset, subsets and their version to configurable folder.
Delivery follows structure based on defined template, this template must be configured first by Admin in the Settings.
![delivery_action](assets/tools/tools_delivery_loader.png)
* Usage
- Select all required subsets for export (you can change theirs versions by double clicking on 'Version' value)
- Right click and select **Deliver Versions** from context menu
- Select predefined Delivery template (must be configured by Admin system or project wide)
- Fill value for root folder (folder will be created if it doesn't exist)
- Filter out type of representation you are not interested in
- Push **Deliver** button
- Dialog must be kept open until export is finished
- In a case of problems with any of the representation, that one will be skipped, description of error will be provided in the dialog
* * *
## Publisher

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -7,14 +7,31 @@ sidebar_label: Build
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
## Introduction
To build Pype you currently need (on all platforms):
- **[Python 3.7](https://www.python.org/downloads/)** as we are following [vfx platform](https://vfxplatform.com).
- **[git](https://git-scm.com/downloads)**
We use [CX_Freeze](https://cx-freeze.readthedocs.io/en/latest) to freeze the code and all dependencies.
We use [CX_Freeze](https://cx-freeze.readthedocs.io/en/latest) to freeze the code and all dependencies and
[Poetry](https://python-poetry.org/) for virtual environment management.
This is outline of build steps. Most of them are done automatically via scripts:
- Virtual environment is created using **Poetry** in `.venv`
- Necessary third-party tools (like [ffmpeg](https://www.ffmpeg.org/), [OpenImageIO](https://github.com/OpenImageIO/oiio)
and [usd libraries](https://developer.nvidia.com/usd)) are downloaded to `./vendor/bin`
- OpenPype code is frozen with **cx_freeze** to `./build`
- Modules are moved from `lib` to `dependencies` to solve some Python 2 / Python 3 clashes
- On Mac application bundle and dmg image will be created from built code.
- On Windows, you can create executable installer with `./tools/build_win_installer.ps1`
### Clone OpenPype repository:
```powershell
git clone --recurse-submodules https://github.com/pypeclub/OpenPype.git
```
## Platform specific steps
<Tabs
groupId="platforms"
@ -27,32 +44,31 @@ We use [CX_Freeze](https://cx-freeze.readthedocs.io/en/latest) to freeze the cod
<TabItem value="win">
### Windows
More tools might be needed for installing some dependencies (for example for **OpenTimelineIO**) - mostly
development tools like [CMake](https://cmake.org/) and [Visual Studio](https://visualstudio.microsoft.com/cs/downloads/)
### Clone repository:
```sh
git clone --recurse-submodules git@github.com:pypeclub/pype.git
```
### Run from source
#### Run from source
For development purposes it is possible to run OpenPype directly from the source. We provide a simple launcher script for this.
To start OpenPype from source you need to
1) Run `.\tools\create_env.ps1` to create virtual environment in `.\venv`
2) Run `.\tools\run_tray.ps1` if you have all required dependencies on your machine you should be greeted with OpenPype igniter window and once you give it your Mongo URL, with OpenPype icon in the system tray.
1. Run `.\tools\create_env.ps1` to create virtual environment in `.venv`
2. Run `.\tools\fetch_thirdparty_libs.ps1` to get **ffmpeg**, **oiio** and other tools needed.
3. Run `.\tools\run_tray.ps1` if you have all required dependencies on your machine you should be greeted with OpenPype igniter window and once you give it your Mongo URL, with OpenPype icon in the system tray.
Step 1 and 2 needs to be run only once (or when something was changed).
#### To build OpenPype:
1. Run `.\tools\create_env.ps1` to create virtual environment in `.venv`
2. Run `.\tools\fetch_thirdparty_libs.ps1` to get **ffmpeg**, **oiio** and other tools needed.
3. `.\tools\build.ps1` to build OpenPype to `.\build`
### To build OpenPype:
1) Run `.\tools\create_env.ps1` to create virtual environment in `.\venv`
2) Run `.\tools\build.ps1` to build pype executables in `.\build\`
To create distributable OpenPype versions, run `./tools/create_zip.ps1` - that will
To create distributable OpenPype versions, run `.\tools\create_zip.ps1` - that will
create zip file with name `pype-vx.x.x.zip` parsed from current pype repository and
copy it to user data dir. You can specify `--path /path/to/zip` to force it into a different
copy it to user data dir. You can specify `--path \path\to\zip` to force it into a different
location. This can be used to prepare new version releases for artists in the studio environment
without the need to re-build the whole package
@ -61,27 +77,33 @@ without the need to re-build the whole package
</TabItem>
<TabItem value="linux">
### Linux
#### Docker
You can use Docker to build OpenPype. Just run:
```sh
sudo ./tools/docker_build.sh
```shell
$ sudo ./tools/docker_build.sh
```
and you should have built OpenPype in `build` directory. It is using **Centos 7**
as a base image.
You can pull the image:
```sh
```shell
# replace 3.0.0 tag with version you want
docker pull pypeclub/openpype:3.0.0
$ docker pull pypeclub/openpype:3.0.0
```
See https://hub.docker.com/r/pypeclub/openpype/tag for more.
Beware that as Python is built against some libraries version in Centos 7 base image,
those might not be available in linux version you are using. We try to handle those we
found (libffi, libcrypto/ssl, etc.) but there might be more.
#### Manual build
To build OpenPype on Linux you wil need:
To build OpenPype on Linux you will need:
- **[curl](https://curl.se)** on systems that doesn't have one preinstalled.
- Python header files installed (**python3-dev** on Ubuntu for example).
- **bzip2**, **readline**, **sqlite3** and other libraries.
Because some Linux distros come with newer Python version pre-installed, you might
@ -90,117 +112,283 @@ Your best bet is probably using [pyenv](https://github.com/pyenv/pyenv).
You can use your package manager to install **git** and other packages to your build
environment.
Use curl for pyenv installation
#### Common steps for all Distros
Use pyenv to prepare Python version for Pype build
```shell
$ curl https://pyenv.run | bash
# you can add those to ~/.bashrc
$ export PATH="$HOME/.pyenv/bin:$PATH"
$ eval "$(pyenv init -)"
$ eval "$(pyenv virtualenv-init -)"
# reload shell
$ exec $SHELL
# install Python 3.7.10
# python will be downloaded and build so please make sure
# you have all necessary requirements installed (see bellow).
$ pyenv install -v 3.7.10
# change path to pype 3
$ cd /path/to/pype-3
# set local python version
$ pyenv local 3.7.9
```
:::note Install build requirements for **Ubuntu**
```sh
```shell
sudo apt-get update; sudo apt-get install --no-install-recommends make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git
```
In case you run in error about `xcb` when running Pype,
you'll need also additional libraries for Qt5:
```sh
```shell
sudo apt install qt5-default
```
:::
:::note Install build requirements for **Centos**
:::note Install build requirements for **Centos 7**
```sh
yum install gcc zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel tk-devel libffi-devel git
```shell
$ sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
$ sudo yum install centos-release-scl
$ sudo yum install bash which git devtoolset-7-gcc* \
make cmake curl wget gcc zlib-devel bzip2 \
bzip2-devel readline-devel sqlite sqlite-devel \
openssl-devel tk-devel libffi-devel qt5-qtbase-devel \
patchelf
```
In case you run in error about `xcb` when running Pype,
you'll need also additional libraries for Qt5:
```sh
sudo yum install qt5-qtbase-devel
```
:::
For more information about setting your build environmet please refer to [pyenv suggested build environment](https://github.com/pyenv/pyenv/wiki#suggested-build-environment)
:::note Install build requirements for other distros
#### Common steps for all Distros
Build process usually needs some reasonably recent versions of libraries and tools. You
can follow what's needed for Ubuntu and change it for your package manager. Centos 7 steps
have additional magic to overcame very old versions.
:::
Use pyenv to prepare Python version for Pype build
For more information about setting your build environment please refer to [pyenv suggested build environment](https://github.com/pyenv/pyenv/wiki#suggested-build-environment).
```sh
curl https://pyenv.run | bash
# you can add those to ~/.bashrc
export PATH="$HOME/.pyenv/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
# reload shell
exec $SHELL
# install Python 3.7.9
pyenv install -v 3.7.9
# change path to pype 3
cd /path/to/pype-3
# set local python version
pyenv local 3.7.9
```
#### To build Pype:
1. Run `.\tools\create_env.sh` to create virtual environment in `.\venv`
2. Run `.\tools\build.sh` to build pype executables in `.\build\`
1. Run `./tools/create_env.sh` to create virtual environment in `./venv`
2. Run `./tools/fetch_thirdparty_libs.sh` to get **ffmpeg**, **oiio** and other tools needed.
3. Run `./tools/build.sh` to build pype executables in `.\build\`
</TabItem>
<TabItem value="mac">
### MacOS
To build pype on MacOS you wil need:
- **[Homebrew](https://brew.sh)**, Easy way of installing everything necessary is to use.
- **[Homebrew](https://brew.sh)** - easy way of installing everything necessary.
- **[CMake](https://cmake.org/)** to build some external OpenPype dependencies.
- **XCode Command Line Tools** (or some other build system)
- **[create-dmg](https://formulae.brew.sh/formula/create-dmg)** to create dmg image from application
bundle.
1) Install **Homebrew**:
```sh
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```shell
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
2) Install **cmake**:
```sh
brew install cmake
```shell
$ brew install cmake
```
3) Install [pyenv](https://github.com/pyenv/pyenv):
```sh
brew install pyenv
echo 'eval "$(pypenv init -)"' >> ~/.zshrc
pyenv init
exec "$SHELL"
PATH=$(pyenv root)/shims:$PATH
```shell
$ brew install pyenv
$ echo 'eval "$(pypenv init -)"' >> ~/.zshrc
$ pyenv init
$ exec "$SHELL"
$ PATH=$(pyenv root)/shims:$PATH
```
4) Pull in required Python version 3.7.x
```sh
```shell
# install Python build dependences
brew install openssl readline sqlite3 xz zlib
$ brew install openssl readline sqlite3 xz zlib
# replace with up-to-date 3.7.x version
pyenv install 3.7.9
$ pyenv install 3.7.9
```
5) Set local Python version
```sh
```shell
# switch to Pype source directory
pyenv local 3.7.9
$ pyenv local 3.7.9
```
6) Install `create-dmg`
```shell
$ brew install create-dmg
```
#### To build Pype:
1. Run `.\tools\create_env.sh` to create virtual environment in `.\venv`
2. Run `.\tools\build.sh` to build Pype executables in `.\build\`
1. Run `./tools/create_env.sh` to create virtual environment in `./venv`.
2. Run `./tools/fetch_thirdparty_libs.sh` to get **ffmpeg**, **oiio** and other tools needed.
3. Run `./tools/build.sh` to build OpenPype Application bundle in `./build/`.
</TabItem>
</Tabs>
## Adding dependencies
### Python modules
If you are extending OpenPype and you need some new modules not included, you can add them
to `pyproject.toml` to `[tool.poetry.dependencies]` section.
```toml title="/pyproject.toml"
[tool.poetry.dependencies]
python = "3.7.*"
aiohttp = "^3.7"
aiohttp_json_rpc = "*" # TVPaint server
acre = { git = "https://github.com/pypeclub/acre.git" }
opentimelineio = { version = "0.14.0.dev1", source = "openpype" }
#...
```
It is useful to add comment to it so others can see why this was added and where it is used.
As you can see you can add git repositories or custom wheels (those must be
added to `[[tool.poetry.source]]` section).
To add something only for specific platform, you can use markers like:
```toml title="Install pywin32 only on Windows"
pywin32 = { version = "300", markers = "sys_platform == 'win32'" }
```
For more information see [Poetry documentation](https://python-poetry.org/docs/dependency-specification/).
### Binary dependencies
To add some binary tool or something that doesn't fit standard Python distribution methods, you
can use [fetch_thirdparty_libs](#fetch_thirdparty_libs) script. It will take things defined in
`pyproject.toml` under `[openpype]` section like this:
```toml title="/pyproject.toml"
[openpype]
[openpype.thirdparty.ffmpeg.windows]
url = "https://distribute.openpype.io/thirdparty/ffmpeg-4.4-windows.zip"
hash = "dd51ba29d64ee238e7c4c3c7301b19754c3f0ee2e2a729c20a0e2789e72db925"
# ...
```
This defines FFMpeg for Windows. It will be downloaded from specified url, its checksum will
be validated (it's sha256) and it will be extracted to `/vendor/bin/ffmpeg/windows` (partly taken
from its section name).
## Script tools
(replace extension with the one for your system - `ps1` for windows, `sh` for linux/macos)
### build
This will build OpenPype to `build` directory. If virtual environment is not created yet, it will
install [Poetry](https://python-poetry.org/) and using it download and install necessary
packages needed for build. It is recommended that you run [fetch_thirdparty_libs](#fetch_thirdparty_libs)
to download FFMpeg, OpenImageIO and others that are needed by OpenPype and are copied during the build.
#### Arguments
`--no-submodule-update` - to disable updating submodules. This allows to make custom-builds for testing
feature changes in submodules.
### build_win_installer
This will take already existing build in `build` directory and create executable installer using
[Inno Setup](https://jrsoftware.org/isinfo.php) and definitions in `./inno_setup.iss`. You need OpenPype
build using [build script](#build), Inno Setup installed and in PATH before running this script.
:::note
Windows only
:::
### create_env
Script to create virtual environment for build and running OpenPype from sources. It is using
[Poetry](https://python-poetry.org/). All dependencies are defined in `pyproject.toml`, resolved by
Poetry into `poetry.lock` file and then installed. Running this script without Poetry will download
it, install it to `.poetry` and then install virtual environment from `poetry.lock` file. If you want
to update packages version, just run `poetry update` or delete lock file.
#### Arguments
`--verbose` - to increase verbosity of Poetry. This can be useful for debugging package conflicts.
### create_zip
Script to create packaged OpenPype version from current sources. This will strip developer stuff and
package it into zip that can be used for [auto-updates for studio wide distributions](admin_distribute#automatic-updates), etc.
Same as:
```shell
poetry run python ./tools/create_zip.py
```
### docker_build.sh
Script to build OpenPype on [Docker](https://www.docker.com/) enabled systems - usually Linux and Windows
with [Docker Desktop](https://docs.docker.com/docker-for-windows/install/)
and [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (WSL) installed.
It must be run with administrative privileges - `sudo ./docker_build.sh`.
It will use **Centos 7** base image to build OpenPype. You'll see your build in `./build` folder.
### fetch_thirdparty_libs
This script will download necessary tools for OpenPype defined in `pyproject.toml` like FFMpeg,
OpenImageIO and USD libraries and put them to `./vendor/bin`. Those are then included in build.
Running it will overwrite everything on their respective paths.
Same as:
```shell
poetry run python ./tools/fetch_thirdparty_libs.py
```
### make_docs
Script will run [sphinx](https://www.sphinx-doc.org/) to build api documentation in html. You
should see it then under `./docs/build/html`.
### run_documentation
This will start up [Docusaurus](https://docusaurus.io/) to display OpenPype user documentation.
Useful for offline browsing or editing documentation itself. You will need [Node.js](https://nodejs.org/)
and [Yarn](https://yarnpkg.com/) to run this script. After executing it, you'll see new
browser window with current OpenPype documentation.
Same as:
```shell
cd ./website
yarn start
```
### run_mongo
Helper script to run local mongoDB server for development and testing. You will need
[mongoDB server](https://www.mongodb.com/try/download/community) installed in standard location
or in PATH (standard location works only on Windows). It will start by default on port `2707` and
it will put its db files to `../mongo_db_data` relative to OpenPype sources.
### run_project_manager
Helper script to start OpenPype Project Manager tool.
Same as:
```shell
poetry run python start.py projectmanager
```
### run_settings
Helper script to open OpenPype Settings UI.
Same as:
```shell
poetry run python start.py settings --dev
```
### run_tests
Runs OpenPype test suite.
### run_tray
Helper script to run OpenPype Tray.
Same as:
```shell
poetry run python start.py tray
```
### update_submodules
Helper script to update OpenPype git submodules.
Same as:
```shell
git submodule update --recursive --remote
```

View file

@ -1,5 +1,6 @@
{
"name": "pype-documentation",
"license": "MIT",
"scripts": {
"examples": "docusaurus-examples",
"start": "docusaurus start",
@ -13,8 +14,8 @@
"docusaurus": "docusaurus"
},
"dependencies": {
"@docusaurus/core": "2.0.0-alpha.72",
"@docusaurus/preset-classic": "2.0.0-alpha.72",
"@docusaurus/core": "2.0.0-beta.0",
"@docusaurus/preset-classic": "2.0.0-beta.0",
"classnames": "^2.2.6",
"clsx": "^1.1.1",
"react": "^16.10.2",

File diff suppressed because it is too large Load diff