From a7150bd6f1c9494734f03265eecbe86ff284d882 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 6 Oct 2022 17:28:30 +0200 Subject: [PATCH] OP-4181 - clean up after review comments --- igniter/GPUCache/data_0 | Bin 0 -> 8192 bytes igniter/GPUCache/data_1 | Bin 0 -> 270336 bytes igniter/GPUCache/data_2 | Bin 0 -> 8192 bytes igniter/GPUCache/data_3 | Bin 0 -> 8192 bytes igniter/GPUCache/index | Bin 0 -> 262512 bytes openpype/hooks/pre_python2_prelaunch.py | 35 + openpype/hosts/photoshop/tests/expr.py | 51 + openpype/lib/token | 1 + .../event_handlers_user/action_edl_create.py | 275 ++++ openpype/pipeline/temp_anatomy.py | 1330 +++++++++++++++++ .../plugins/publish/integrate_hero_version.py | 9 +- .../_process_referenced_pipeline_result.json | 92 ++ tests/unit/test_unzip.py | 11 + vendor/configs/OpenColorIO-Configs | 1 + vendor/instance.json | 1133 ++++++++++++++ vendor/response.json | 1 + vendor/temp.json | 46 + 17 files changed, 2982 insertions(+), 3 deletions(-) create mode 100644 igniter/GPUCache/data_0 create mode 100644 igniter/GPUCache/data_1 create mode 100644 igniter/GPUCache/data_2 create mode 100644 igniter/GPUCache/data_3 create mode 100644 igniter/GPUCache/index create mode 100644 openpype/hooks/pre_python2_prelaunch.py create mode 100644 openpype/hosts/photoshop/tests/expr.py create mode 100644 openpype/lib/token create mode 100644 openpype/modules/ftrack/event_handlers_user/action_edl_create.py create mode 100644 openpype/pipeline/temp_anatomy.py create mode 100644 tests/unit/openpype/lib/resources/_process_referenced_pipeline_result.json create mode 100644 tests/unit/test_unzip.py create mode 160000 vendor/configs/OpenColorIO-Configs create mode 100644 vendor/instance.json create mode 100644 vendor/response.json create mode 100644 vendor/temp.json diff --git a/igniter/GPUCache/data_0 b/igniter/GPUCache/data_0 new file mode 100644 index 0000000000000000000000000000000000000000..d76fb77e93ac8a536b5dbade616d63abd00626c5 GIT binary patch literal 8192 zcmeIuK?wjL5Jka{7-jo+5O1auw}mk8@B+*}b0s6M>Kg$91PBlyK!5-N0t5&UAV7cs W0RjXF5FkK+009C72oNCfo4^Gh&;oe? literal 0 HcmV?d00001 diff --git a/igniter/GPUCache/data_1 b/igniter/GPUCache/data_1 new file mode 100644 index 0000000000000000000000000000000000000000..212f73166781160e472f8e76c3b9998b3775ecb7 GIT binary patch literal 270336 zcmeI%u?@m75CFhW@CfV>3QP3t%>amw0a8XffrJVo)0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N c0t5&UAV7cs0RjXF5FkK+009C72>hqO2eI!7!2kdN literal 0 HcmV?d00001 diff --git a/igniter/GPUCache/data_2 b/igniter/GPUCache/data_2 new file mode 100644 index 0000000000000000000000000000000000000000..c7e2eb9adcfb2d3313ec85f5c28cedda950a3f9b GIT binary patch literal 8192 zcmeIu!3h8`2n0b1_TQ7_m#U&=2(t%Qz}%M=ae7_Oi2wlt1PBlyK!5-N0t5&UAV7cs V0RjXF5FkK+009C72oTsN@Bv`}0$Tt8 literal 0 HcmV?d00001 diff --git a/igniter/GPUCache/data_3 b/igniter/GPUCache/data_3 new file mode 100644 index 0000000000000000000000000000000000000000..5eec97358cf550862fd343fc9a73c159d4c0ab10 GIT binary patch literal 8192 zcmeIuK@9*P5CpLeAOQbv2)|PW$RO!FMnHFsm9+HS=9>r*AV7cs0RjXF5FkK+009C7 W2oNAZfB*pk1PBlyK!5;&-vkZ-dID$w literal 0 HcmV?d00001 diff --git a/igniter/GPUCache/index b/igniter/GPUCache/index new file mode 100644 index 0000000000000000000000000000000000000000..b2998cfef1a6457e5cfe9dc37e029bdbe0a7f778 GIT binary patch literal 262512 zcmeIuu?>JQ00XcT9zZ-&b?3`o&{Gf_S0S;K0~nnt$>{4|&t%Cr+dIlg%Diho_EzWC z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjYm G5qJP2>jZZI literal 0 HcmV?d00001 diff --git a/openpype/hooks/pre_python2_prelaunch.py b/openpype/hooks/pre_python2_prelaunch.py new file mode 100644 index 00000000000..84272d2e5d7 --- /dev/null +++ b/openpype/hooks/pre_python2_prelaunch.py @@ -0,0 +1,35 @@ +import os +from openpype.lib import PreLaunchHook + + +class PrePython2Vendor(PreLaunchHook): + """Prepend python 2 dependencies for py2 hosts.""" + order = 10 + + def execute(self): + if not self.application.use_python_2: + return + + # Prepare vendor dir path + self.log.info("adding global python 2 vendor") + pype_root = os.getenv("OPENPYPE_REPOS_ROOT") + python_2_vendor = os.path.join( + pype_root, + "openpype", + "vendor", + "python", + "python_2" + ) + + # Add Python 2 modules + python_paths = [ + python_2_vendor + ] + + # Load PYTHONPATH from current launch context + python_path = self.launch_context.env.get("PYTHONPATH") + if python_path: + python_paths.append(python_path) + + # Set new PYTHONPATH to launch context environments + self.launch_context.env["PYTHONPATH"] = os.pathsep.join(python_paths) diff --git a/openpype/hosts/photoshop/tests/expr.py b/openpype/hosts/photoshop/tests/expr.py new file mode 100644 index 00000000000..ff796f417c3 --- /dev/null +++ b/openpype/hosts/photoshop/tests/expr.py @@ -0,0 +1,51 @@ +import json + +data = [ + { + "schema": "openpype:container-2.0", + "id": "pyblish.avalon.container", + "name": "imageArtNeew", + "namespace": "Jungle_imageArtNeew_001", + "loader": "ReferenceLoader", + "representation": "61c1eb91e1a4d1e5a23582f6", + "members": [ + "131" + ] + }, + { + "id": "pyblish.avalon.instance", + "family": "image", + "asset": "Jungle", + "subset": "imageMainBg", + "active": True, + "variant": "Main", + "uuid": "199", + "long_name": "BG" + }, + { + "id": "pyblish.avalon.instance", + "family": "image", + "asset": "Jungle", + "subset": "imageMain", + "active": True, + "variant": "Main", + "uuid": "192", + "long_name": "imageMain" + }, + { + "id": "pyblish.avalon.instance", + "family": "workfile", + "subset": "workfile", + "active": True, + "creator_identifier": "workfile", + "asset": "Jungle", + "task": "art", + "variant": "", + "instance_id": "3ed19342-cd8e-4bb6-8cda-d6e74d9a7efe", + "creator_attributes": {}, + "publish_attributes": {} + } +] + +with open("C:\\Users\\petrk\\PycharmProjects\\Pype3.0\\pype\\openpype\\hosts\\photoshop\\tests\\mock_get_layers_metadata.json", 'w') as fp: + fp.write(json.dumps(data, indent=4)) \ No newline at end of file diff --git a/openpype/lib/token b/openpype/lib/token new file mode 100644 index 00000000000..193a2aac957 --- /dev/null +++ b/openpype/lib/token @@ -0,0 +1 @@ +5d58370a7702b2efee5120704246baf4abb865323fc9db9a04827bfb478569d6 \ No newline at end of file diff --git a/openpype/modules/ftrack/event_handlers_user/action_edl_create.py b/openpype/modules/ftrack/event_handlers_user/action_edl_create.py new file mode 100644 index 00000000000..7ac139ae630 --- /dev/null +++ b/openpype/modules/ftrack/event_handlers_user/action_edl_create.py @@ -0,0 +1,275 @@ +import os +import subprocess +import tempfile +import shutil +import json +import sys + +import opentimelineio as otio +import ftrack_api +import requests + +from openpype_modules.ftrack.lib import BaseAction + + +def download_file(url, path): + with open(path, "wb") as f: + print("\nDownloading %s" % path) + response = requests.get(url, stream=True) + total_length = response.headers.get('content-length') + + if total_length is None: + f.write(response.content) + else: + dl = 0 + total_length = int(total_length) + for data in response.iter_content(chunk_size=4096): + dl += len(data) + f.write(data) + done = int(50 * dl / total_length) + sys.stdout.write("\r[%s%s]" % ('=' * done, ' ' * (50-done))) + sys.stdout.flush() + + +class ExportEditorialAction(BaseAction): + '''Export Editorial action''' + + label = "Export Editorial" + variant = None + identifier = "export-editorial" + description = None + component_name_order = ["exr", "mov", "ftrackreview-mp4_src"] + + def export_editorial(self, entity, output_path): + session = ftrack_api.Session() + unmanaged_location = session.query( + "Location where name is \"ftrack.unmanaged\"" + ).one() + temp_path = tempfile.mkdtemp() + + files = {} + for obj in entity["review_session_objects"]: + data = {} + parent_name = obj["asset_version"]["asset"]["parent"]["name"] + component_query = "Component where version_id is \"{}\"" + component_query += " and name is \"{}\"" + for name in self.component_name_order: + try: + component = session.query( + component_query.format( + obj["asset_version"]["id"], name + ) + ).one() + path = unmanaged_location.get_filesystem_path(component) + data["path"] = path.replace("\\", "/") + break + except ftrack_api.exception.NoResultFoundError: + pass + + # Download online review if not local path found. + if "path" not in data: + component = session.query( + component_query.format( + obj["asset_version"]["id"], "ftrackreview-mp4" + ) + ).one() + location = component["component_locations"][0] + component_url = location["location"].get_url(component) + asset_name = obj["asset_version"]["asset"]["name"] + version = obj["asset_version"]["version"] + filename = "{}_{}_v{:03d}.mp4".format( + parent_name, asset_name, version + ) + filepath = os.path.join( + output_path, "downloads", filename + ).replace("\\", "/") + + if not os.path.exists(os.path.dirname(filepath)): + os.makedirs(os.path.dirname(filepath)) + + download_file(component_url, filepath) + data["path"] = filepath + + # Get frame duration and framerate. + query = "Component where version_id is \"{}\"" + query += " and name is \"ftrackreview-mp4\"" + component = session.query( + query.format(obj["asset_version"]["id"]) + ).one() + metadata = json.loads(component["metadata"]["ftr_meta"]) + data["framerate"] = metadata["frameRate"] + data["frames"] = metadata["frameOut"] - metadata["frameIn"] + + # Find audio if it exists. + query = "Asset where parent.id is \"{}\"" + query += " and type.name is \"Audio\"" + asset = session.query( + query.format(obj["asset_version"]["asset"]["parent"]["id"]) + ) + if asset: + asset_version = asset[0]["versions"][-1] + query = "Component where version_id is \"{}\"" + query += " and name is \"{}\"" + comp = session.query( + query.format(asset_version["id"], "wav") + ).one() + src = unmanaged_location.get_filesystem_path(comp) + dst = os.path.join(temp_path, parent_name + ".wav") + shutil.copy(src, dst) + + # Collect data. + files[parent_name] = data + + clips = [] + for name, data in files.items(): + self.log.info("Processing {} with {}".format(name, data)) + f = data["path"] + range = otio.opentime.TimeRange( + start_time=otio.opentime.RationalTime(0, data["framerate"]), + duration=otio.opentime.RationalTime( + data["frames"], data["framerate"] + ) + ) + + media_reference = otio.schema.ExternalReference( + available_range=range, + target_url=f"file://{f}" + ) + + clip = otio.schema.Clip( + name=name, + media_reference=media_reference, + source_range=range + ) + clips.append(clip) + + # path = os.path.join(temp_path, name + ".wav").replace("\\", "/") + # if not os.path.exists(path): + # args = ["ffmpeg", "-y", "-i", f, path] + # self.log.info(subprocess.list2cmdline(args)) + # subprocess.call(args) + + timeline = otio.schema.timeline_from_clips(clips) + otio.adapters.write_to_file( + timeline, os.path.join(output_path, entity["name"] + ".xml") + ) + + data = "" + for f in os.listdir(temp_path): + f = f.replace("\\", "/") + data += f"file '{f}'\n" + + path = os.path.join(temp_path, "temp.txt") + with open(path, "w") as f: + f.write(data) + + args = [ + "ffmpeg", "-y", "-f", "concat", "-safe", "0", + "-i", os.path.basename(path), + os.path.join(output_path, entity["name"] + ".wav") + ] + self.log.info(subprocess.list2cmdline(args)) + subprocess.call(args, cwd=temp_path) + + shutil.rmtree(temp_path) + + def discover(self, session, entities, event): + '''Return true if we can handle the selected entities. + *session* is a `ftrack_api.Session` instance + *entities* is a list of tuples each containing the entity type and the + entity id. + If the entity is a hierarchical you will always get the entity + type TypedContext, once retrieved through a get operation you + will have the "real" entity type ie. example Shot, Sequence + or Asset Build. + *event* the unmodified original event + ''' + if len(entities) == 1: + if entities[0].entity_type == "ReviewSession": + return True + + return False + + def launch(self, session, entities, event): + '''Callback method for the custom action. + return either a bool ( True if successful or False if the action + failed ) or a dictionary with they keys `message` and `success`, the + message should be a string and will be displayed as feedback to the + user, success should be a bool, True if successful or False if the + action failed. + *session* is a `ftrack_api.Session` instance + *entities* is a list of tuples each containing the entity type and the + entity id. + If the entity is a hierarchical you will always get the entity + type TypedContext, once retrieved through a get operation you + will have the "real" entity type ie. example Shot, Sequence + or Asset Build. + *event* the unmodified original event + ''' + if 'values' in event['data']: + userId = event['source']['user']['id'] + user = session.query('User where id is ' + userId).one() + job = session.create( + 'Job', + { + 'user': user, + 'status': 'running', + 'data': json.dumps({ + 'description': 'Export Editorial.' + }) + } + ) + session.commit() + + try: + output_path = event["data"]["values"]["output_path"] + + if not os.path.exists(output_path): + os.makedirs(output_path) + + self.export_editorial(entities[0], output_path) + + job['status'] = 'done' + session.commit() + except Exception: + session.rollback() + job["status"] = "failed" + session.commit() + self.log.error( + "Exporting editorial failed ({})", exc_info=True + ) + + return { + 'success': True, + 'message': 'Action completed successfully' + } + + items = [ + { + 'label': 'Output folder:', + 'type': 'text', + 'value': '', + 'name': 'output_path' + } + + ] + return { + 'success': True, + 'message': "", + 'items': items + } + + +def register(session): + '''Register action. Called when used as an event plugin.''' + + ExportEditorialAction(session).register() + + +if __name__ == "__main__": + session = ftrack_api.Session() + action = ExportEditorialAction(session) + id = "bfe0477c-d5a8-49d8-88b9-6d44d2e48fd9" + review_session = session.get("ReviewSession", id) + path = r"c:/projects" + action.export_editorial(review_session, path) \ No newline at end of file diff --git a/openpype/pipeline/temp_anatomy.py b/openpype/pipeline/temp_anatomy.py new file mode 100644 index 00000000000..27a9370928a --- /dev/null +++ b/openpype/pipeline/temp_anatomy.py @@ -0,0 +1,1330 @@ +import os +import re +import copy +import platform +import collections +import numbers + +import six +import time + +from openpype.settings.lib import ( + get_anatomy_settings, + get_project_settings, + get_default_project_settings, + get_local_settings +) + +from openpype.client import get_project +from openpype.lib.path_templates import ( + TemplateUnsolved, + TemplateResult, + TemplatesDict, + FormatObject, +) +from openpype.lib.log import Logger +from openpype.lib import get_local_site_id + +log = Logger.get_logger(__name__) + + +class ProjectNotSet(Exception): + """Exception raised when is created Anatomy without project name.""" + + +class RootCombinationError(Exception): + """This exception is raised when templates has combined root types.""" + + def __init__(self, roots): + joined_roots = ", ".join( + ["\"{}\"".format(_root) for _root in roots] + ) + # TODO better error message + msg = ( + "Combination of root with and" + " without root name in AnatomyTemplates. {}" + ).format(joined_roots) + + super(RootCombinationError, self).__init__(msg) + + +class BaseAnatomy(object): + """Anatomy module helps to keep project settings. + + Wraps key project specifications, AnatomyTemplates and Roots. + """ + + def __init__(self, project_doc, local_settings): + project_name = project_doc["name"] + self.project_name = project_name + + self._data = self._prepare_anatomy_data( + project_doc, local_settings + ) + self._templates_obj = AnatomyTemplates(self) + self._roots_obj = Roots(self) + + root_key_regex = re.compile(r"{(root?[^}]+)}") + root_name_regex = re.compile(r"root\[([^]]+)\]") + + # Anatomy used as dictionary + # - implemented only getters returning copy + def __getitem__(self, key): + return copy.deepcopy(self._data[key]) + + def get(self, key, default=None): + return copy.deepcopy(self._data).get(key, default) + + def keys(self): + return copy.deepcopy(self._data).keys() + + def values(self): + return copy.deepcopy(self._data).values() + + def items(self): + return copy.deepcopy(self._data).items() + + @staticmethod + def _prepare_anatomy_data(anatomy_data): + """Prepare anatomy data for further processing. + + Method added to replace `{task}` with `{task[name]}` in templates. + """ + templates_data = anatomy_data.get("templates") + if templates_data: + # Replace `{task}` with `{task[name]}` in templates + value_queue = collections.deque() + value_queue.append(templates_data) + while value_queue: + item = value_queue.popleft() + if not isinstance(item, dict): + continue + + for key in tuple(item.keys()): + value = item[key] + if isinstance(value, dict): + value_queue.append(value) + + elif isinstance(value, six.string_types): + item[key] = value.replace("{task}", "{task[name]}") + return anatomy_data + + def reset(self): + """Reset values of cached data in templates and roots objects.""" + self._data = self._prepare_anatomy_data( + get_anatomy_settings(self.project_name, self._site_name) + ) + self.templates_obj.reset() + self.roots_obj.reset() + + @property + def templates(self): + """Wrap property `templates` of Anatomy's AnatomyTemplates instance.""" + return self._templates_obj.templates + + @property + def templates_obj(self): + """Return `AnatomyTemplates` object of current Anatomy instance.""" + return self._templates_obj + + def format(self, *args, **kwargs): + """Wrap `format` method of Anatomy's `templates_obj`.""" + return self._templates_obj.format(*args, **kwargs) + + def format_all(self, *args, **kwargs): + """Wrap `format_all` method of Anatomy's `templates_obj`.""" + return self._templates_obj.format_all(*args, **kwargs) + + @property + def roots(self): + """Wrap `roots` property of Anatomy's `roots_obj`.""" + return self._roots_obj.roots + + @property + def roots_obj(self): + """Return `Roots` object of current Anatomy instance.""" + return self._roots_obj + + def root_environments(self): + """Return OPENPYPE_ROOT_* environments for current project in dict.""" + return self._roots_obj.root_environments() + + def root_environmets_fill_data(self, template=None): + """Environment variable values in dictionary for rootless path. + + Args: + template (str): Template for environment variable key fill. + By default is set to `"${}"`. + """ + return self.roots_obj.root_environmets_fill_data(template) + + def find_root_template_from_path(self, *args, **kwargs): + """Wrapper for Roots `find_root_template_from_path`.""" + return self.roots_obj.find_root_template_from_path(*args, **kwargs) + + def path_remapper(self, *args, **kwargs): + """Wrapper for Roots `path_remapper`.""" + return self.roots_obj.path_remapper(*args, **kwargs) + + def all_root_paths(self): + """Wrapper for Roots `all_root_paths`.""" + return self.roots_obj.all_root_paths() + + def set_root_environments(self): + """Set OPENPYPE_ROOT_* environments for current project.""" + self._roots_obj.set_root_environments() + + def root_names(self): + """Return root names for current project.""" + return self.root_names_from_templates(self.templates) + + def _root_keys_from_templates(self, data): + """Extract root key from templates in data. + + Args: + data (dict): Data that may contain templates as string. + + Return: + set: Set of all root names from templates as strings. + + Output example: `{"root[work]", "root[publish]"}` + """ + + output = set() + if isinstance(data, dict): + for value in data.values(): + for root in self._root_keys_from_templates(value): + output.add(root) + + elif isinstance(data, str): + for group in re.findall(self.root_key_regex, data): + output.add(group) + + return output + + def root_value_for_template(self, template): + """Returns value of root key from template.""" + root_templates = [] + for group in re.findall(self.root_key_regex, template): + root_templates.append("{" + group + "}") + + if not root_templates: + return None + + return root_templates[0].format(**{"root": self.roots}) + + def root_names_from_templates(self, templates): + """Extract root names form anatomy templates. + + Returns None if values in templates contain only "{root}". + Empty list is returned if there is no "root" in templates. + Else returns all root names from templates in list. + + RootCombinationError is raised when templates contain both root types, + basic "{root}" and with root name specification "{root[work]}". + + Args: + templates (dict): Anatomy templates where roots are not filled. + + Return: + list/None: List of all root names from templates as strings when + multiroot setup is used, otherwise None is returned. + """ + roots = list(self._root_keys_from_templates(templates)) + # Return empty list if no roots found in templates + if not roots: + return roots + + # Raise exception when root keys have roots with and without root name. + # Invalid output example: ["root", "root[project]", "root[render]"] + if len(roots) > 1 and "root" in roots: + raise RootCombinationError(roots) + + # Return None if "root" without root name in templates + if len(roots) == 1 and roots[0] == "root": + return None + + names = set() + for root in roots: + for group in re.findall(self.root_name_regex, root): + names.add(group) + return list(names) + + def fill_root(self, template_path): + """Fill template path where is only "root" key unfilled. + + Args: + template_path (str): Path with "root" key in. + Example path: "{root}/projects/MyProject/Shot01/Lighting/..." + + Return: + str: formatted path + """ + # NOTE does not care if there are different keys than "root" + return template_path.format(**{"root": self.roots}) + + @classmethod + def fill_root_with_path(cls, rootless_path, root_path): + """Fill path without filled "root" key with passed path. + + This is helper to fill root with different directory path than anatomy + has defined no matter if is single or multiroot. + + Output path is same as input path if `rootless_path` does not contain + unfilled root key. + + Args: + rootless_path (str): Path without filled "root" key. Example: + "{root[work]}/MyProject/..." + root_path (str): What should replace root key in `rootless_path`. + + Returns: + str: Path with filled root. + """ + output = str(rootless_path) + for group in re.findall(cls.root_key_regex, rootless_path): + replacement = "{" + group + "}" + output = output.replace(replacement, root_path) + + return output + + def replace_root_with_env_key(self, filepath, template=None): + """Replace root of path with environment key. + + # Example: + ## Project with roots: + ``` + { + "nas": { + "windows": P:/projects", + ... + } + ... + } + ``` + + ## Entered filepath + "P:/projects/project/asset/task/animation_v001.ma" + + ## Entered template + "<{}>" + + ## Output + "/project/asset/task/animation_v001.ma" + + Args: + filepath (str): Full file path where root should be replaced. + template (str): Optional template for environment key. Must + have one index format key. + Default value if not entered: "${}" + + Returns: + str: Path where root is replaced with environment root key. + + Raise: + ValueError: When project's roots were not found in entered path. + """ + success, rootless_path = self.find_root_template_from_path(filepath) + if not success: + raise ValueError( + "{}: Project's roots were not found in path: {}".format( + self.project_name, filepath + ) + ) + + data = self.root_environmets_fill_data(template) + return rootless_path.format(**data) + + +class Anatomy(BaseAnatomy): + _project_cache = {} + + def __init__(self, project_name=None, site_name=None): + if not project_name: + project_name = os.environ.get("AVALON_PROJECT") + + if not project_name: + raise ProjectNotSet(( + "Implementation bug: Project name is not set. Anatomy requires" + " to load data for specific project." + )) + + self._site_name = site_name + project_info = self.get_project_data_and_cache(project_name, site_name) + + super(Anatomy, self).__init__( + project_info["project_doc"], + project_info["local_settings"] + ) + + @classmethod + def get_project_data_and_cache(cls, project_name, site_name): + project_info = cls._project_cache.get(project_name) + if project_info is not None: + if time.time() - project_info["start"] > 10: + cls._project_cache.pop(project_name) + project_info = None + + if project_info is None: + if site_name is None: + if project_name: + project_settings = get_project_settings(project_name) + else: + project_settings = get_default_project_settings() + site_name = ( + project_settings["global"] + ["sync_server"] + ["config"] + ["active_site"] + ) + if site_name == "local": + site_name = get_local_site_id() + + project_info = { + "project_doc": get_project(project_name), + "local_settings": get_local_settings(site_name), + "site_name": site_name, + "start": time.time() + } + cls._project_cache[project_name] = project_info + + return project_info + + def reset(self): + """Reset values of cached data in templates and roots objects.""" + self._data = self._prepare_anatomy_data( + get_anatomy_settings(self.project_name, self._site_name) + ) + self.templates_obj.reset() + self.roots_obj.reset() + + +class AnatomyTemplateUnsolved(TemplateUnsolved): + """Exception for unsolved template when strict is set to True.""" + + msg = "Anatomy template \"{0}\" is unsolved.{1}{2}" + + +class AnatomyTemplateResult(TemplateResult): + rootless = None + + def __new__(cls, result, rootless_path): + new_obj = super(AnatomyTemplateResult, cls).__new__( + cls, + str(result), + result.template, + result.solved, + result.used_values, + result.missing_keys, + result.invalid_types + ) + new_obj.rootless = rootless_path + return new_obj + + def validate(self): + if not self.solved: + raise AnatomyTemplateUnsolved( + self.template, + self.missing_keys, + self.invalid_types + ) + + def copy(self): + tmp = TemplateResult( + str(self), + self.template, + self.solved, + self.used_values, + self.missing_keys, + self.invalid_types + ) + return self.__class__(tmp, self.rootless) + + def normalized(self): + """Convert to normalized path.""" + + tmp = TemplateResult( + os.path.normpath(self), + self.template, + self.solved, + self.used_values, + self.missing_keys, + self.invalid_types + ) + return self.__class__(tmp, self.rootless) + + +class AnatomyTemplates(TemplatesDict): + inner_key_pattern = re.compile(r"(\{@.*?[^{}0]*\})") + inner_key_name_pattern = re.compile(r"\{@(.*?[^{}0]*)\}") + + def __init__(self, anatomy): + super(AnatomyTemplates, self).__init__() + self.anatomy = anatomy + self.loaded_project = None + + def __getitem__(self, key): + return self.templates[key] + + def get(self, key, default=None): + return self.templates.get(key, default) + + def reset(self): + self._raw_templates = None + self._templates = None + self._objected_templates = None + + @property + def project_name(self): + return self.anatomy.project_name + + @property + def roots(self): + return self.anatomy.roots + + @property + def templates(self): + self._validate_discovery() + return self._templates + + @property + def objected_templates(self): + self._validate_discovery() + return self._objected_templates + + def _validate_discovery(self): + if self.project_name != self.loaded_project: + self.reset() + + if self._templates is None: + self._discover() + self.loaded_project = self.project_name + + def _format_value(self, value, data): + if isinstance(value, RootItem): + return self._solve_dict(value, data) + + result = super(AnatomyTemplates, self)._format_value(value, data) + if isinstance(result, TemplateResult): + rootless_path = self._rootless_path(result, data) + result = AnatomyTemplateResult(result, rootless_path) + return result + + def set_templates(self, templates): + if not templates: + self.reset() + return + + self._raw_templates = copy.deepcopy(templates) + templates = copy.deepcopy(templates) + v_queue = collections.deque() + v_queue.append(templates) + while v_queue: + item = v_queue.popleft() + if not isinstance(item, dict): + continue + + for key in tuple(item.keys()): + value = item[key] + if isinstance(value, dict): + v_queue.append(value) + + elif ( + isinstance(value, six.string_types) + and "{task}" in value + ): + item[key] = value.replace("{task}", "{task[name]}") + + solved_templates = self.solve_template_inner_links(templates) + self._templates = solved_templates + self._objected_templates = self.create_ojected_templates( + solved_templates + ) + + def default_templates(self): + """Return default templates data with solved inner keys.""" + return self.solve_template_inner_links( + self.anatomy["templates"] + ) + + def _discover(self): + """ Loads anatomy templates from yaml. + Default templates are loaded if project is not set or project does + not have set it's own. + TODO: create templates if not exist. + + Returns: + TemplatesResultDict: Contain templates data for current project of + default templates. + """ + + if self.project_name is None: + # QUESTION create project specific if not found? + raise AssertionError(( + "Project \"{0}\" does not have his own templates." + " Trying to use default." + ).format(self.project_name)) + + self.set_templates(self.anatomy["templates"]) + + @classmethod + def replace_inner_keys(cls, matches, value, key_values, key): + """Replacement of inner keys in template values.""" + for match in matches: + anatomy_sub_keys = ( + cls.inner_key_name_pattern.findall(match) + ) + if key in anatomy_sub_keys: + raise ValueError(( + "Unsolvable recursion in inner keys, " + "key: \"{}\" is in his own value." + " Can't determine source, please check Anatomy templates." + ).format(key)) + + for anatomy_sub_key in anatomy_sub_keys: + replace_value = key_values.get(anatomy_sub_key) + if replace_value is None: + raise KeyError(( + "Anatomy templates can't be filled." + " Anatomy key `{0}` has" + " invalid inner key `{1}`." + ).format(key, anatomy_sub_key)) + + if not ( + isinstance(replace_value, numbers.Number) + or isinstance(replace_value, six.string_types) + ): + raise ValueError(( + "Anatomy templates can't be filled." + " Anatomy key `{0}` has" + " invalid inner key `{1}`" + " with value `{2}`." + ).format(key, anatomy_sub_key, str(replace_value))) + + value = value.replace(match, str(replace_value)) + + return value + + @classmethod + def prepare_inner_keys(cls, key_values): + """Check values of inner keys. + + Check if inner key exist in template group and has valid value. + It is also required to avoid infinite loop with unsolvable recursion + when first inner key's value refers to second inner key's value where + first is used. + """ + keys_to_solve = set(key_values.keys()) + while True: + found = False + for key in tuple(keys_to_solve): + value = key_values[key] + + if isinstance(value, six.string_types): + matches = cls.inner_key_pattern.findall(value) + if not matches: + keys_to_solve.remove(key) + continue + + found = True + key_values[key] = cls.replace_inner_keys( + matches, value, key_values, key + ) + continue + + elif not isinstance(value, dict): + keys_to_solve.remove(key) + continue + + subdict_found = False + for _key, _value in tuple(value.items()): + matches = cls.inner_key_pattern.findall(_value) + if not matches: + continue + + subdict_found = True + found = True + key_values[key][_key] = cls.replace_inner_keys( + matches, _value, key_values, + "{}.{}".format(key, _key) + ) + + if not subdict_found: + keys_to_solve.remove(key) + + if not found: + break + + return key_values + + @classmethod + def solve_template_inner_links(cls, templates): + """Solve templates inner keys identified by "{@*}". + + Process is split into 2 parts. + First is collecting all global keys (keys in top hierarchy where value + is not dictionary). All global keys are set for all group keys (keys + in top hierarchy where value is dictionary). Value of a key is not + overridden in group if already contain value for the key. + + In second part all keys with "at" symbol in value are replaced with + value of the key afterward "at" symbol from the group. + + Args: + templates (dict): Raw templates data. + + Example: + templates:: + key_1: "value_1", + key_2: "{@key_1}/{filling_key}" + + group_1: + key_3: "value_3/{@key_2}" + + group_2: + key_2": "value_2" + key_4": "value_4/{@key_2}" + + output:: + key_1: "value_1" + key_2: "value_1/{filling_key}" + + group_1: { + key_1: "value_1" + key_2: "value_1/{filling_key}" + key_3: "value_3/value_1/{filling_key}" + + group_2: { + key_1: "value_1" + key_2: "value_2" + key_4: "value_3/value_2" + """ + default_key_values = templates.pop("defaults", {}) + for key, value in tuple(templates.items()): + if isinstance(value, dict): + continue + default_key_values[key] = templates.pop(key) + + # Pop "others" key before before expected keys are processed + other_templates = templates.pop("others") or {} + + keys_by_subkey = {} + for sub_key, sub_value in templates.items(): + key_values = {} + key_values.update(default_key_values) + key_values.update(sub_value) + keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) + + for sub_key, sub_value in other_templates.items(): + if sub_key in keys_by_subkey: + log.warning(( + "Key \"{}\" is duplicated in others. Skipping." + ).format(sub_key)) + continue + + key_values = {} + key_values.update(default_key_values) + key_values.update(sub_value) + keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) + + default_keys_by_subkeys = cls.prepare_inner_keys(default_key_values) + + for key, value in default_keys_by_subkeys.items(): + keys_by_subkey[key] = value + + return keys_by_subkey + + def _dict_to_subkeys_list(self, subdict, pre_keys=None): + if pre_keys is None: + pre_keys = [] + output = [] + for key in subdict: + value = subdict[key] + result = list(pre_keys) + result.append(key) + if isinstance(value, dict): + for item in self._dict_to_subkeys_list(value, result): + output.append(item) + else: + output.append(result) + return output + + def _keys_to_dicts(self, key_list, value): + if not key_list: + return None + if len(key_list) == 1: + return {key_list[0]: value} + return {key_list[0]: self._keys_to_dicts(key_list[1:], value)} + + def _rootless_path(self, result, final_data): + used_values = result.used_values + missing_keys = result.missing_keys + template = result.template + invalid_types = result.invalid_types + if ( + "root" not in used_values + or "root" in missing_keys + or "{root" not in template + ): + return + + for invalid_type in invalid_types: + if "root" in invalid_type: + return + + root_keys = self._dict_to_subkeys_list({"root": used_values["root"]}) + if not root_keys: + return + + output = str(result) + for used_root_keys in root_keys: + if not used_root_keys: + continue + + used_value = used_values + root_key = None + for key in used_root_keys: + used_value = used_value[key] + if root_key is None: + root_key = key + else: + root_key += "[{}]".format(key) + + root_key = "{" + root_key + "}" + output = output.replace(str(used_value), root_key) + + return output + + def format(self, data, strict=True): + copy_data = copy.deepcopy(data) + roots = self.roots + if roots: + copy_data["root"] = roots + result = super(AnatomyTemplates, self).format(copy_data) + result.strict = strict + return result + + def format_all(self, in_data, only_keys=True): + """ Solves templates based on entered data. + + Args: + data (dict): Containing keys to be filled into template. + + Returns: + TemplatesResultDict: Output `TemplateResult` have `strict` + attribute set to False so accessing unfilled keys in templates + won't raise any exceptions. + """ + return self.format(in_data, strict=False) + + +class RootItem(FormatObject): + """Represents one item or roots. + + Holds raw data of root item specification. Raw data contain value + for each platform, but current platform value is used when object + is used for formatting of template. + + Args: + root_raw_data (dict): Dictionary containing root values by platform + names. ["windows", "linux" and "darwin"] + name (str, optional): Root name which is representing. Used with + multi root setup otherwise None value is expected. + parent_keys (list, optional): All dictionary parent keys. Values of + `parent_keys` are used for get full key which RootItem is + representing. Used for replacing root value in path with + formattable key. e.g. parent_keys == ["work"] -> {root[work]} + parent (object, optional): It is expected to be `Roots` object. + Value of `parent` won't affect code logic much. + """ + + def __init__( + self, root_raw_data, name=None, parent_keys=None, parent=None + ): + lowered_platform_keys = {} + for key, value in root_raw_data.items(): + lowered_platform_keys[key.lower()] = value + self.raw_data = lowered_platform_keys + self.cleaned_data = self._clean_roots(lowered_platform_keys) + self.name = name + self.parent_keys = parent_keys or [] + self.parent = parent + + self.available_platforms = list(lowered_platform_keys.keys()) + self.value = lowered_platform_keys.get(platform.system().lower()) + self.clean_value = self.clean_root(self.value) + + def __format__(self, *args, **kwargs): + return self.value.__format__(*args, **kwargs) + + def __str__(self): + return str(self.value) + + def __repr__(self): + return self.__str__() + + def __getitem__(self, key): + if isinstance(key, numbers.Number): + return self.value[key] + + additional_info = "" + if self.parent and self.parent.project_name: + additional_info += " for project \"{}\"".format( + self.parent.project_name + ) + + raise AssertionError( + "Root key \"{}\" is missing{}.".format( + key, additional_info + ) + ) + + def full_key(self): + """Full key value for dictionary formatting in template. + + Returns: + str: Return full replacement key for formatting. This helps when + multiple roots are set. In that case e.g. `"root[work]"` is + returned. + """ + if not self.name: + return "root" + + joined_parent_keys = "".join( + ["[{}]".format(key) for key in self.parent_keys] + ) + return "root{}".format(joined_parent_keys) + + def clean_path(self, path): + """Just replace backslashes with forward slashes.""" + return str(path).replace("\\", "/") + + def clean_root(self, root): + """Makes sure root value does not end with slash.""" + if root: + root = self.clean_path(root) + while root.endswith("/"): + root = root[:-1] + return root + + def _clean_roots(self, raw_data): + """Clean all values of raw root item values.""" + cleaned = {} + for key, value in raw_data.items(): + cleaned[key] = self.clean_root(value) + return cleaned + + def path_remapper(self, path, dst_platform=None, src_platform=None): + """Remap path for specific platform. + + Args: + path (str): Source path which need to be remapped. + dst_platform (str, optional): Specify destination platform + for which remapping should happen. + src_platform (str, optional): Specify source platform. This is + recommended to not use and keep unset until you really want + to use specific platform. + roots (dict/RootItem/None, optional): It is possible to remap + path with different roots then instance where method was + called has. + + Returns: + str/None: When path does not contain known root then + None is returned else returns remapped path with "{root}" + or "{root[]}". + """ + cleaned_path = self.clean_path(path) + if dst_platform: + dst_root_clean = self.cleaned_data.get(dst_platform) + if not dst_root_clean: + key_part = "" + full_key = self.full_key() + if full_key != "root": + key_part += "\"{}\" ".format(full_key) + + log.warning( + "Root {}miss platform \"{}\" definition.".format( + key_part, dst_platform + ) + ) + return None + + if cleaned_path.startswith(dst_root_clean): + return cleaned_path + + if src_platform: + src_root_clean = self.cleaned_data.get(src_platform) + if src_root_clean is None: + log.warning( + "Root \"{}\" miss platform \"{}\" definition.".format( + self.full_key(), src_platform + ) + ) + return None + + if not cleaned_path.startswith(src_root_clean): + return None + + subpath = cleaned_path[len(src_root_clean):] + if dst_platform: + # `dst_root_clean` is used from upper condition + return dst_root_clean + subpath + return self.clean_value + subpath + + result, template = self.find_root_template_from_path(path) + if not result: + return None + + def parent_dict(keys, value): + if not keys: + return value + + key = keys.pop(0) + return {key: parent_dict(keys, value)} + + if dst_platform: + format_value = parent_dict(list(self.parent_keys), dst_root_clean) + else: + format_value = parent_dict(list(self.parent_keys), self.value) + + return template.format(**{"root": format_value}) + + def find_root_template_from_path(self, path): + """Replaces known root value with formattable key in path. + + All platform values are checked for this replacement. + + Args: + path (str): Path where root value should be found. + + Returns: + tuple: Tuple contain 2 values: `success` (bool) and `path` (str). + When success it True then path should contain replaced root + value with formattable key. + + Example: + When input path is:: + "C:/windows/path/root/projects/my_project/file.ext" + + And raw data of item looks like:: + { + "windows": "C:/windows/path/root", + "linux": "/mount/root" + } + + Output will be:: + (True, "{root}/projects/my_project/file.ext") + + If any of raw data value wouldn't match path's root output is:: + (False, "C:/windows/path/root/projects/my_project/file.ext") + """ + result = False + output = str(path) + + root_paths = list(self.cleaned_data.values()) + mod_path = self.clean_path(path) + for root_path in root_paths: + # Skip empty paths + if not root_path: + continue + + if mod_path.startswith(root_path): + result = True + replacement = "{" + self.full_key() + "}" + output = replacement + mod_path[len(root_path):] + break + + return (result, output) + + +class Roots: + """Object which should be used for formatting "root" key in templates. + + Args: + anatomy Anatomy: Anatomy object created for a specific project. + """ + + env_prefix = "OPENPYPE_PROJECT_ROOT" + roots_filename = "roots.json" + + def __init__(self, anatomy): + self.anatomy = anatomy + self.loaded_project = None + self._roots = None + + def __format__(self, *args, **kwargs): + return self.roots.__format__(*args, **kwargs) + + def __getitem__(self, key): + return self.roots[key] + + def reset(self): + """Reset current roots value.""" + self._roots = None + + def path_remapper( + self, path, dst_platform=None, src_platform=None, roots=None + ): + """Remap path for specific platform. + + Args: + path (str): Source path which need to be remapped. + dst_platform (str, optional): Specify destination platform + for which remapping should happen. + src_platform (str, optional): Specify source platform. This is + recommended to not use and keep unset until you really want + to use specific platform. + roots (dict/RootItem/None, optional): It is possible to remap + path with different roots then instance where method was + called has. + + Returns: + str/None: When path does not contain known root then + None is returned else returns remapped path with "{root}" + or "{root[]}". + """ + if roots is None: + roots = self.roots + + if roots is None: + raise ValueError("Roots are not set. Can't find path.") + + if "{root" in path: + path = path.format(**{"root": roots}) + # If `dst_platform` is not specified then return else continue. + if not dst_platform: + return path + + if isinstance(roots, RootItem): + return roots.path_remapper(path, dst_platform, src_platform) + + for _root in roots.values(): + result = self.path_remapper( + path, dst_platform, src_platform, _root + ) + if result is not None: + return result + + def find_root_template_from_path(self, path, roots=None): + """Find root value in entered path and replace it with formatting key. + + Args: + path (str): Source path where root will be searched. + roots (Roots/dict, optional): It is possible to use different + roots than instance where method was triggered has. + + Returns: + tuple: Output contains tuple with bool representing success as + first value and path with or without replaced root with + formatting key as second value. + + Raises: + ValueError: When roots are not entered and can't be loaded. + """ + if roots is None: + log.debug( + "Looking for matching root in path \"{}\".".format(path) + ) + roots = self.roots + + if roots is None: + raise ValueError("Roots are not set. Can't find path.") + + if isinstance(roots, RootItem): + return roots.find_root_template_from_path(path) + + for root_name, _root in roots.items(): + success, result = self.find_root_template_from_path(path, _root) + if success: + log.info("Found match in root \"{}\".".format(root_name)) + return success, result + + log.warning("No matching root was found in current setting.") + return (False, path) + + def set_root_environments(self): + """Set root environments for current project.""" + for key, value in self.root_environments().items(): + os.environ[key] = value + + def root_environments(self): + """Use root keys to create unique keys for environment variables. + + Concatenates prefix "OPENPYPE_ROOT" with root keys to create unique + keys. + + Returns: + dict: Result is `{(str): (str)}` dicitonary where key represents + unique key concatenated by keys and value is root value of + current platform root. + + Example: + With raw root values:: + "work": { + "windows": "P:/projects/work", + "linux": "/mnt/share/projects/work", + "darwin": "/darwin/path/work" + }, + "publish": { + "windows": "P:/projects/publish", + "linux": "/mnt/share/projects/publish", + "darwin": "/darwin/path/publish" + } + + Result on windows platform:: + { + "OPENPYPE_ROOT_WORK": "P:/projects/work", + "OPENPYPE_ROOT_PUBLISH": "P:/projects/publish" + } + + Short example when multiroot is not used:: + { + "OPENPYPE_ROOT": "P:/projects" + } + """ + return self._root_environments() + + def all_root_paths(self, roots=None): + """Return all paths for all roots of all platforms.""" + if roots is None: + roots = self.roots + + output = [] + if isinstance(roots, RootItem): + for value in roots.raw_data.values(): + output.append(value) + return output + + for _roots in roots.values(): + output.extend(self.all_root_paths(_roots)) + return output + + def _root_environments(self, keys=None, roots=None): + if not keys: + keys = [] + if roots is None: + roots = self.roots + + if isinstance(roots, RootItem): + key_items = [self.env_prefix] + for _key in keys: + key_items.append(_key.upper()) + + key = "_".join(key_items) + # Make sure key and value does not contain unicode + # - can happen in Python 2 hosts + return {str(key): str(roots.value)} + + output = {} + for _key, _value in roots.items(): + _keys = list(keys) + _keys.append(_key) + output.update(self._root_environments(_keys, _value)) + return output + + def root_environmets_fill_data(self, template=None): + """Environment variable values in dictionary for rootless path. + + Args: + template (str): Template for environment variable key fill. + By default is set to `"${}"`. + """ + if template is None: + template = "${}" + return self._root_environmets_fill_data(template) + + def _root_environmets_fill_data(self, template, keys=None, roots=None): + if keys is None and roots is None: + return { + "root": self._root_environmets_fill_data( + template, [], self.roots + ) + } + + if isinstance(roots, RootItem): + key_items = [Roots.env_prefix] + for _key in keys: + key_items.append(_key.upper()) + key = "_".join(key_items) + return template.format(key) + + output = {} + for key, value in roots.items(): + _keys = list(keys) + _keys.append(key) + output[key] = self._root_environmets_fill_data( + template, _keys, value + ) + return output + + @property + def project_name(self): + """Return project name which will be used for loading root values.""" + return self.anatomy.project_name + + @property + def roots(self): + """Property for filling "root" key in templates. + + This property returns roots for current project or default root values. + Warning: + Default roots value may cause issues when project use different + roots settings. That may happen when project use multiroot + templates but default roots miss their keys. + """ + if self.project_name != self.loaded_project: + self._roots = None + + if self._roots is None: + self._roots = self._discover() + self.loaded_project = self.project_name + return self._roots + + def _discover(self): + """ Loads current project's roots or default. + + Default roots are loaded if project override's does not contain roots. + + Returns: + `RootItem` or `dict` with multiple `RootItem`s when multiroot + setting is used. + """ + + return self._parse_dict(self.anatomy["roots"], parent=self) + + @staticmethod + def _parse_dict(data, key=None, parent_keys=None, parent=None): + """Parse roots raw data into RootItem or dictionary with RootItems. + + Converting raw roots data to `RootItem` helps to handle platform keys. + This method is recursive to be able handle multiroot setup and + is static to be able to load default roots without creating new object. + + Args: + data (dict): Should contain raw roots data to be parsed. + key (str, optional): Current root key. Set by recursion. + parent_keys (list): Parent dictionary keys. Set by recursion. + parent (Roots, optional): Parent object set in `RootItem` + helps to keep RootItem instance updated with `Roots` object. + + Returns: + `RootItem` or `dict` with multiple `RootItem`s when multiroot + setting is used. + """ + if not parent_keys: + parent_keys = [] + is_last = False + for value in data.values(): + if isinstance(value, six.string_types): + is_last = True + break + + if is_last: + return RootItem(data, key, parent_keys, parent=parent) + + output = {} + for _key, value in data.items(): + _parent_keys = list(parent_keys) + _parent_keys.append(_key) + output[_key] = Roots._parse_dict(value, _key, _parent_keys, parent) + return output diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index adc629352e2..661975993b5 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -14,7 +14,6 @@ ) from openpype.client.operations import ( OperationsSession, - _create_or_convert_to_mongo_id, new_hero_version_doc, prepare_hero_version_update_data, prepare_representation_update_data, @@ -193,9 +192,13 @@ def integrate_instance( op_session = OperationsSession() + entity_id = None + if old_version: + entity_id = old_version["_id"] new_hero_version = new_hero_version_doc( src_version_entity["_id"], - src_version_entity["parent"] + src_version_entity["parent"], + entity_id=entity_id ) if old_version: @@ -408,7 +411,7 @@ def integrate_instance( # Create representation else: - repre["_id"] = _create_or_convert_to_mongo_id(None) + repre.pop("_id", None) op_session.create_entity(project_name, "representation", repre) diff --git a/tests/unit/openpype/lib/resources/_process_referenced_pipeline_result.json b/tests/unit/openpype/lib/resources/_process_referenced_pipeline_result.json new file mode 100644 index 00000000000..fb798524bc8 --- /dev/null +++ b/tests/unit/openpype/lib/resources/_process_referenced_pipeline_result.json @@ -0,0 +1,92 @@ +[ + { + "_id": { + "$oid": "623c9d53db3f5046eb1ad5f4" + }, + "schema": "openpype:version-3.0", + "type": "version", + "parent": { + "$oid": "5f3e439a30a9464d6c181cbc" + }, + "name": 94, + "data": { + "families": [ + "workfile" + ], + "time": "20220324T173254Z", + "author": "petrk", + "source": "C:/projects_local/petr_test/assets/locations/Jungle/work/art/petr_test_Jungle_art_v009.psd", + "comment": "", + "machine": "LAPTOP-UB778LHG", + "fps": 25.0, + "intent": "-", + "inputLinks": [ + { + "type": "reference", + "id": { + "$oid": "618eb14f0a55a9c1591e913c" + }, + "linkedBy": "publish" + } + ] + }, + "outputs_recursive": [ + { + "_id": { + "$oid": "618eb14f0a55a9c1591e913c" + }, + "schema": "openpype:version-3.0", + "type": "version", + "parent": { + "$oid": "618e42a72ff49bd543bc1768" + }, + "name": 8, + "data": { + "families": [ + "image" + ], + "time": "20211112T192359Z", + "author": "petrk", + "source": "C:/projects_local/petr_test/assets/locations/Town/work/art/petr_test_Town_art_v005.psd", + "comment": "", + "machine": "LAPTOP-UB778LHG", + "fps": 25.0, + "intent": "-", + "inputLinks": [ + { + "type": "reference", + "id": { + "$oid": "5f3cd2d530a94638544837c3" + }, + "linkedBy": "publish" + } + ] + }, + "depth": 0 + }, + { + "_id": { + "$oid": "5f3cd2d530a94638544837c3" + }, + "schema": "pype:version-3.0", + "type": "version", + "parent": { + "$oid": "5f3a714030a9464bfc7d2382" + }, + "name": 7, + "data": { + "families": [ + "image" + ], + "time": "20200819T092032Z", + "author": "petrk", + "source": "/c/projects/petr_test/assets/characters/Hero/work/art/Hero_v019.psd", + "comment": "", + "machine": "LAPTOP-UB778LHG", + "fps": null + }, + "depth": 1 + } + ] + } +] \ No newline at end of file diff --git a/tests/unit/test_unzip.py b/tests/unit/test_unzip.py new file mode 100644 index 00000000000..586fc49b6fa --- /dev/null +++ b/tests/unit/test_unzip.py @@ -0,0 +1,11 @@ + +from openpype.hosts.harmony.api.lib import _ZipFile +from pathlib import Path + +def test_zip(): + source = "c:/Users/petrk/Downloads/fbb_fbb100_sh0020_workfileAnimation_v010.zip" + dest = "c:/projects/temp/unzipped_with_python_111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111\\2222222222222222222222222222222222222222222222222222222222222222222222222222222222" + + dest = Path(dest) + with _ZipFile(source, "r") as zip_ref: + zip_ref.extractall(dest.as_posix()) \ No newline at end of file diff --git a/vendor/configs/OpenColorIO-Configs b/vendor/configs/OpenColorIO-Configs new file mode 160000 index 00000000000..0bb079c08be --- /dev/null +++ b/vendor/configs/OpenColorIO-Configs @@ -0,0 +1 @@ +Subproject commit 0bb079c08be410030669cbf5f19ff869b88af953 diff --git a/vendor/instance.json b/vendor/instance.json new file mode 100644 index 00000000000..b1d623e85d7 --- /dev/null +++ b/vendor/instance.json @@ -0,0 +1,1133 @@ +{ + 'family': 'render', + 'name': 'renderLightingDefault', + 'label': 'renderLightingDefault - local', + 'version': 1, + 'time': '', + 'source': 'C:/projects/petr_test/sequences/seq01/shot01/work/lighting/petr_test_shot01_lighting_v001.aep', + 'subset': 'renderLightingDefault', + 'asset': 'shot01', + 'attachTo': False, + 'setMembers': '', + 'publish': True, + 'resolutionWidth': 1920.0, + 'resolutionHeight': 1080.0, + 'pixelAspect': 1, + 'frameStart': 0, + 'frameEnd': 0, + 'frameStep': 1, + 'handleStart': 0, + 'handleEnd': 0, + 'ignoreFrameHandleCheck': False, + 'renderer': 'aerender', + 'review': True, + 'priority': 50, + 'families': [ + 'render', + 'review', + 'ftrack', + 'slack' + ], + 'multipartExr': False, + 'convertToScanline': False, + 'tileRendering': False, + 'tilesX': 0, + 'tilesY': 0, + 'toBeRenderedOn': 'deadline', + 'deadlineSubmissionJob': None, + 'anatomyData': { + 'project': { + 'name': 'petr_test', + 'code': 'petr_test' + }, + 'asset': 'shot01', + 'parent': 'seq01', + 'hierarchy': 'sequences/seq01', + 'task': { + 'name': 'lighting', + 'type': 'Lighting', + 'short': 'lgt' + }, + 'username': 'petrk', + 'app': 'aftereffects', + 'd': '6', + 'dd': '06', + 'ddd': 'Thu', + 'dddd': 'Thursday', + 'm': '1', + 'mm': '01', + 'mmm': 'Jan', + 'mmmm': 'January', + 'yy': '22', + 'yyyy': '2022', + 'H': '18', + 'HH': '18', + 'h': '6', + 'hh': '06', + 'ht': 'PM', + 'M': '14', + 'MM': '14', + 'S': '23', + 'SS': '23', + 'version': 1, + 'subset': 'renderLightingDefault', + 'family': 'render', + 'intent': '-' + }, + 'outputDir': 'C:/projects/petr_test/sequences/seq01/shot01/work/lighting\\renders\\aftereffects\\petr_test_shot01_lighting_v001', + 'comp_name': '℗ renderLightingDefault', + 'comp_id': 1, + 'fps': 25, + 'projectEntity': { + '_id': ObjectId( + '5f2a6d2311e06a9818a1958b' + ), + 'name': 'petr_test', + 'created_d': datetime.datetime(2020, + 9, + 17, + 15, + 27, + 27, + 927000), + 'data': { + 'ftrackId': 'e5eda2bc-d682-11ea-afc1-92591a5b5e3e', + 'entityType': 'Project', + 'applications': [ + 'maya_2019', + 'photoshop_2021', + 'photoshop_2022', + 'harmony_17', + 'aftereffects_2022', + 'harmony_20', + 'nukestudio_12.2', + 'nukex_12.2', + 'hiero_12.2', + 'blender_2.93' + ], + 'library_project': True, + 'clipIn': 1, + 'resolutionWidth': 1920.0, + 'handleEnd': 0, + 'frameEnd': 1001, + 'resolutionHeight': 1080.0, + 'frameStart': 1001.0, + 'pixelAspect': 1.0, + 'fps': 25.0, + 'handleStart': 0, + 'clipOut': 1, + 'tools_env': [], + 'code': 'petr_test', + 'active': True + }, + 'type': 'project', + 'config': { + 'apps': [ + { + 'name': 'aftereffects/2022' + }, + { + 'name': 'maya/2019' + }, + { + 'name': 'hiero/12-2' + }, + { + 'name': 'photoshop/2021' + }, + { + 'name': 'nuke/12-2' + }, + { + 'name': 'photoshop/2022' + } + ], + 'tasks': { + 'Layout': { + 'short_name': 'lay' + }, + 'Setdress': { + 'short_name': 'dress' + }, + 'Previz': { + 'short_name': '' + }, + 'Generic': { + 'short_name': 'gener' + }, + 'Animation': { + 'short_name': 'anim' + }, + 'Modeling': { + 'short_name': 'mdl' + }, + 'Lookdev': { + 'short_name': 'look' + }, + 'FX': { + 'short_name': 'fx' + }, + 'Lighting': { + 'short_name': 'lgt' + }, + 'Compositing': { + 'short_name': 'comp' + }, + 'Tracking': { + 'short_name': '' + }, + 'Rigging': { + 'short_name': 'rig' + }, + 'Paint': { + 'short_name': 'paint' + }, + 'schedulle': { + 'short_name': '' + }, + 'Art': { + 'short_name': 'art' + }, + 'Texture': { + 'short_name': 'tex' + }, + 'Edit': { + 'short_name': 'edit' + } + }, + 'imageio': { + 'hiero': { + 'workfile': { + 'ocioConfigName': 'nuke-default', + 'ocioconfigpath': { + 'windows': [], + 'darwin': [], + 'linux': [] + }, + 'workingSpace': 'linear', + 'sixteenBitLut': 'sRGB', + 'eightBitLut': 'sRGB', + 'floatLut': 'linear', + 'logLut': 'Cineon', + 'viewerLut': 'sRGB', + 'thumbnailLut': 'sRGB' + }, + 'regexInputs': { + 'inputs': [ + { + 'regex': '[^-a-zA-Z0-9](plateRef).*(?=mp4)', + 'colorspace': 'sRGB' + } + ] + } + }, + 'nuke': { + 'viewer': { + 'viewerProcess': 'sRGB' + }, + 'baking': { + 'viewerProcess': 'rec709' + }, + 'workfile': { + 'colorManagement': 'Nuke', + 'OCIO_config': 'nuke-default', + 'customOCIOConfigPath': { + 'windows': [], + 'darwin': [], + 'linux': [] + }, + 'workingSpaceLUT': 'linear', + 'monitorLut': 'sRGB', + 'int8Lut': 'sRGB', + 'int16Lut': 'sRGB', + 'logLut': 'Cineon', + 'floatLut': 'linear' + }, + 'nodes': { + 'requiredNodes': [ + { + 'plugins': [ + 'CreateWriteRender' + ], + 'nukeNodeClass': 'Write', + 'knobs': [ + { + 'name': 'file_type', + 'value': 'exr' + }, + { + 'name': 'datatype', + 'value': '16 bit half' + }, + { + 'name': 'compression', + 'value': 'Zip (1 scanline)' + }, + { + 'name': 'autocrop', + 'value': 'True' + }, + { + 'name': 'tile_color', + 'value': '0xff0000ff' + }, + { + 'name': 'channels', + 'value': 'rgb' + }, + { + 'name': 'colorspace', + 'value': 'linear' + }, + { + 'name': 'create_directories', + 'value': 'True' + } + ] + }, + { + 'plugins': [ + 'CreateWritePrerender' + ], + 'nukeNodeClass': 'Write', + 'knobs': [ + { + 'name': 'file_type', + 'value': 'exr' + }, + { + 'name': 'datatype', + 'value': '16 bit half' + }, + { + 'name': 'compression', + 'value': 'Zip (1 scanline)' + }, + { + 'name': 'autocrop', + 'value': 'False' + }, + { + 'name': 'tile_color', + 'value': '0xadab1dff' + }, + { + 'name': 'channels', + 'value': 'rgb' + }, + { + 'name': 'colorspace', + 'value': 'linear' + }, + { + 'name': 'create_directories', + 'value': 'True' + } + ] + } + ], + 'customNodes': [] + }, + 'regexInputs': { + 'inputs': [ + { + 'regex': '[^-a-zA-Z0-9]beauty[^-a-zA-Z0-9]', + 'colorspace': 'linear' + } + ] + } + }, + 'maya': { + 'colorManagementPreference': { + 'configFilePath': { + 'windows': [], + 'darwin': [], + 'linux': [] + }, + 'renderSpace': 'scene-linear Rec 709/sRGB', + 'viewTransform': 'sRGB gamma' + } + } + }, + 'roots': { + 'work': { + 'windows': 'C:/projects', + 'darwin': '/Volumes/path', + 'linux': '/mnt/share/projects' + } + }, + 'templates': { + 'defaults': { + 'version_padding': 3, + 'version': 'v{version:0>{@version_padding}}', + 'frame_padding': 4, + 'frame': '{frame:0>{@frame_padding}}' + }, + 'work': { + 'folder': '{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task}', + 'file': '{project[code]}_{asset}_{task}_{@version}<_{comment}>.{ext}', + 'path': '{@folder}/{@file}' + }, + 'render': { + 'folder': '{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}', + 'file': '{project[code]}_{asset}_{subset}_{@version}<_{output}><.{@frame}>.{ext}', + 'path': '{@folder}/{@file}' + }, + 'publish': { + 'folder': '{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}', + 'file': '{project[code]}_{asset}_{subset}_{@version}<_{output}><.{@frame}><_{udim}>.{ext}', + 'path': '{@folder}/{@file}', + 'thumbnail': '{thumbnail_root}/{project[name]}/{_id}_{thumbnail_type}.{ext}' + }, + 'hero': { + 'folder': '{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/hero', + 'file': '{project[code]}_{asset}_{subset}_hero<_{output}><.{frame}>.{ext}', + 'path': '{@folder}/{@file}' + }, + 'delivery': {}, + 'others': {} + } + }, + 'parent': None, + 'schema': 'avalon-core:project-2.0' + }, + 'stagingDir': 'C:\\Users\\petrk\\AppData\\Local\\Temp\\tmpwyhr_ecr', + 'frameStartHandle': 0, + 'frameEndHandle': 0, + 'byFrameStep': 1, + 'author': 'petrk', + 'expectedFiles': [ + 'C:/projects/petr_test/sequences/seq01/shot01/work/lighting\\renders\\aftereffects\\petr_test_shot01_lighting_v001\\shot01_renderLightingDefault_v001.mov' + ], + 'slack_channel_message_profiles': [ + { + 'channels': [ + 'test_integration' + ], + 'upload_thumbnail': True, + 'message': 'Test message' + } + ], + 'slack_token': 'xoxb-1494100953104-2176825439264-jGqvQzfq9uZJPmyX5Q4o4TnP', + 'representations': [ + { + 'frameStart': 0, + 'frameEnd': 0, + 'name': 'mov', + 'ext': 'mov', + 'files': ' renderLightingDefault.mov', + 'stagingDir': 'C:\\Users\\petrk\\AppData\\Local\\Temp\\tmpwyhr_ecr', + 'tags': [ + 'review' + ], + 'published_path': 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001.mov', + 'publishedFiles': [ + 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001.mov' + ] + }, + { + 'name': 'thumbnail', + 'ext': 'jpg', + 'files': 'thumbnail.jpg', + 'stagingDir': 'C:\\Users\\petrk\\AppData\\Local\\Temp\\tmpwyhr_ecr', + 'tags': [ + 'thumbnail' + ], + 'published_path': 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001.jpg', + 'publishedFiles': [ + 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001.jpg' + ] + }, + { + 'frameStart': 0, + 'frameEnd': 0, + 'name': 'h264_mp4', + 'ext': 'mp4', + 'files': ' renderLightingDefault_h264burnin.mp4', + 'stagingDir': 'C:\\Users\\petrk\\AppData\\Local\\Temp\\tmpwyhr_ecr', + 'tags': [ + 'review', + 'burnin', + 'ftrackreview' + ], + 'resolutionWidth': 1920, + 'resolutionHeight': 1080, + 'outputName': 'h264', + 'outputDef': { + 'ext': 'mp4', + 'tags': [ + 'burnin', + 'ftrackreview' + ], + 'burnins': [], + 'ffmpeg_args': { + 'video_filters': [], + 'audio_filters': [], + 'input': [ + '-apply_trc gamma22' + ], + 'output': [ + '-pix_fmt yuv420p', + '-crf 18', + '-intra' + ] + }, + 'filter': { + 'families': [ + 'render', + 'review', + 'ftrack' + ] + }, + 'overscan_crop': '', + 'overscan_color': [ + 0, + 0, + 0, + 255 + ], + 'width': 0, + 'height': 0, + 'bg_color': [ + 0, + 0, + 0, + 0 + ], + 'letter_box': { + 'enabled': False, + 'ratio': 0.0, + 'state': 'letterbox', + 'fill_color': [ + 0, + 0, + 0, + 255 + ], + 'line_thickness': 0, + 'line_color': [ + 255, + 0, + 0, + 255 + ] + }, + 'filename_suffix': 'h264' + }, + 'frameStartFtrack': 0, + 'frameEndFtrack': 0, + 'ffmpeg_cmd': 'C:\\Users\\petrk\\PycharmProjects\\Pype3.0\\pype\\vendor\\bin\\ffmpeg\\windows\\bin\\ffmpeg -apply_trc gamma22 -i "C:\\Users\\petrk\\AppData\\Local\\Temp\\tmpwyhr_ecr\\ renderLightingDefault.mov" -pix_fmt yuv420p -crf 18 -intra -y "C:\\Users\\petrk\\AppData\\Local\\Temp\\tmpwyhr_ecr\\ renderLightingDefault_h264.mp4"', + 'published_path': 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001_h264.mp4', + 'publishedFiles': [ + 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001_h264.mp4' + ] + } + ], + 'assetEntity': { + '_id': ObjectId( + '5fabee9730a94666449245b7' + ), + 'name': 'shot01', + 'data': { + 'ftrackId': '0c5f548c-2425-11eb-b203-628b111fac3c', + 'entityType': 'Shot', + 'clipIn': 1, + 'resolutionWidth': 1920.0, + 'handleEnd': 0.0, + 'frameEnd': 1001, + 'resolutionHeight': 1080.0, + 'frameStart': 1001.0, + 'pixelAspect': 1.0, + 'fps': 25.0, + 'handleStart': 0.0, + 'clipOut': 1, + 'tools_env': [], + 'avalon_mongo_id': '5fabee9730a94666449245b7', + 'parents': [ + 'sequences', + 'seq01' + ], + 'hierarchy': 'sequences\\seq01', + 'tasks': { + 'lighting': { + 'type': 'Lighting' + }, + 'animation': { + 'type': 'Animation' + }, + 'compositing': { + 'type': 'Compositing' + } + }, + 'visualParent': ObjectId( + '5fabee9730a94666449245b6' + ) + }, + 'type': 'asset', + 'parent': ObjectId( + '5f2a6d2311e06a9818a1958b' + ), + 'schema': 'pype:asset-3.0' + }, + 'subsetEntity': { + '_id': ObjectId( + '61d723a271e6fce378bd428c' + ), + 'schema': 'openpype:subset-3.0', + 'type': 'subset', + 'name': 'renderLightingDefault', + 'data': { + 'families': [ + 'render', + 'review', + 'ftrack', + 'slack' + ] + }, + 'parent': ObjectId( + '5fabee9730a94666449245b7' + ) + }, + 'versionEntity': { + '_id': ObjectId( + '61d723a371e6fce378bd428d' + ), + 'schema': 'openpype:version-3.0', + 'type': 'version', + 'parent': ObjectId( + '61d723a271e6fce378bd428c' + ), + 'name': 1, + 'data': { + 'families': [ + 'render', + 'render', + 'review', + 'ftrack', + 'slack' + ], + 'time': '20220106T181423Z', + 'author': 'petrk', + 'source': 'C:/projects/petr_test/sequences/seq01/shot01/work/lighting/petr_test_shot01_lighting_v001.aep', + 'comment': '', + 'machine': 'LAPTOP-UB778LHG', + 'fps': 25.0, + 'intent': '-', + 'frameStart': 0, + 'frameEnd': 0, + 'handleEnd': 0, + 'handleStart': 0, + 'inputLinks': [ + OrderedDict( + [ + ( + 'type', + 'generative' + ), + ( + 'id', + ObjectId( + '600ab849c411725a626b8c35' + )), + ( + 'linkedBy', + 'publish' + ) + ] + ) + ] + } + }, + 'transfers': [ + [ + 'C:\\Users\\petrk\\AppData\\Local\\Temp\\tmpwyhr_ecr\\ renderLightingDefault_h264burnin.mp4', + 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001_h264.mp4' + ] + ], + 'destination_list': [ + 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001.mov', + 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001.jpg', + 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001_h264.mp4' + ], + 'published_representations': { + ObjectId( + '61d723a371e6fce378bd428e' + ): { + 'representation': { + '_id': ObjectId( + '61d723a371e6fce378bd428e' + ), + 'schema': 'openpype:representation-2.0', + 'type': 'representation', + 'parent': ObjectId( + '61d723a371e6fce378bd428d' + ), + 'name': 'mov', + 'data': { + 'path': 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001.mov', + 'template': '{root[work]}\\{project[name]}\\{hierarchy}\\{asset}\\publish\\{family}\\{subset}\\v{version:0>3}\\{project[code]}_{asset}_{subset}_v{version:0>3}<_{output}><.{frame:0>4}>.{ext}' + }, + 'dependencies': [], + 'context': { + 'root': { + 'work': 'C:/projects' + }, + 'project': { + 'name': 'petr_test', + 'code': 'petr_test' + }, + 'hierarchy': 'sequences/seq01', + 'asset': 'shot01', + 'family': 'render', + 'subset': 'renderLightingDefault', + 'version': 1, + 'ext': 'mov', + 'task': { + 'name': 'lighting', + 'type': 'Lighting', + 'short': 'lgt' + }, + 'representation': 'mov', + 'username': 'petrk' + }, + 'files': [ + { + '_id': ObjectId( + '61d723a371e6fce378bd4291' + ), + 'path': '{root[work]}/petr_test/sequences/seq01/shot01/publish/render/renderLightingDefault/v001/petr_test_shot01_renderLightingDefault_v001.mov', + 'size': 1654788, + 'hash': 'petr_test_shot01_renderLightingDefault_v001,mov|1641489300,6230524|1654788', + 'sites': [ + { + 'name': 'studio', + 'created_dt': datetime.datetime(2022, + 1, + 6, + 18, + 15, + 15, + 264448) + } + ] + } + ] + }, + 'anatomy_data': { + 'project': { + 'name': 'petr_test', + 'code': 'petr_test' + }, + 'asset': 'shot01', + 'parent': 'seq01', + 'hierarchy': 'sequences/seq01', + 'task': { + 'name': 'lighting', + 'type': 'Lighting', + 'short': 'lgt' + }, + 'username': 'petrk', + 'app': 'aftereffects', + 'd': '6', + 'dd': '06', + 'ddd': 'Thu', + 'dddd': 'Thursday', + 'm': '1', + 'mm': '01', + 'mmm': 'Jan', + 'mmmm': 'January', + 'yy': '22', + 'yyyy': '2022', + 'H': '18', + 'HH': '18', + 'h': '6', + 'hh': '06', + 'ht': 'PM', + 'M': '14', + 'MM': '14', + 'S': '23', + 'SS': '23', + 'version': 1, + 'subset': 'renderLightingDefault', + 'family': 'render', + 'intent': '-', + 'representation': 'mov', + 'ext': 'mov' + }, + 'published_files': [ + 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001.mov' + ] + }, + ObjectId( + '61d723a371e6fce378bd4292' + ): { + 'representation': { + '_id': ObjectId( + '61d723a371e6fce378bd4292' + ), + 'schema': 'openpype:representation-2.0', + 'type': 'representation', + 'parent': ObjectId( + '61d723a371e6fce378bd428d' + ), + 'name': 'thumbnail', + 'data': { + 'path': 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001.jpg', + 'template': '{root[work]}\\{project[name]}\\{hierarchy}\\{asset}\\publish\\{family}\\{subset}\\v{version:0>3}\\{project[code]}_{asset}_{subset}_v{version:0>3}<_{output}><.{frame:0>4}>.{ext}' + }, + 'dependencies': [], + 'context': { + 'root': { + 'work': 'C:/projects' + }, + 'project': { + 'name': 'petr_test', + 'code': 'petr_test' + }, + 'hierarchy': 'sequences/seq01', + 'asset': 'shot01', + 'family': 'render', + 'subset': 'renderLightingDefault', + 'version': 1, + 'ext': 'jpg', + 'task': { + 'name': 'lighting', + 'type': 'Lighting', + 'short': 'lgt' + }, + 'representation': 'jpg', + 'username': 'petrk' + }, + 'files': [ + { + '_id': ObjectId( + '61d723a371e6fce378bd4295' + ), + 'path': '{root[work]}/petr_test/sequences/seq01/shot01/publish/render/renderLightingDefault/v001/petr_test_shot01_renderLightingDefault_v001.jpg', + 'size': 871, + 'hash': 'petr_test_shot01_renderLightingDefault_v001,jpg|1641489301,1720147|871', + 'sites': [ + { + 'name': 'studio', + 'created_dt': datetime.datetime(2022, + 1, + 6, + 18, + 15, + 15, + 825446) + } + ] + } + ] + }, + 'anatomy_data': { + 'project': { + 'name': 'petr_test', + 'code': 'petr_test' + }, + 'asset': 'shot01', + 'parent': 'seq01', + 'hierarchy': 'sequences/seq01', + 'task': { + 'name': 'lighting', + 'type': 'Lighting', + 'short': 'lgt' + }, + 'username': 'petrk', + 'app': 'aftereffects', + 'd': '6', + 'dd': '06', + 'ddd': 'Thu', + 'dddd': 'Thursday', + 'm': '1', + 'mm': '01', + 'mmm': 'Jan', + 'mmmm': 'January', + 'yy': '22', + 'yyyy': '2022', + 'H': '18', + 'HH': '18', + 'h': '6', + 'hh': '06', + 'ht': 'PM', + 'M': '14', + 'MM': '14', + 'S': '23', + 'SS': '23', + 'version': 1, + 'subset': 'renderLightingDefault', + 'family': 'render', + 'intent': '-', + 'representation': 'jpg', + 'ext': 'jpg' + }, + 'published_files': [ + 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001.jpg' + ] + }, + ObjectId( + '61d723a471e6fce378bd4296' + ): { + 'representation': { + '_id': ObjectId( + '61d723a471e6fce378bd4296' + ), + 'schema': 'openpype:representation-2.0', + 'type': 'representation', + 'parent': ObjectId( + '61d723a371e6fce378bd428d' + ), + 'name': 'h264_mp4', + 'data': { + 'path': 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001_h264.mp4', + 'template': '{root[work]}\\{project[name]}\\{hierarchy}\\{asset}\\publish\\{family}\\{subset}\\v{version:0>3}\\{project[code]}_{asset}_{subset}_v{version:0>3}<_{output}><.{frame:0>4}>.{ext}' + }, + 'dependencies': [], + 'context': { + 'root': { + 'work': 'C:/projects' + }, + 'project': { + 'name': 'petr_test', + 'code': 'petr_test' + }, + 'hierarchy': 'sequences/seq01', + 'asset': 'shot01', + 'family': 'render', + 'subset': 'renderLightingDefault', + 'version': 1, + 'output': 'h264', + 'ext': 'mp4', + 'task': { + 'name': 'lighting', + 'type': 'Lighting', + 'short': 'lgt' + }, + 'representation': 'mp4', + 'username': 'petrk' + }, + 'files': [ + { + '_id': ObjectId( + '61d723a471e6fce378bd4299' + ), + 'path': '{root[work]}/petr_test/sequences/seq01/shot01/publish/render/renderLightingDefault/v001/petr_test_shot01_renderLightingDefault_v001_h264.mp4', + 'size': 10227, + 'hash': 'petr_test_shot01_renderLightingDefault_v001_h264,mp4|1641489313,659368|10227', + 'sites': [ + { + 'name': 'studio', + 'created_dt': datetime.datetime(2022, + 1, + 6, + 18, + 15, + 16, + 53445) + } + ] + } + ] + }, + 'anatomy_data': { + 'project': { + 'name': 'petr_test', + 'code': 'petr_test' + }, + 'asset': 'shot01', + 'parent': 'seq01', + 'hierarchy': 'sequences/seq01', + 'task': { + 'name': 'lighting', + 'type': 'Lighting', + 'short': 'lgt' + }, + 'username': 'petrk', + 'app': 'aftereffects', + 'd': '6', + 'dd': '06', + 'ddd': 'Thu', + 'dddd': 'Thursday', + 'm': '1', + 'mm': '01', + 'mmm': 'Jan', + 'mmmm': 'January', + 'yy': '22', + 'yyyy': '2022', + 'H': '18', + 'HH': '18', + 'h': '6', + 'hh': '06', + 'ht': 'PM', + 'M': '14', + 'MM': '14', + 'S': '23', + 'SS': '23', + 'version': 1, + 'subset': 'renderLightingDefault', + 'family': 'render', + 'intent': '-', + 'resolution_width': 1920, + 'resolution_height': 1080, + 'fps': 25, + 'output': 'h264', + 'representation': 'mp4', + 'ext': 'mp4' + }, + 'published_files': [ + 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001_h264.mp4' + ] + } + }, + 'ftrackComponentsList': [ + { + 'assettype_data': { + 'short': 'render' + }, + 'asset_data': { + 'name': 'renderLightingDefault' + }, + 'assetversion_data': { + 'version': 1 + }, + 'component_overwrite': False, + 'thumbnail': True, + 'component_data': { + 'name': 'thumbnail' + }, + 'component_path': 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001.jpg', + 'component_location': , + 'component': + }, + { + 'assettype_data': { + 'short': 'render' + }, + 'asset_data': { + 'name': 'renderLightingDefault' + }, + 'assetversion_data': { + 'version': 1 + }, + 'component_overwrite': False, + 'thumbnail': False, + 'component_data': { + 'name': 'ftrackreview-mp4', + 'metadata': { + 'ftr_meta': '{"frameIn": 0, "frameOut": 1, "frameRate": 25.0}' + } + }, + 'component_path': 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001_h264.mp4', + 'component_location': , + 'component': + }, + { + 'assettype_data': { + 'short': 'render' + }, + 'asset_data': { + 'name': 'renderLightingDefault' + }, + 'assetversion_data': { + 'version': 1 + }, + 'component_overwrite': False, + 'thumbnail': False, + 'component_data': { + 'name': 'thumbnail_src' + }, + 'component_path': 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001.jpg', + 'component_location': , + 'component': + }, + { + 'assettype_data': { + 'short': 'render' + }, + 'asset_data': { + 'name': 'renderLightingDefault' + }, + 'assetversion_data': { + 'version': 1 + }, + 'component_overwrite': False, + 'thumbnail': False, + 'component_data': { + 'name': 'ftrackreview-mp4_src', + 'metadata': { + 'ftr_meta': '{"frameIn": 0, "frameOut": 1, "frameRate": 25.0}' + } + }, + 'component_path': 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001_h264.mp4', + 'component_location': , + 'component': + }, + { + 'assettype_data': { + 'short': 'render' + }, + 'asset_data': { + 'name': 'renderLightingDefault' + }, + 'assetversion_data': { + 'version': 1 + }, + 'component_overwrite': False, + 'thumbnail': False, + 'component_data': { + 'name': 'mov' + }, + 'component_path': 'C:\\projects\\petr_test\\sequences\\seq01\\shot01\\publish\\render\\renderLightingDefault\\v001\\petr_test_shot01_renderLightingDefault_v001.mov', + 'component_location': , + 'component': + } + ], + 'ftrackIntegratedAssetVersions': [ + + ] +} \ No newline at end of file diff --git a/vendor/response.json b/vendor/response.json new file mode 100644 index 00000000000..26a4fae2fdc --- /dev/null +++ b/vendor/response.json @@ -0,0 +1 @@ +{status: 200, headers: {'date': 'Tue, 11 Jan 2022 11:08:57 GMT', 'server': 'Apache', 'x-powered-by': 'HHVM/4.128.0', 'access-control-allow-origin': '*', 'referrer-policy': 'no-referrer', 'x-slack-backend': 'r', 'strict-transport-security': 'max-age=31536000; includeSubDomains; preload', 'access-control-allow-headers': 'slack-route, x-slack-version-ts, x-b3-traceid, x-b3-spanid, x-b3-parentspanid, x-b3-sampled, x-b3-flags', 'access-control-expose-headers': 'x-slack-req-id, retry-after', 'x-oauth-scopes': 'chat:write,chat:write.public,files:write,chat:write.customize', 'x-accepted-oauth-scopes': 'chat:write', 'expires': 'Mon, 26 Jul 1997 05:00:00 GMT', 'cache-control': 'private, no-cache, no-store, must-revalidate', 'pragma': 'no-cache', 'x-xss-protection': '0', 'x-content-type-options': 'nosniff', 'x-slack-req-id': '9d1d11399a44c8751f89bb4dcd2b91fb', 'vary': 'Accept-Encoding', 'content-type': 'application/json; charset=utf-8', 'x-envoy-upstream-service-time': '52', 'x-backend': 'main_normal main_bedrock_normal_with_overflow main_canary_with_overflow main_bedrock_canary_with_overflow main_control_with_overflow main_bedrock_control_with_overflow', 'x-server': 'slack-www-hhvm-main-iad-qno3', 'x-slack-shared-secret-outcome': 'no-match', 'via': 'envoy-www-iad-omsy, envoy-edge-iad-bgfx', 'x-edge-backend': 'envoy-www', 'x-slack-edge-shared-secret-outcome': 'no-match', 'connection': 'close', 'transfer-encoding': 'chunked'}, body: {"ok":true,"channel":"C024DUFM8MB","ts":"1641899337.001100","message":{"type":"message","subtype":"bot_message","text":"RenderCompositingDefault published for Jungle\n\nHere should be link to review C:\\projects\\petr_test\\assets\\locations\\Jungle\\publish\\render\\renderCompositingDefault\\v253\\petr_test_Jungle_renderCompositingDefault_v253_h264.mp4\n\n Attachment links: \n\n","ts":"1641899337.001100","username":"OpenPypeNotifier","icons":{"image_48":"https:\/\/s3-us-west-2.amazonaws.com\/slack-files2\/bot_icons\/2022-01-07\/2934353684385_48.png"},"bot_id":"B024H0P0CAE"}} \ No newline at end of file diff --git a/vendor/temp.json b/vendor/temp.json new file mode 100644 index 00000000000..089174d26c3 --- /dev/null +++ b/vendor/temp.json @@ -0,0 +1,46 @@ +{ + project(name: "demo_Big_Episodic") { + representations( + first: 0, + after: 0, + localSite: "local", + remoteSite: "local" + ) { + edges { + node { + id + name + # Sorry: totalSize is not implemented, but it will be + # totalSize + fileCount + # overal sync state + localState{ + status + size + timestamp + } + remoteState{ + status + size + timestamp + } + # crawl to the top to get parent info + version { + version + subset { + family + name + folder { + name + } + } + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } +} \ No newline at end of file