From 48c5372ba53dc17d354439158d462359167e7b01 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Oct 2021 16:29:25 +0200 Subject: [PATCH 001/307] Flame adding flame to ftrack script --- .../openpype_flame_to_ftrack.py | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py new file mode 100644 index 0000000000..5f9a78ce16 --- /dev/null +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py @@ -0,0 +1,178 @@ +from __future__ import print_function +from PySide2 import QtWidgets, QtCore + + +class FlameTreeWidget(QtWidgets.QTreeWidget): + """ + Custom Qt Flame Tree Widget + + To use: + + tree_headers = ['Header1', 'Header2', 'Header3', 'Header4'] + tree = FlameTreeWidget(tree_headers, window) + """ + + def __init__(self, tree_headers, parent_window, *args, **kwargs): + super(FlameTreeWidget, self).__init__(*args, **kwargs) + + self.setMinimumWidth(1000) + self.setMinimumHeight(300) + self.setSortingEnabled(True) + self.sortByColumn(0, QtCore.Qt.AscendingOrder) + self.setAlternatingRowColors(True) + self.setFocusPolicy(QtCore.Qt.NoFocus) + self.setStyleSheet('QTreeWidget {color: #9a9a9a; background-color: #2a2a2a; alternate-background-color: #2d2d2d; font: 14px "Discreet"}' + 'QTreeWidget::item:selected {color: #d9d9d9; background-color: #474747; border: 1px solid #111111}' + 'QHeaderView {color: #9a9a9a; background-color: #393939; font: 14px "Discreet"}' + 'QTreeWidget::item:selected {selection-background-color: #111111}' + 'QMenu {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' + 'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}') + self.verticalScrollBar().setStyleSheet('color: #818181') + self.horizontalScrollBar().setStyleSheet('color: #818181') + self.setHeaderLabels(tree_headers) + + +class FlameButton(QtWidgets.QPushButton): + """ + Custom Qt Flame Button Widget + + To use: + + button = FlameButton('Button Name', do_this_when_pressed, window) + """ + + def __init__(self, button_name, do_when_pressed, parent_window, *args, **kwargs): + super(FlameButton, self).__init__(*args, **kwargs) + + self.setText(button_name) + self.setParent(parent_window) + self.setMinimumSize(QtCore.QSize(110, 28)) + self.setMaximumSize(QtCore.QSize(110, 28)) + self.setFocusPolicy(QtCore.Qt.NoFocus) + self.clicked.connect(do_when_pressed) + self.setStyleSheet('QPushButton {color: #9a9a9a; background-color: #424142; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}' + 'QPushButton:pressed {color: #d9d9d9; background-color: #4f4f4f; border-top: 1px inset #666666; font: italic}' + 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}') + + +def main_window(selection): + + def timeline_info(selection): + import flame + + # identificar as informacoes dos segmentos na timeline + + for sequence in selection: + for ver in sequence.versions: + for tracks in ver.tracks: + for segment in tracks.segments: + # Add timeline segment to tree + QtWidgets.QTreeWidgetItem(tree, [ + str(sequence.name)[1:-1], + str(segment.shot_name)[1:-1], + 'Compositing', + 'Ready to Start', + 'Tape: {} - Duration {}'.format( + segment.tape_name, + str(segment.source_duration)[4:-1] + ), + str(segment.comment)[1:-1] + ]).setFlags( + QtCore.Qt.ItemIsEditable + | QtCore.Qt.ItemIsEnabled + | QtCore.Qt.ItemIsSelectable + ) + # Select top item in tree + + tree.setCurrentItem(tree.topLevelItem(0)) + + def select_all(): + + tree.selectAll() + + def send_to_ftrack(): + # Get all selected items from treewidget + clip_info = '' + + for item in tree.selectedItems(): + tree_line = [ + item.text(0), + item.text(1), + item.text(2), + item.text(3), + item.text(4), + item.text(5) + ] + print(tree_line) + clip_info += tree_line + '\n' + + # creating ui + window = QtWidgets.QWidget() + window.setMinimumSize(500, 350) + window.setWindowTitle('Sequence Shots to Ftrack') + window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + window.setAttribute(QtCore.Qt.WA_DeleteOnClose) + window.setStyleSheet('background-color: #313131') + + # Center window in linux + + resolution = QtWidgets.QDesktopWidget().screenGeometry() + window.move((resolution.width() / 2) - (window.frameSize().width() / 2), + (resolution.height() / 2) - (window.frameSize().height() / 2)) + + ## TreeWidget + headers = ['Sequence Name', 'Shot Name', 'Task Type', + 'Task Status', 'Shot Description', 'Task Description'] + tree = FlameTreeWidget(headers, window) + + # Allow multiple items in tree to be selected + tree.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) + + # Set tree column width + tree.setColumnWidth(0, 200) + tree.setColumnWidth(1, 100) + tree.setColumnWidth(2, 100) + tree.setColumnWidth(3, 120) + tree.setColumnWidth(4, 270) + tree.setColumnWidth(5, 270) + + # Prevent weird characters when shrinking tree columns + tree.setTextElideMode(QtCore.Qt.ElideNone) + + ## Button + select_all_btn = FlameButton('Select All', select_all, window) + copy_btn = FlameButton('Send to Ftrack', send_to_ftrack, window) + + ## Window Layout + gridbox = QtWidgets.QGridLayout() + gridbox.setMargin(20) + gridbox.addWidget(tree, 1, 0, 5, 1) + gridbox.addWidget(select_all_btn, 1, 1) + gridbox.addWidget(copy_btn, 2, 1) + + window.setLayout(gridbox) + window.show() + + timeline_info(selection) + + return window + + +def scope_sequence(selection): + import flame + return any(isinstance(item, flame.PySequence) for item in selection) + +def get_media_panel_custom_ui_actions(): + return [ + { + "name": "OpenPype: Ftrack", + "actions": [ + { + "name": "Create Shots", + "isVisible": scope_sequence, + "execute": main_window + } + ] + } + + ] From 051e74a448ef2edb765fb24e3357dfb6ed7b108d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Oct 2021 16:50:14 +0200 Subject: [PATCH 002/307] testing ftrack --- .../openpype_flame_to_ftrack.py | 60 +++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py index 5f9a78ce16..2f40c25e88 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py @@ -1,6 +1,6 @@ from __future__ import print_function from PySide2 import QtWidgets, QtCore - +from pprint import pformat class FlameTreeWidget(QtWidgets.QTreeWidget): """ @@ -61,7 +61,6 @@ def main_window(selection): import flame # identificar as informacoes dos segmentos na timeline - for sequence in selection: for ver in sequence.versions: for tracks in ver.tracks: @@ -91,8 +90,59 @@ def main_window(selection): tree.selectAll() def send_to_ftrack(): + import ftrack_api + + def validate_credentials(url, user, api): + first_validation = True + if not user: + print('- Ftrack Username is not set') + first_validation = False + if not api: + print('- Ftrack API key is not set') + first_validation = False + if not first_validation: + return False + + try: + session = ftrack_api.Session( + server_url=url, + api_user=user, + api_key=api + ) + session.close() + except Exception as _e: + print( + "Can't log into Ftrack with used credentials: {}".format( + _e) + ) + ftrack_cred = { + 'Ftrack server': str(url), + 'Username': str(user), + 'API key': str(api), + } + + item_lens = [len(key) + 1 for key in ftrack_cred] + justify_len = max(*item_lens) + for key, value in ftrack_cred.items(): + print('{} {}'.format((key + ':').ljust( + justify_len, ' '), value)) + return False + print( + 'Credentials Username: "{}", API key: "{}" are valid.'.format( + user, api) + ) + return True + + # fill your own credentials + url = "" + user = "" + api = "" + + if validate_credentials(url, user, api): + print("validation of ftrack went trough") + # Get all selected items from treewidget - clip_info = '' + clips_info = [] for item in tree.selectedItems(): tree_line = [ @@ -104,7 +154,9 @@ def main_window(selection): item.text(5) ] print(tree_line) - clip_info += tree_line + '\n' + clips_info.append(tree_line) + + print("selected clips: {}".format(pformat(clips_info))) # creating ui window = QtWidgets.QWidget() From eb63e6910f2a6744c8c38eeeb569ced1c57d1fd5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Oct 2021 17:16:53 +0200 Subject: [PATCH 003/307] flame to ftrack: adding maintained session --- .../openpype_flame_to_ftrack.py | 150 ++++++++++-------- 1 file changed, 85 insertions(+), 65 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py index 2f40c25e88..7460313daa 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py @@ -1,6 +1,75 @@ from __future__ import print_function from PySide2 import QtWidgets, QtCore from pprint import pformat +from contextlib import contextmanager + +@contextmanager +def maintained_ftrack_session(): + import ftrack_api + import os + + def validate_credentials(url, user, api): + first_validation = True + if not user: + print('- Ftrack Username is not set') + first_validation = False + if not api: + print('- Ftrack API key is not set') + first_validation = False + if not first_validation: + return False + + try: + session = ftrack_api.Session( + server_url=url, + api_user=user, + api_key=api + ) + session.close() + except Exception as _e: + print( + "Can't log into Ftrack with used credentials: {}".format( + _e) + ) + ftrack_cred = { + 'Ftrack server': str(url), + 'Username': str(user), + 'API key': str(api), + } + + item_lens = [len(key) + 1 for key in ftrack_cred] + justify_len = max(*item_lens) + for key, value in ftrack_cred.items(): + print('{} {}'.format((key + ':').ljust( + justify_len, ' '), value)) + return False + print( + 'Credentials Username: "{}", API key: "{}" are valid.'.format( + user, api) + ) + return True + + # fill your own credentials + url = os.getenv("FTRACK_SERVER") + user = os.getenv("FTRACK_API_USER") + api = os.getenv("FTRACK_API_KEY") + + try: + assert validate_credentials(url, user, api), ( + "Ftrack credentials failed") + # open ftrack session + session = ftrack_api.Session( + server_url=url, + api_user=user, + api_key=api + ) + yield session + except Exception as _E: + ConnectionRefusedError( + "ERROR: {}".format(_E)) + finally: + # close the session + session.close() class FlameTreeWidget(QtWidgets.QTreeWidget): """ @@ -90,73 +159,24 @@ def main_window(selection): tree.selectAll() def send_to_ftrack(): - import ftrack_api + with maintained_ftrack_session() as session: + print("Ftrack session is: {}".format(session)) + # Get all selected items from treewidget + clips_info = [] - def validate_credentials(url, user, api): - first_validation = True - if not user: - print('- Ftrack Username is not set') - first_validation = False - if not api: - print('- Ftrack API key is not set') - first_validation = False - if not first_validation: - return False + for item in tree.selectedItems(): + tree_line = [ + item.text(0), + item.text(1), + item.text(2), + item.text(3), + item.text(4), + item.text(5) + ] + print(tree_line) + clips_info.append(tree_line) - try: - session = ftrack_api.Session( - server_url=url, - api_user=user, - api_key=api - ) - session.close() - except Exception as _e: - print( - "Can't log into Ftrack with used credentials: {}".format( - _e) - ) - ftrack_cred = { - 'Ftrack server': str(url), - 'Username': str(user), - 'API key': str(api), - } - - item_lens = [len(key) + 1 for key in ftrack_cred] - justify_len = max(*item_lens) - for key, value in ftrack_cred.items(): - print('{} {}'.format((key + ':').ljust( - justify_len, ' '), value)) - return False - print( - 'Credentials Username: "{}", API key: "{}" are valid.'.format( - user, api) - ) - return True - - # fill your own credentials - url = "" - user = "" - api = "" - - if validate_credentials(url, user, api): - print("validation of ftrack went trough") - - # Get all selected items from treewidget - clips_info = [] - - for item in tree.selectedItems(): - tree_line = [ - item.text(0), - item.text(1), - item.text(2), - item.text(3), - item.text(4), - item.text(5) - ] - print(tree_line) - clips_info.append(tree_line) - - print("selected clips: {}".format(pformat(clips_info))) + print("selected clips: {}".format(pformat(clips_info))) # creating ui window = QtWidgets.QWidget() From 6700b0896d0066cfc02ee660835ddb2168ec3065 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Oct 2021 17:47:53 +0200 Subject: [PATCH 004/307] improving shot name and ftrack test create --- .../openpype_flame_to_ftrack.py | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py index 7460313daa..c2934f0dc1 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py @@ -136,8 +136,8 @@ def main_window(selection): for segment in tracks.segments: # Add timeline segment to tree QtWidgets.QTreeWidgetItem(tree, [ - str(sequence.name)[1:-1], - str(segment.shot_name)[1:-1], + str(sequence.name), + str(segment.name), 'Compositing', 'Ready to Start', 'Tape: {} - Duration {}'.format( @@ -159,12 +159,46 @@ def main_window(selection): tree.selectAll() def send_to_ftrack(): + import flame + import six + import sys + + def create_ftrack_entity(session, name, parent): + entity = session.create(type, { + 'name': name, + 'parent': parent + }) + try: + session.commit() + except Exception: + tp, value, tb = sys.exc_info() + session.rollback() + session._configure_locations() + six.reraise(tp, value, tb) + return entity + with maintained_ftrack_session() as session: print("Ftrack session is: {}".format(session)) + + # get project name from flame current project + project_name = flame.project.current_project.name + # get project from ftrack - + # ftrack project name has to be the same as flame project! + query = 'Project where full_name is "{}"'.format(project_name) + f_project = session.query(query).one() + print("Ftrack project is: {}".format(f_project)) + # Get all selected items from treewidget clips_info = [] for item in tree.selectedItems(): + f_entity = create_ftrack_entity( + session, + item.text(1), + f_project + ) + print("Shot entity is: {}".format(f_entity)) + tree_line = [ item.text(0), item.text(1), From 1532b9caab674a8d8f311470e551107945e692d0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Oct 2021 17:53:34 +0200 Subject: [PATCH 005/307] fix name and duration population --- .../hosts/flame/utility_scripts/openpype_flame_to_ftrack.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py index c2934f0dc1..bd529cda1d 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py @@ -136,13 +136,13 @@ def main_window(selection): for segment in tracks.segments: # Add timeline segment to tree QtWidgets.QTreeWidgetItem(tree, [ - str(sequence.name), - str(segment.name), + str(sequence.name)[1:-1], + str(segment.name)[1:-1], 'Compositing', 'Ready to Start', 'Tape: {} - Duration {}'.format( segment.tape_name, - str(segment.source_duration)[4:-1] + str(segment.record_duration)[4:-1] ), str(segment.comment)[1:-1] ]).setFlags( From e24716cb8cb1e8546b702ae31a9a4185b42d1982 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Oct 2021 18:03:50 +0200 Subject: [PATCH 006/307] test ftrack entity create fixes --- .../hosts/flame/utility_scripts/openpype_flame_to_ftrack.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py index bd529cda1d..aaf4211f38 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py @@ -65,7 +65,7 @@ def maintained_ftrack_session(): ) yield session except Exception as _E: - ConnectionRefusedError( + print( "ERROR: {}".format(_E)) finally: # close the session @@ -163,7 +163,7 @@ def main_window(selection): import six import sys - def create_ftrack_entity(session, name, parent): + def create_ftrack_entity(session, type, name, parent): entity = session.create(type, { 'name': name, 'parent': parent @@ -194,6 +194,7 @@ def main_window(selection): for item in tree.selectedItems(): f_entity = create_ftrack_entity( session, + "Shot", item.text(1), f_project ) @@ -221,7 +222,6 @@ def main_window(selection): window.setStyleSheet('background-color: #313131') # Center window in linux - resolution = QtWidgets.QDesktopWidget().screenGeometry() window.move((resolution.width() / 2) - (window.frameSize().width() / 2), (resolution.height() / 2) - (window.frameSize().height() / 2)) From e1dc75f3a6addab2487e481f9a2258d0af93cf97 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 29 Oct 2021 16:13:28 +0200 Subject: [PATCH 007/307] flame: expanding columns and collecttors --- .../openpype_flame_to_ftrack.py | 186 ++++++++++++++++-- 1 file changed, 165 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py index aaf4211f38..f922998a2e 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py @@ -3,6 +3,12 @@ from PySide2 import QtWidgets, QtCore from pprint import pformat from contextlib import contextmanager +# Constants +WORKFILE_START_FRAME = 1001 +HIERARCHY_TEMPLATE = "shots/{sequence}" +CREATE_TASK_TYPE = "Compositing" + + @contextmanager def maintained_ftrack_session(): import ftrack_api @@ -71,6 +77,63 @@ def maintained_ftrack_session(): # close the session session.close() + +class FlameLabel(QtWidgets.QLabel): + """ + Custom Qt Flame Label Widget + + For different label looks set label_type as: 'normal', 'background', or 'outline' + + To use: + + label = FlameLabel('Label Name', 'normal', window) + """ + + def __init__(self, label_name, label_type, parent_window, *args, **kwargs): + super(FlameLabel, self).__init__(*args, **kwargs) + + self.setText(label_name) + self.setParent(parent_window) + self.setMinimumSize(130, 28) + self.setMaximumHeight(28) + self.setFocusPolicy(QtCore.Qt.NoFocus) + + # Set label stylesheet based on label_type + + if label_type == 'normal': + self.setStyleSheet('QLabel {color: #9a9a9a; border-bottom: 1px inset #282828; font: 14px "Discreet"}' + 'QLabel:disabled {color: #6a6a6a}') + elif label_type == 'background': + self.setAlignment(QtCore.Qt.AlignCenter) + self.setStyleSheet('color: #9a9a9a; background-color: #393939; font: 14px "Discreet"') + elif label_type == 'outline': + self.setAlignment(QtCore.Qt.AlignCenter) + self.setStyleSheet('color: #9a9a9a; background-color: #212121; border: 1px solid #404040; font: 14px "Discreet"') + + +class FlameLineEdit(QtWidgets.QLineEdit): + """ + Custom Qt Flame Line Edit Widget + + Main window should include this: window.setFocusPolicy(QtCore.Qt.StrongFocus) + + To use: + + line_edit = FlameLineEdit('Some text here', window) + """ + + def __init__(self, text, parent_window, *args, **kwargs): + super(FlameLineEdit, self).__init__(*args, **kwargs) + + self.setText(text) + self.setParent(parent_window) + self.setMinimumHeight(28) + self.setMinimumWidth(110) + self.setStyleSheet('QLineEdit {color: #9a9a9a; background-color: #373e47; selection-color: #262626; selection-background-color: #b8b1a7; font: 14px "Discreet"}' + 'QLineEdit:focus {background-color: #474e58}' + 'QLineEdit:disabled {color: #6a6a6a; background-color: #373737}') + + class FlameTreeWidget(QtWidgets.QTreeWidget): """ Custom Qt Flame Tree Widget @@ -125,33 +188,74 @@ class FlameButton(QtWidgets.QPushButton): def main_window(selection): + def timecode_to_frames(timecode, framerate): + + def _seconds(value): + if isinstance(value, str): + _zip_ft = zip((3600, 60, 1, 1/framerate), value.split(':')) + return sum(f * float(t) for f, t in _zip_ft) + elif isinstance(value, (int, float)): + return value / framerate + return 0 + + def _frames(seconds): + return seconds * framerate + + def timecode_to_frames(_timecode, start=None): + return _frames(_seconds(_timecode) - _seconds(start)) + + if '+' in timecode: + timecode = timecode.replace('+', ':') + elif '#' in timecode: + timecode = timecode.replace('#', ':') + + frames = int(round(timecode_to_frames(timecode, start='00:00:00:00'))) + + return frames def timeline_info(selection): import flame # identificar as informacoes dos segmentos na timeline for sequence in selection: + frame_rate = float(str(sequence.frame_rate)[:-4]) for ver in sequence.versions: for tracks in ver.tracks: for segment in tracks.segments: + print(segment.type) + # get clip frame duration + record_duration = str(segment.record_duration)[1:-1] + clip_duration = timecode_to_frames( + record_duration, frame_rate) + + # populate shot source metadata + shot_description = "" + for attr in ["tape_name", "source_name", "head", + "tail", "file_path"]: + if not hasattr(segment, attr): + continue + _value = getattr(segment, attr) + _label = attr.replace("_", " ").capitalize() + row = "{}: {}\n".format(_label, _value) + shot_description += row + # Add timeline segment to tree QtWidgets.QTreeWidgetItem(tree, [ - str(sequence.name)[1:-1], - str(segment.name)[1:-1], - 'Compositing', - 'Ready to Start', - 'Tape: {} - Duration {}'.format( - segment.tape_name, - str(segment.record_duration)[4:-1] - ), - str(segment.comment)[1:-1] + str(sequence.name)[1:-1], # seq + str(segment.shot_name)[1:-1], # shot + CREATE_TASK_TYPE, # task type + str(WORKFILE_START_FRAME), # start frame + str(clip_duration), # clip duration + "0:0", # handles + shot_description, # shot description + str(segment.comment)[1:-1] # task description ]).setFlags( QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable ) - # Select top item in tree + # Select top item in tree tree.setCurrentItem(tree.topLevelItem(0)) def select_all(): @@ -206,7 +310,9 @@ def main_window(selection): item.text(2), item.text(3), item.text(4), - item.text(5) + item.text(5), + item.text(6), + item.text(7) ] print(tree_line) clips_info.append(tree_line) @@ -226,25 +332,61 @@ def main_window(selection): window.move((resolution.width() / 2) - (window.frameSize().width() / 2), (resolution.height() / 2) - (window.frameSize().height() / 2)) - ## TreeWidget - headers = ['Sequence Name', 'Shot Name', 'Task Type', - 'Task Status', 'Shot Description', 'Task Description'] - tree = FlameTreeWidget(headers, window) + # TreeWidget + columns = { + "Sequence name": { + "columnWidth": 100, + "order": 0 + }, + "Shot name": { + "columnWidth": 100, + "order": 1 + }, + "Task type": { + "columnWidth": 100, + "order": 2 + }, + "Start frame": { + "columnWidth": 100, + "order": 3 + }, + "Clip duration": { + "columnWidth": 100, + "order": 4 + }, + "Handles": { + "columnWidth": 100, + "order": 5 + }, + "Shot description": { + "columnWidth": 300, + "order": 6 + }, + "Task description": { + "columnWidth": 300, + "order": 7 + }, + } + tree = FlameTreeWidget(columns.keys(), window) # Allow multiple items in tree to be selected tree.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) # Set tree column width - tree.setColumnWidth(0, 200) - tree.setColumnWidth(1, 100) - tree.setColumnWidth(2, 100) - tree.setColumnWidth(3, 120) - tree.setColumnWidth(4, 270) - tree.setColumnWidth(5, 270) + for _name, _val in columns.items(): + tree.setColumnWidth( + _val["order"], + _val["columnWidth"] + ) # Prevent weird characters when shrinking tree columns tree.setTextElideMode(QtCore.Qt.ElideNone) + # input fields + hierarchy_label = FlameLabel( + 'Parents template', 'normal', window) + hierarchy_template = FlameLineEdit(HIERARCHY_TEMPLATE, window) + ## Button select_all_btn = FlameButton('Select All', select_all, window) copy_btn = FlameButton('Send to Ftrack', send_to_ftrack, window) @@ -252,6 +394,8 @@ def main_window(selection): ## Window Layout gridbox = QtWidgets.QGridLayout() gridbox.setMargin(20) + gridbox.addWidget(hierarchy_label, 0, 0) + gridbox.addWidget(hierarchy_template, 0, 1) gridbox.addWidget(tree, 1, 0, 5, 1) gridbox.addWidget(select_all_btn, 1, 1) gridbox.addWidget(copy_btn, 2, 1) From 319dcaf2e6b7b0c39eb74211c59a3665dc2c4008 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 29 Oct 2021 17:42:26 +0200 Subject: [PATCH 008/307] flame: tuning the shot data population --- .../openpype_flame_to_ftrack.py | 76 ++++++++++++++----- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py index f922998a2e..9364d8d57f 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py @@ -242,7 +242,7 @@ def main_window(selection): # Add timeline segment to tree QtWidgets.QTreeWidgetItem(tree, [ str(sequence.name)[1:-1], # seq - str(segment.shot_name)[1:-1], # shot + str(segment.name)[1:-1], # shot CREATE_TASK_TYPE, # task type str(WORKFILE_START_FRAME), # start frame str(clip_duration), # clip duration @@ -281,6 +281,15 @@ def main_window(selection): six.reraise(tp, value, tb) return entity + def get_ftrack_entity(session, type, name, project_entity): + query = '{} where name is "{}" and project_id is "{}"'.format( + type, name, project_entity["id"]) + return session.query(query).one() + + def generate_parents_from_template(template): + template_split = template.split("/") + return template_split + with maintained_ftrack_session() as session: print("Ftrack session is: {}".format(session)) @@ -296,24 +305,45 @@ def main_window(selection): clips_info = [] for item in tree.selectedItems(): - f_entity = create_ftrack_entity( + parents = generate_parents_from_template( + hierarchy_template.text()) + print(parents) + + f_entity = get_ftrack_entity( session, "Shot", item.text(1), f_project ) + # if entity doesnt exist then create one + if not f_entity: + f_entity = create_ftrack_entity( + session, + "Shot", + item.text(1), + f_project + ) print("Shot entity is: {}".format(f_entity)) - tree_line = [ - item.text(0), - item.text(1), - item.text(2), - item.text(3), - item.text(4), - item.text(5), - item.text(6), - item.text(7) - ] + # solve handle start and end + handles = item.text(5) + if ":" in handles: + _s, _e = handles.split(":") + handles = (int(_s), int(_e)) + else: + handles = (int(handles), int(handles)) + + # populate full shot info + tree_line = { + "sequence": item.text(0), + "shot": item.text(1), + "task": item.text(2), + "frameStart": int(item.text(3)), + "duration": int(item.text(4)), + "handles": handles, + "shotDescription": item.text(6), + "taskDescription": item.text(7) + } print(tree_line) clips_info.append(tree_line) @@ -321,7 +351,7 @@ def main_window(selection): # creating ui window = QtWidgets.QWidget() - window.setMinimumSize(500, 350) + window.setMinimumSize(1500, 600) window.setWindowTitle('Sequence Shots to Ftrack') window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) window.setAttribute(QtCore.Qt.WA_DeleteOnClose) @@ -367,7 +397,14 @@ def main_window(selection): "order": 7 }, } - tree = FlameTreeWidget(columns.keys(), window) + ordered_column_labels = columns.keys() + for _name, _value in columns.items(): + ordered_column_labels.pop(_value["order"]) + ordered_column_labels.insert(_value["order"], _name) + + print(ordered_column_labels) + + tree = FlameTreeWidget(ordered_column_labels, window) # Allow multiple items in tree to be selected tree.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) @@ -389,16 +426,17 @@ def main_window(selection): ## Button select_all_btn = FlameButton('Select All', select_all, window) - copy_btn = FlameButton('Send to Ftrack', send_to_ftrack, window) + ftrack_send_btn = FlameButton('Send to Ftrack', send_to_ftrack, window) ## Window Layout gridbox = QtWidgets.QGridLayout() gridbox.setMargin(20) + gridbox.setHorizontalSpacing(20) gridbox.addWidget(hierarchy_label, 0, 0) - gridbox.addWidget(hierarchy_template, 0, 1) - gridbox.addWidget(tree, 1, 0, 5, 1) - gridbox.addWidget(select_all_btn, 1, 1) - gridbox.addWidget(copy_btn, 2, 1) + gridbox.addWidget(hierarchy_template, 0, 1, 1, 4) + gridbox.addWidget(tree, 1, 0, 5, 5) + gridbox.addWidget(select_all_btn, 6, 3) + gridbox.addWidget(ftrack_send_btn, 6, 4) window.setLayout(gridbox) window.show() From 2792750d253882f033317f5aebead35a3e9e08eb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 29 Oct 2021 17:43:21 +0200 Subject: [PATCH 009/307] make it nicer --- .../flame/utility_scripts/openpype_flame_to_ftrack.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py index 9364d8d57f..37c8109713 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py @@ -105,10 +105,12 @@ class FlameLabel(QtWidgets.QLabel): 'QLabel:disabled {color: #6a6a6a}') elif label_type == 'background': self.setAlignment(QtCore.Qt.AlignCenter) - self.setStyleSheet('color: #9a9a9a; background-color: #393939; font: 14px "Discreet"') + self.setStyleSheet( + 'color: #9a9a9a; background-color: #393939; font: 14px "Discreet"') elif label_type == 'outline': self.setAlignment(QtCore.Qt.AlignCenter) - self.setStyleSheet('color: #9a9a9a; background-color: #212121; border: 1px solid #404040; font: 14px "Discreet"') + self.setStyleSheet( + 'color: #9a9a9a; background-color: #212121; border: 1px solid #404040; font: 14px "Discreet"') class FlameLineEdit(QtWidgets.QLineEdit): @@ -192,7 +194,7 @@ def main_window(selection): def _seconds(value): if isinstance(value, str): - _zip_ft = zip((3600, 60, 1, 1/framerate), value.split(':')) + _zip_ft = zip((3600, 60, 1, 1 / framerate), value.split(':')) return sum(f * float(t) for f, t in _zip_ft) elif isinstance(value, (int, float)): return value / framerate @@ -450,6 +452,7 @@ def scope_sequence(selection): import flame return any(isinstance(item, flame.PySequence) for item in selection) + def get_media_panel_custom_ui_actions(): return [ { From 70928f410c6f21844fd90ce40ea9e14e3d4dd3cf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 29 Oct 2021 23:32:49 +0200 Subject: [PATCH 010/307] adding custom attributes, tasks, comment --- .../openpype_flame_to_ftrack.py | 190 ++++++++++++++---- 1 file changed, 147 insertions(+), 43 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py index 37c8109713..958d7f7a11 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py @@ -5,7 +5,7 @@ from contextlib import contextmanager # Constants WORKFILE_START_FRAME = 1001 -HIERARCHY_TEMPLATE = "shots/{sequence}" +HIERARCHY_TEMPLATE = "shots[Folder]/{sequence}[Sequence]" CREATE_TASK_TYPE = "Compositing" @@ -268,8 +268,10 @@ def main_window(selection): import flame import six import sys + import re - def create_ftrack_entity(session, type, name, parent): + def create_ftrack_entity(session, type, name, parent=None): + parent = parent or f_project entity = session.create(type, { 'name': name, 'parent': parent @@ -283,20 +285,71 @@ def main_window(selection): six.reraise(tp, value, tb) return entity - def get_ftrack_entity(session, type, name, project_entity): + def get_ftrack_entity(session, type, name, parent): query = '{} where name is "{}" and project_id is "{}"'.format( - type, name, project_entity["id"]) - return session.query(query).one() + type, name, f_project["id"]) + + try: + entity = session.query(query).one() + except Exception: + entity = None + + # if entity doesnt exist then create one + if not entity: + entity = create_ftrack_entity( + session, + type, + name, + parent + ) + + return entity def generate_parents_from_template(template): - template_split = template.split("/") - return template_split + parents = [] + t_split = template.split("/") + replace_patern = re.compile(r"(\[.*\])") + type_patern = re.compile(r"\[(.*)\]") + for t_s in t_split: + match_type = type_patern.findall(t_s) + if not match_type: + raise Exception(( + "Missing correct type flag in : {}" + "/n Example: name[Type]").format( + t_s) + ) + new_name = re.sub(replace_patern, "", t_s) + f_type = match_type.pop() + + parents.append((new_name, f_type)) + + return parents + + def create_task(task_type, parent): + existing_task = [ + child for child in parent['children'] + if child.entity_type.lower() == 'task' + if child['name'].lower() in task_type.lower() + ] + print(existing_task) + if existing_task: + return existing_task + + # create task on shot + return session.create('Task', { + "name": task_type.lower(), + "type": task_type, + "parent": parent + }) + + # start procedure with maintained_ftrack_session() as session: print("Ftrack session is: {}".format(session)) # get project name from flame current project project_name = flame.project.current_project.name + # get project from ftrack - # ftrack project name has to be the same as flame project! query = 'Project where full_name is "{}"'.format(project_name) @@ -304,52 +357,103 @@ def main_window(selection): print("Ftrack project is: {}".format(f_project)) # Get all selected items from treewidget - clips_info = [] - for item in tree.selectedItems(): - parents = generate_parents_from_template( - hierarchy_template.text()) - print(parents) - - f_entity = get_ftrack_entity( - session, - "Shot", - item.text(1), - f_project - ) - # if entity doesnt exist then create one - if not f_entity: - f_entity = create_ftrack_entity( - session, - "Shot", - item.text(1), - f_project - ) - print("Shot entity is: {}".format(f_entity)) - # solve handle start and end handles = item.text(5) if ":" in handles: _s, _e = handles.split(":") - handles = (int(_s), int(_e)) + handle_start = int(_s) + handle_end = int(_e) else: - handles = (int(handles), int(handles)) + handle_start = int(handles) + handle_end = int(handles) + + # frame ranges + frame_start = int(item.text(3)) + frame_duration = int(item.text(4)) + frame_end = frame_start + frame_duration + + # description + shot_description = item.text(6) + task_description = item.text(7) + + # other + task_type = item.text(2) + shot_name = item.text(1) + sequence_name = item.text(0) # populate full shot info - tree_line = { - "sequence": item.text(0), - "shot": item.text(1), - "task": item.text(2), - "frameStart": int(item.text(3)), - "duration": int(item.text(4)), - "handles": handles, - "shotDescription": item.text(6), - "taskDescription": item.text(7) + shot_attributes = { + "sequence": sequence_name, + "shot": shot_name, + "task": task_type } - print(tree_line) - clips_info.append(tree_line) - print("selected clips: {}".format(pformat(clips_info))) + # format hierarchy template + hierarchy_text = hierarchy_template.text() + hierarchy_text = hierarchy_text.format(**shot_attributes) + print(hierarchy_text) + + # solve parents + parents = generate_parents_from_template(hierarchy_text) + print(parents) + + # obtain shot parents entities + _parent = None + for _name, _type in parents: + p_entity = get_ftrack_entity( + session, + _type, + _name, + _parent + ) + print(p_entity) + _parent = p_entity + + # obtain shot ftrack entity + f_s_entity = get_ftrack_entity( + session, + "Shot", + item.text(1), + _parent + ) + print("Shot entity is: {}".format(f_s_entity)) + + # create custom attributtes + custom_attrs = { + "frameStart": frame_start, + "frameEnd": frame_end, + "handleStart": handle_start, + "handleEnd": handle_end + } + + # update custom attributes on shot entity + for key in custom_attrs: + f_s_entity['custom_attributes'][key] = custom_attrs[key] + + task_entity = create_task(task_type, f_s_entity) + print(task_entity) + + # Create notes. + user = session.query( + "User where username is \"{}\"".format(session.api_user) + ).first() + + print(user) + print(shot_description) + + f_s_entity.create_note(shot_description, author=user) + + if task_description: + task_entity.create_note(task_description, user) + + try: + session.commit() + except Exception: + tp, value, tb = sys.exc_info() + session.rollback() + session._configure_locations() + six.reraise(tp, value, tb) # creating ui window = QtWidgets.QWidget() From a4b2bf095143abbab0e51f768298bf149a9705aa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sat, 30 Oct 2021 23:44:52 +0200 Subject: [PATCH 011/307] flame: fixing Task creation and adding notes --- .../publish/integrate_hierarchy_ftrack.py | 839 +++++++++++------- 1 file changed, 533 insertions(+), 306 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/default_modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index fbd64d9f70..25607aead6 100644 --- a/openpype/modules/default_modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/default_modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -1,356 +1,583 @@ -import sys -import collections -import six -import pyblish.api -from avalon import io +from __future__ import print_function +from PySide2 import QtWidgets, QtCore +from pprint import pformat +from contextlib import contextmanager -# Copy of constant `openpype_modules.ftrack.lib.avalon_sync.CUST_ATTR_AUTO_SYNC` -CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" -CUST_ATTR_GROUP = "openpype" +# Constants +WORKFILE_START_FRAME = 1001 +HIERARCHY_TEMPLATE = "shots[Folder]/{sequence}[Sequence]" +CREATE_TASK_TYPE = "Compositing" -# Copy of `get_pype_attr` from openpype_modules.ftrack.lib -# TODO import from openpype's ftrack module when possible to not break Python 2 -def get_pype_attr(session, split_hierarchical=True): - custom_attributes = [] - hier_custom_attributes = [] - # TODO remove deprecated "avalon" group from query - cust_attrs_query = ( - "select id, entity_type, object_type_id, is_hierarchical, default" - " from CustomAttributeConfiguration" - # Kept `pype` for Backwards Compatiblity - " where group.name in (\"pype\", \"{}\")" - ).format(CUST_ATTR_GROUP) - all_avalon_attr = session.query(cust_attrs_query).all() - for cust_attr in all_avalon_attr: - if split_hierarchical and cust_attr["is_hierarchical"]: - hier_custom_attributes.append(cust_attr) - continue +@contextmanager +def maintained_ftrack_session(): + import ftrack_api + import os - custom_attributes.append(cust_attr) + def validate_credentials(url, user, api): + first_validation = True + if not user: + print('- Ftrack Username is not set') + first_validation = False + if not api: + print('- Ftrack API key is not set') + first_validation = False + if not first_validation: + return False - if split_hierarchical: - # return tuple - return custom_attributes, hier_custom_attributes - - return custom_attributes - - -class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): - """ - Create entities in ftrack based on collected data from premiere - Example of entry data: - { - "ProjectXS": { - "entity_type": "Project", - "custom_attributes": { - "fps": 24,... - }, - "tasks": [ - "Compositing", - "Lighting",... *task must exist as task type in project schema* - ], - "childs": { - "sq01": { - "entity_type": "Sequence", - ... - } + try: + session = ftrack_api.Session( + server_url=url, + api_user=user, + api_key=api + ) + session.close() + except Exception as _e: + print( + "Can't log into Ftrack with used credentials: {}".format( + _e) + ) + ftrack_cred = { + 'Ftrack server': str(url), + 'Username': str(user), + 'API key': str(api), } - } - } + + item_lens = [len(key) + 1 for key in ftrack_cred] + justify_len = max(*item_lens) + for key, value in ftrack_cred.items(): + print('{} {}'.format((key + ':').ljust( + justify_len, ' '), value)) + return False + print( + 'Credentials Username: "{}", API key: "{}" are valid.'.format( + user, api) + ) + return True + + # fill your own credentials + url = os.getenv("FTRACK_SERVER") + user = os.getenv("FTRACK_API_USER") + api = os.getenv("FTRACK_API_KEY") + + try: + assert validate_credentials(url, user, api), ( + "Ftrack credentials failed") + # open ftrack session + session = ftrack_api.Session( + server_url=url, + api_user=user, + api_key=api + ) + yield session + except Exception as _E: + print( + "ERROR: {}".format(_E)) + finally: + # close the session + session.close() + + +class FlameLabel(QtWidgets.QLabel): + """ + Custom Qt Flame Label Widget + + For different label looks set label_type as: 'normal', 'background', or 'outline' + + To use: + + label = FlameLabel('Label Name', 'normal', window) """ - order = pyblish.api.IntegratorOrder - 0.04 - label = 'Integrate Hierarchy To Ftrack' - families = ["shot"] - hosts = ["hiero", "resolve", "standalonepublisher"] - optional = False + def __init__(self, label_name, label_type, parent_window, *args, **kwargs): + super(FlameLabel, self).__init__(*args, **kwargs) - def process(self, context): - self.context = context - if "hierarchyContext" not in self.context.data: - return + self.setText(label_name) + self.setParent(parent_window) + self.setMinimumSize(130, 28) + self.setMaximumHeight(28) + self.setFocusPolicy(QtCore.Qt.NoFocus) - hierarchy_context = self.context.data["hierarchyContext"] + # Set label stylesheet based on label_type - self.session = self.context.data["ftrackSession"] - project_name = self.context.data["projectEntity"]["name"] - query = 'Project where full_name is "{}"'.format(project_name) - project = self.session.query(query).one() - auto_sync_state = project[ - "custom_attributes"][CUST_ATTR_AUTO_SYNC] + if label_type == 'normal': + self.setStyleSheet('QLabel {color: #9a9a9a; border-bottom: 1px inset #282828; font: 14px "Discreet"}' + 'QLabel:disabled {color: #6a6a6a}') + elif label_type == 'background': + self.setAlignment(QtCore.Qt.AlignCenter) + self.setStyleSheet( + 'color: #9a9a9a; background-color: #393939; font: 14px "Discreet"') + elif label_type == 'outline': + self.setAlignment(QtCore.Qt.AlignCenter) + self.setStyleSheet( + 'color: #9a9a9a; background-color: #212121; border: 1px solid #404040; font: 14px "Discreet"') - if not io.Session: - io.install() - self.ft_project = None +class FlameLineEdit(QtWidgets.QLineEdit): + """ + Custom Qt Flame Line Edit Widget - input_data = hierarchy_context + Main window should include this: window.setFocusPolicy(QtCore.Qt.StrongFocus) - # disable termporarily ftrack project's autosyncing - if auto_sync_state: - self.auto_sync_off(project) + To use: - try: - # import ftrack hierarchy - self.import_to_ftrack(input_data) - except Exception: - raise - finally: - if auto_sync_state: - self.auto_sync_on(project) + line_edit = FlameLineEdit('Some text here', window) + """ - def import_to_ftrack(self, input_data, parent=None): - # Prequery hiearchical custom attributes - hier_custom_attributes = get_pype_attr(self.session)[1] - hier_attr_by_key = { - attr["key"]: attr - for attr in hier_custom_attributes - } - # Get ftrack api module (as they are different per python version) - ftrack_api = self.context.data["ftrackPythonModule"] + def __init__(self, text, parent_window, *args, **kwargs): + super(FlameLineEdit, self).__init__(*args, **kwargs) - for entity_name in input_data: - entity_data = input_data[entity_name] - entity_type = entity_data['entity_type'] - self.log.debug(entity_data) - self.log.debug(entity_type) + self.setText(text) + self.setParent(parent_window) + self.setMinimumHeight(28) + self.setMinimumWidth(110) + self.setStyleSheet('QLineEdit {color: #9a9a9a; background-color: #373e47; selection-color: #262626; selection-background-color: #b8b1a7; font: 14px "Discreet"}' + 'QLineEdit:focus {background-color: #474e58}' + 'QLineEdit:disabled {color: #6a6a6a; background-color: #373737}') - if entity_type.lower() == 'project': - query = 'Project where full_name is "{}"'.format(entity_name) - entity = self.session.query(query).one() - self.ft_project = entity - self.task_types = self.get_all_task_types(entity) - elif self.ft_project is None or parent is None: - raise AssertionError( - "Collected items are not in right order!" - ) +class FlameTreeWidget(QtWidgets.QTreeWidget): + """ + Custom Qt Flame Tree Widget - # try to find if entity already exists - else: - query = ( - 'TypedContext where name is "{0}" and ' - 'project_id is "{1}"' - ).format(entity_name, self.ft_project["id"]) - try: - entity = self.session.query(query).one() - except Exception: - entity = None + To use: - # Create entity if not exists - if entity is None: - entity = self.create_entity( - name=entity_name, - type=entity_type, - parent=parent - ) - # self.log.info('entity: {}'.format(dict(entity))) - # CUSTOM ATTRIBUTES - custom_attributes = entity_data.get('custom_attributes', []) - instances = [ - i for i in self.context if i.data['asset'] in entity['name'] - ] - for key in custom_attributes: - hier_attr = hier_attr_by_key.get(key) - # Use simple method if key is not hierarchical - if not hier_attr: - assert (key in entity['custom_attributes']), ( - 'Missing custom attribute key: `{0}` in attrs: ' - '`{1}`'.format(key, entity['custom_attributes'].keys()) - ) + tree_headers = ['Header1', 'Header2', 'Header3', 'Header4'] + tree = FlameTreeWidget(tree_headers, window) + """ - entity['custom_attributes'][key] = custom_attributes[key] + def __init__(self, tree_headers, parent_window, *args, **kwargs): + super(FlameTreeWidget, self).__init__(*args, **kwargs) - else: - # Use ftrack operations method to set hiearchical - # attribute value. - # - this is because there may be non hiearchical custom - # attributes with different properties - entity_key = collections.OrderedDict() - entity_key["configuration_id"] = hier_attr["id"] - entity_key["entity_id"] = entity["id"] - self.session.recorded_operations.push( - ftrack_api.operation.UpdateEntityOperation( - "ContextCustomAttributeValue", - entity_key, - "value", - ftrack_api.symbol.NOT_SET, - custom_attributes[key] + self.setMinimumWidth(1000) + self.setMinimumHeight(300) + self.setSortingEnabled(True) + self.sortByColumn(0, QtCore.Qt.AscendingOrder) + self.setAlternatingRowColors(True) + self.setFocusPolicy(QtCore.Qt.NoFocus) + self.setStyleSheet('QTreeWidget {color: #9a9a9a; background-color: #2a2a2a; alternate-background-color: #2d2d2d; font: 14px "Discreet"}' + 'QTreeWidget::item:selected {color: #d9d9d9; background-color: #474747; border: 1px solid #111111}' + 'QHeaderView {color: #9a9a9a; background-color: #393939; font: 14px "Discreet"}' + 'QTreeWidget::item:selected {selection-background-color: #111111}' + 'QMenu {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' + 'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}') + self.verticalScrollBar().setStyleSheet('color: #818181') + self.horizontalScrollBar().setStyleSheet('color: #818181') + self.setHeaderLabels(tree_headers) + + +class FlameButton(QtWidgets.QPushButton): + """ + Custom Qt Flame Button Widget + + To use: + + button = FlameButton('Button Name', do_this_when_pressed, window) + """ + + def __init__(self, button_name, do_when_pressed, parent_window, *args, **kwargs): + super(FlameButton, self).__init__(*args, **kwargs) + + self.setText(button_name) + self.setParent(parent_window) + self.setMinimumSize(QtCore.QSize(110, 28)) + self.setMaximumSize(QtCore.QSize(110, 28)) + self.setFocusPolicy(QtCore.Qt.NoFocus) + self.clicked.connect(do_when_pressed) + self.setStyleSheet('QPushButton {color: #9a9a9a; background-color: #424142; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}' + 'QPushButton:pressed {color: #d9d9d9; background-color: #4f4f4f; border-top: 1px inset #666666; font: italic}' + 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}') + + +def main_window(selection): + def timecode_to_frames(timecode, framerate): + + def _seconds(value): + if isinstance(value, str): + _zip_ft = zip((3600, 60, 1, 1 / framerate), value.split(':')) + return sum(f * float(t) for f, t in _zip_ft) + elif isinstance(value, (int, float)): + return value / framerate + return 0 + + def _frames(seconds): + return seconds * framerate + + def timecode_to_frames(_timecode, start=None): + return _frames(_seconds(_timecode) - _seconds(start)) + + if '+' in timecode: + timecode = timecode.replace('+', ':') + elif '#' in timecode: + timecode = timecode.replace('#', ':') + + frames = int(round(timecode_to_frames(timecode, start='00:00:00:00'))) + + return frames + + def timeline_info(selection): + import flame + + # identificar as informacoes dos segmentos na timeline + for sequence in selection: + frame_rate = float(str(sequence.frame_rate)[:-4]) + for ver in sequence.versions: + for tracks in ver.tracks: + for segment in tracks.segments: + print(segment.type) + # get clip frame duration + record_duration = str(segment.record_duration)[1:-1] + clip_duration = timecode_to_frames( + record_duration, frame_rate) + + # populate shot source metadata + shot_description = "" + for attr in ["tape_name", "source_name", "head", + "tail", "file_path"]: + if not hasattr(segment, attr): + continue + _value = getattr(segment, attr) + _label = attr.replace("_", " ").capitalize() + row = "{}: {}\n".format(_label, _value) + shot_description += row + + # Add timeline segment to tree + QtWidgets.QTreeWidgetItem(tree, [ + str(sequence.name)[1:-1], # seq + str(segment.name)[1:-1], # shot + CREATE_TASK_TYPE, # task type + str(WORKFILE_START_FRAME), # start frame + str(clip_duration), # clip duration + "0:0", # handles + shot_description, # shot description + str(segment.comment)[1:-1] # task description + ]).setFlags( + QtCore.Qt.ItemIsEditable + | QtCore.Qt.ItemIsEnabled + | QtCore.Qt.ItemIsSelectable ) - ) - for instance in instances: - instance.data['ftrackEntity'] = entity + # Select top item in tree + tree.setCurrentItem(tree.topLevelItem(0)) + + def select_all(): + + tree.selectAll() + + def send_to_ftrack(): + import flame + import six + import sys + import re + + def create_ftrack_entity(session, type, name, parent=None): + parent = parent or f_project + entity = session.create(type, { + 'name': name, + 'parent': parent + }) + try: + session.commit() + except Exception: + tp, value, tb = sys.exc_info() + session.rollback() + session._configure_locations() + six.reraise(tp, value, tb) + return entity + + def get_ftrack_entity(session, type, name, parent): + query = '{} where name is "{}" and project_id is "{}"'.format( + type, name, f_project["id"]) + + try: + entity = session.query(query).one() + except Exception: + entity = None + + # if entity doesnt exist then create one + if not entity: + entity = create_ftrack_entity( + session, + type, + name, + parent + ) + + return entity + + def generate_parents_from_template(template): + parents = [] + t_split = template.split("/") + replace_patern = re.compile(r"(\[.*\])") + type_patern = re.compile(r"\[(.*)\]") + + for t_s in t_split: + match_type = type_patern.findall(t_s) + if not match_type: + raise Exception(( + "Missing correct type flag in : {}" + "/n Example: name[Type]").format( + t_s) + ) + new_name = re.sub(replace_patern, "", t_s) + f_type = match_type.pop() + + parents.append((new_name, f_type)) + + return parents + + def get_all_task_types(): + tasks = {} + proj_template = f_project['project_schema'] + temp_task_types = proj_template['_task_type_schema']['types'] + + for type in temp_task_types: + if type['name'] not in tasks: + tasks[type['name']] = type + + return tasks + + def create_task(task_type, parent): + existing_task = [ + child for child in parent['children'] + if child.entity_type.lower() == 'task' + if child['name'].lower() in task_type.lower() + ] + + if existing_task: + return existing_task + + task = session.create('Task', { + "name": task_type.lower(), + "parent": parent + }) + task_types = get_all_task_types() + task["type"] = task_types[task_type] + + return task + + + # start procedure + with maintained_ftrack_session() as session: + print("Ftrack session is: {}".format(session)) + + # get project name from flame current project + project_name = flame.project.current_project.name + + # get project from ftrack - + # ftrack project name has to be the same as flame project! + query = 'Project where full_name is "{}"'.format(project_name) + f_project = session.query(query).one() + print("Ftrack project is: {}".format(f_project)) + + # Get all selected items from treewidget + for item in tree.selectedItems(): + # solve handle start and end + handles = item.text(5) + if ":" in handles: + _s, _e = handles.split(":") + handle_start = int(_s) + handle_end = int(_e) + else: + handle_start = int(handles) + handle_end = int(handles) + + # frame ranges + frame_start = int(item.text(3)) + frame_duration = int(item.text(4)) + frame_end = frame_start + frame_duration + + # description + shot_description = item.text(6) + task_description = item.text(7) + + # other + task_type = item.text(2) + shot_name = item.text(1) + sequence_name = item.text(0) + + # populate full shot info + shot_attributes = { + "sequence": sequence_name, + "shot": shot_name, + "task": task_type + } + + # format hierarchy template + hierarchy_text = hierarchy_template.text() + hierarchy_text = hierarchy_text.format(**shot_attributes) + print(hierarchy_text) + + # solve parents + parents = generate_parents_from_template(hierarchy_text) + print(parents) + + # obtain shot parents entities + _parent = None + for _name, _type in parents: + p_entity = get_ftrack_entity( + session, + _type, + _name, + _parent + ) + print(p_entity) + _parent = p_entity + + # obtain shot ftrack entity + f_s_entity = get_ftrack_entity( + session, + "Shot", + item.text(1), + _parent + ) + print("Shot entity is: {}".format(f_s_entity)) + + # create custom attributtes + custom_attrs = { + "frameStart": frame_start, + "frameEnd": frame_end, + "handleStart": handle_start, + "handleEnd": handle_end + } + + # update custom attributes on shot entity + for key in custom_attrs: + f_s_entity['custom_attributes'][key] = custom_attrs[key] + + task_entity = create_task(task_type, f_s_entity) + + # Create notes. + user = session.query( + "User where username is \"{}\"".format(session.api_user) + ).first() + + f_s_entity.create_note(shot_description, author=user) + + if task_description: + task_entity.create_note(task_description, user) try: - self.session.commit() + session.commit() except Exception: tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() + session.rollback() + session._configure_locations() six.reraise(tp, value, tb) - # TASKS - tasks = entity_data.get('tasks', []) - existing_tasks = [] - tasks_to_create = [] - for child in entity['children']: - if child.entity_type.lower() == 'task': - existing_tasks.append(child['name'].lower()) - # existing_tasks.append(child['type']['name']) + # creating ui + window = QtWidgets.QWidget() + window.setMinimumSize(1500, 600) + window.setWindowTitle('Sequence Shots to Ftrack') + window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + window.setAttribute(QtCore.Qt.WA_DeleteOnClose) + window.setStyleSheet('background-color: #313131') - for task_name in tasks: - task_type = tasks[task_name]["type"] - if task_name.lower() in existing_tasks: - print("Task {} already exists".format(task_name)) - continue - tasks_to_create.append((task_name, task_type)) + # Center window in linux + resolution = QtWidgets.QDesktopWidget().screenGeometry() + window.move((resolution.width() / 2) - (window.frameSize().width() / 2), + (resolution.height() / 2) - (window.frameSize().height() / 2)) - for task_name, task_type in tasks_to_create: - self.create_task( - name=task_name, - task_type=task_type, - parent=entity - ) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) + # TreeWidget + columns = { + "Sequence name": { + "columnWidth": 100, + "order": 0 + }, + "Shot name": { + "columnWidth": 100, + "order": 1 + }, + "Task type": { + "columnWidth": 100, + "order": 2 + }, + "Start frame": { + "columnWidth": 100, + "order": 3 + }, + "Clip duration": { + "columnWidth": 100, + "order": 4 + }, + "Handles": { + "columnWidth": 100, + "order": 5 + }, + "Shot description": { + "columnWidth": 300, + "order": 6 + }, + "Task description": { + "columnWidth": 300, + "order": 7 + }, + } + ordered_column_labels = columns.keys() + for _name, _value in columns.items(): + ordered_column_labels.pop(_value["order"]) + ordered_column_labels.insert(_value["order"], _name) - # Incoming links. - self.create_links(entity_data, entity) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) + print(ordered_column_labels) - # Create notes. - user = self.session.query( - "User where username is \"{}\"".format(self.session.api_user) - ).first() - if user: - for comment in entity_data.get("comments", []): - entity.create_note(comment, user) - else: - self.log.warning( - "Was not able to query current User {}".format( - self.session.api_user - ) - ) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) + tree = FlameTreeWidget(ordered_column_labels, window) - # Import children. - if 'childs' in entity_data: - self.import_to_ftrack( - entity_data['childs'], entity) + # Allow multiple items in tree to be selected + tree.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) - def create_links(self, entity_data, entity): - # Clear existing links. - for link in entity.get("incoming_links", []): - self.session.delete(link) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) + # Set tree column width + for _name, _val in columns.items(): + tree.setColumnWidth( + _val["order"], + _val["columnWidth"] + ) - # Create new links. - for input in entity_data.get("inputs", []): - input_id = io.find_one({"_id": input})["data"]["ftrackId"] - assetbuild = self.session.get("AssetBuild", input_id) - self.log.debug( - "Creating link from {0} to {1}".format( - assetbuild["name"], entity["name"] - ) - ) - self.session.create( - "TypedContextLink", {"from": assetbuild, "to": entity} - ) + # Prevent weird characters when shrinking tree columns + tree.setTextElideMode(QtCore.Qt.ElideNone) - def get_all_task_types(self, project): - tasks = {} - proj_template = project['project_schema'] - temp_task_types = proj_template['_task_type_schema']['types'] + # input fields + hierarchy_label = FlameLabel( + 'Parents template', 'normal', window) + hierarchy_template = FlameLineEdit(HIERARCHY_TEMPLATE, window) - for type in temp_task_types: - if type['name'] not in tasks: - tasks[type['name']] = type + ## Button + select_all_btn = FlameButton('Select All', select_all, window) + ftrack_send_btn = FlameButton('Send to Ftrack', send_to_ftrack, window) - return tasks + ## Window Layout + gridbox = QtWidgets.QGridLayout() + gridbox.setMargin(20) + gridbox.setHorizontalSpacing(20) + gridbox.addWidget(hierarchy_label, 0, 0) + gridbox.addWidget(hierarchy_template, 0, 1, 1, 4) + gridbox.addWidget(tree, 1, 0, 5, 5) + gridbox.addWidget(select_all_btn, 6, 3) + gridbox.addWidget(ftrack_send_btn, 6, 4) - def create_task(self, name, task_type, parent): - task = self.session.create('Task', { - 'name': name, - 'parent': parent - }) - # TODO not secured!!! - check if task_type exists - self.log.info(task_type) - self.log.info(self.task_types) - task['type'] = self.task_types[task_type] + window.setLayout(gridbox) + window.show() - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) + timeline_info(selection) - return task + return window - def create_entity(self, name, type, parent): - entity = self.session.create(type, { - 'name': name, - 'parent': parent - }) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - return entity +def scope_sequence(selection): + import flame + return any(isinstance(item, flame.PySequence) for item in selection) - def auto_sync_off(self, project): - project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = False - self.log.info("Ftrack autosync swithed off") +def get_media_panel_custom_ui_actions(): + return [ + { + "name": "OpenPype: Ftrack", + "actions": [ + { + "name": "Create Shots", + "isVisible": scope_sequence, + "execute": main_window + } + ] + } - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - def auto_sync_on(self, project): - - project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = True - - self.log.info("Ftrack autosync swithed on") - - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) + ] From af1c979fa16dd176f1dbc8118979f36800f44334 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 1 Nov 2021 13:55:01 +0100 Subject: [PATCH 012/307] adding export presets --- .../openpype_seg_thumbnails_jpg.xml | 58 +++++++++++++++ .../export_preset/openpype_seg_video_h264.xml | 72 +++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml create mode 100644 openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml new file mode 100644 index 0000000000..fa43ceece7 --- /dev/null +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml @@ -0,0 +1,58 @@ + + + sequence + Creates a 8-bit Jpeg file per segment. + + NONE + + <name> + True + True + + image + FX + NoChange + False + 10 + + True + False + + audio + FX + FlattenTracks + True + 10 + + + + + 4 + 1 + 2 + + diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml new file mode 100644 index 0000000000..64447b76e8 --- /dev/null +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml @@ -0,0 +1,72 @@ + + + sequence + Create MOV H264 files per segment with thumbnail + + NONE + + <name> + True + True + + movie + FX + FlattenTracks + True + 5 + + True + False + + audio + Original + NoChange + True + 5 + + + + QuickTime + <segment name>_<video codec> + 0 + PCS_709 + None + Autodesk + Flame + 2021 + + + + 4 + 1 + 2 + + \ No newline at end of file From 0c252055e88b8b44fb1c5629e31b78562b681ce0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 1 Nov 2021 14:02:28 +0100 Subject: [PATCH 013/307] fix module code content swap by mistake --- .../openpype_flame_to_ftrack.py | 46 +- .../publish/integrate_hierarchy_ftrack.py | 855 +++++++----------- 2 files changed, 348 insertions(+), 553 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py index 958d7f7a11..ba57023edc 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py @@ -3,11 +3,17 @@ from PySide2 import QtWidgets, QtCore from pprint import pformat from contextlib import contextmanager + # Constants WORKFILE_START_FRAME = 1001 HIERARCHY_TEMPLATE = "shots[Folder]/{sequence}[Sequence]" CREATE_TASK_TYPE = "Compositing" +# Fill following constants or set them via environment variable +FTRACK_API_KEY = None +FTRACK_API_USER = None +FTRACK_SERVER = None + @contextmanager def maintained_ftrack_session(): @@ -56,9 +62,9 @@ def maintained_ftrack_session(): return True # fill your own credentials - url = os.getenv("FTRACK_SERVER") - user = os.getenv("FTRACK_API_USER") - api = os.getenv("FTRACK_API_KEY") + url = FTRACK_SERVER or os.getenv("FTRACK_SERVER") or "" + user = FTRACK_API_USER or os.getenv("FTRACK_API_USER") or "" + api = FTRACK_API_KEY or os.getenv("FTRACK_API_KEY") or "" try: assert validate_credentials(url, user, api), ( @@ -326,22 +332,35 @@ def main_window(selection): return parents + def get_all_task_types(): + tasks = {} + proj_template = f_project['project_schema'] + temp_task_types = proj_template['_task_type_schema']['types'] + + for type in temp_task_types: + if type['name'] not in tasks: + tasks[type['name']] = type + + return tasks + def create_task(task_type, parent): existing_task = [ child for child in parent['children'] if child.entity_type.lower() == 'task' if child['name'].lower() in task_type.lower() ] - print(existing_task) + if existing_task: return existing_task - # create task on shot - return session.create('Task', { + task = session.create('Task', { "name": task_type.lower(), - "type": task_type, "parent": parent }) + task_types = get_all_task_types() + task["type"] = task_types[task_type] + + return task # start procedure with maintained_ftrack_session() as session: @@ -432,16 +451,12 @@ def main_window(selection): f_s_entity['custom_attributes'][key] = custom_attrs[key] task_entity = create_task(task_type, f_s_entity) - print(task_entity) # Create notes. user = session.query( "User where username is \"{}\"".format(session.api_user) ).first() - print(user) - print(shot_description) - f_s_entity.create_note(shot_description, author=user) if task_description: @@ -530,6 +545,11 @@ def main_window(selection): 'Parents template', 'normal', window) hierarchy_template = FlameLineEdit(HIERARCHY_TEMPLATE, window) + # input fields + start_frame_label = FlameLabel( + 'Workfile start frame', 'normal', window) + start_frame = FlameLineEdit(WORKFILE_START_FRAME, window) + ## Button select_all_btn = FlameButton('Select All', select_all, window) ftrack_send_btn = FlameButton('Send to Ftrack', send_to_ftrack, window) @@ -539,7 +559,9 @@ def main_window(selection): gridbox.setMargin(20) gridbox.setHorizontalSpacing(20) gridbox.addWidget(hierarchy_label, 0, 0) - gridbox.addWidget(hierarchy_template, 0, 1, 1, 4) + gridbox.addWidget(hierarchy_template, 0, 1, 1, 2) + gridbox.addWidget(start_frame_label, 0, 3) + gridbox.addWidget(start_frame, 0, 3, 1, 1) gridbox.addWidget(tree, 1, 0, 5, 5) gridbox.addWidget(select_all_btn, 6, 3) gridbox.addWidget(ftrack_send_btn, 6, 4) diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/default_modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index 25607aead6..fbd64d9f70 100644 --- a/openpype/modules/default_modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/default_modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -1,583 +1,356 @@ -from __future__ import print_function -from PySide2 import QtWidgets, QtCore -from pprint import pformat -from contextlib import contextmanager +import sys +import collections +import six +import pyblish.api +from avalon import io -# Constants -WORKFILE_START_FRAME = 1001 -HIERARCHY_TEMPLATE = "shots[Folder]/{sequence}[Sequence]" -CREATE_TASK_TYPE = "Compositing" +# Copy of constant `openpype_modules.ftrack.lib.avalon_sync.CUST_ATTR_AUTO_SYNC` +CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" +CUST_ATTR_GROUP = "openpype" -@contextmanager -def maintained_ftrack_session(): - import ftrack_api - import os +# Copy of `get_pype_attr` from openpype_modules.ftrack.lib +# TODO import from openpype's ftrack module when possible to not break Python 2 +def get_pype_attr(session, split_hierarchical=True): + custom_attributes = [] + hier_custom_attributes = [] + # TODO remove deprecated "avalon" group from query + cust_attrs_query = ( + "select id, entity_type, object_type_id, is_hierarchical, default" + " from CustomAttributeConfiguration" + # Kept `pype` for Backwards Compatiblity + " where group.name in (\"pype\", \"{}\")" + ).format(CUST_ATTR_GROUP) + all_avalon_attr = session.query(cust_attrs_query).all() + for cust_attr in all_avalon_attr: + if split_hierarchical and cust_attr["is_hierarchical"]: + hier_custom_attributes.append(cust_attr) + continue - def validate_credentials(url, user, api): - first_validation = True - if not user: - print('- Ftrack Username is not set') - first_validation = False - if not api: - print('- Ftrack API key is not set') - first_validation = False - if not first_validation: - return False + custom_attributes.append(cust_attr) + + if split_hierarchical: + # return tuple + return custom_attributes, hier_custom_attributes + + return custom_attributes + + +class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): + """ + Create entities in ftrack based on collected data from premiere + Example of entry data: + { + "ProjectXS": { + "entity_type": "Project", + "custom_attributes": { + "fps": 24,... + }, + "tasks": [ + "Compositing", + "Lighting",... *task must exist as task type in project schema* + ], + "childs": { + "sq01": { + "entity_type": "Sequence", + ... + } + } + } + } + """ + + order = pyblish.api.IntegratorOrder - 0.04 + label = 'Integrate Hierarchy To Ftrack' + families = ["shot"] + hosts = ["hiero", "resolve", "standalonepublisher"] + optional = False + + def process(self, context): + self.context = context + if "hierarchyContext" not in self.context.data: + return + + hierarchy_context = self.context.data["hierarchyContext"] + + self.session = self.context.data["ftrackSession"] + project_name = self.context.data["projectEntity"]["name"] + query = 'Project where full_name is "{}"'.format(project_name) + project = self.session.query(query).one() + auto_sync_state = project[ + "custom_attributes"][CUST_ATTR_AUTO_SYNC] + + if not io.Session: + io.install() + + self.ft_project = None + + input_data = hierarchy_context + + # disable termporarily ftrack project's autosyncing + if auto_sync_state: + self.auto_sync_off(project) try: - session = ftrack_api.Session( - server_url=url, - api_user=user, - api_key=api - ) - session.close() - except Exception as _e: - print( - "Can't log into Ftrack with used credentials: {}".format( - _e) - ) - ftrack_cred = { - 'Ftrack server': str(url), - 'Username': str(user), - 'API key': str(api), - } + # import ftrack hierarchy + self.import_to_ftrack(input_data) + except Exception: + raise + finally: + if auto_sync_state: + self.auto_sync_on(project) - item_lens = [len(key) + 1 for key in ftrack_cred] - justify_len = max(*item_lens) - for key, value in ftrack_cred.items(): - print('{} {}'.format((key + ':').ljust( - justify_len, ' '), value)) - return False - print( - 'Credentials Username: "{}", API key: "{}" are valid.'.format( - user, api) - ) - return True + def import_to_ftrack(self, input_data, parent=None): + # Prequery hiearchical custom attributes + hier_custom_attributes = get_pype_attr(self.session)[1] + hier_attr_by_key = { + attr["key"]: attr + for attr in hier_custom_attributes + } + # Get ftrack api module (as they are different per python version) + ftrack_api = self.context.data["ftrackPythonModule"] - # fill your own credentials - url = os.getenv("FTRACK_SERVER") - user = os.getenv("FTRACK_API_USER") - api = os.getenv("FTRACK_API_KEY") + for entity_name in input_data: + entity_data = input_data[entity_name] + entity_type = entity_data['entity_type'] + self.log.debug(entity_data) + self.log.debug(entity_type) - try: - assert validate_credentials(url, user, api), ( - "Ftrack credentials failed") - # open ftrack session - session = ftrack_api.Session( - server_url=url, - api_user=user, - api_key=api - ) - yield session - except Exception as _E: - print( - "ERROR: {}".format(_E)) - finally: - # close the session - session.close() + if entity_type.lower() == 'project': + query = 'Project where full_name is "{}"'.format(entity_name) + entity = self.session.query(query).one() + self.ft_project = entity + self.task_types = self.get_all_task_types(entity) - -class FlameLabel(QtWidgets.QLabel): - """ - Custom Qt Flame Label Widget - - For different label looks set label_type as: 'normal', 'background', or 'outline' - - To use: - - label = FlameLabel('Label Name', 'normal', window) - """ - - def __init__(self, label_name, label_type, parent_window, *args, **kwargs): - super(FlameLabel, self).__init__(*args, **kwargs) - - self.setText(label_name) - self.setParent(parent_window) - self.setMinimumSize(130, 28) - self.setMaximumHeight(28) - self.setFocusPolicy(QtCore.Qt.NoFocus) - - # Set label stylesheet based on label_type - - if label_type == 'normal': - self.setStyleSheet('QLabel {color: #9a9a9a; border-bottom: 1px inset #282828; font: 14px "Discreet"}' - 'QLabel:disabled {color: #6a6a6a}') - elif label_type == 'background': - self.setAlignment(QtCore.Qt.AlignCenter) - self.setStyleSheet( - 'color: #9a9a9a; background-color: #393939; font: 14px "Discreet"') - elif label_type == 'outline': - self.setAlignment(QtCore.Qt.AlignCenter) - self.setStyleSheet( - 'color: #9a9a9a; background-color: #212121; border: 1px solid #404040; font: 14px "Discreet"') - - -class FlameLineEdit(QtWidgets.QLineEdit): - """ - Custom Qt Flame Line Edit Widget - - Main window should include this: window.setFocusPolicy(QtCore.Qt.StrongFocus) - - To use: - - line_edit = FlameLineEdit('Some text here', window) - """ - - def __init__(self, text, parent_window, *args, **kwargs): - super(FlameLineEdit, self).__init__(*args, **kwargs) - - self.setText(text) - self.setParent(parent_window) - self.setMinimumHeight(28) - self.setMinimumWidth(110) - self.setStyleSheet('QLineEdit {color: #9a9a9a; background-color: #373e47; selection-color: #262626; selection-background-color: #b8b1a7; font: 14px "Discreet"}' - 'QLineEdit:focus {background-color: #474e58}' - 'QLineEdit:disabled {color: #6a6a6a; background-color: #373737}') - - -class FlameTreeWidget(QtWidgets.QTreeWidget): - """ - Custom Qt Flame Tree Widget - - To use: - - tree_headers = ['Header1', 'Header2', 'Header3', 'Header4'] - tree = FlameTreeWidget(tree_headers, window) - """ - - def __init__(self, tree_headers, parent_window, *args, **kwargs): - super(FlameTreeWidget, self).__init__(*args, **kwargs) - - self.setMinimumWidth(1000) - self.setMinimumHeight(300) - self.setSortingEnabled(True) - self.sortByColumn(0, QtCore.Qt.AscendingOrder) - self.setAlternatingRowColors(True) - self.setFocusPolicy(QtCore.Qt.NoFocus) - self.setStyleSheet('QTreeWidget {color: #9a9a9a; background-color: #2a2a2a; alternate-background-color: #2d2d2d; font: 14px "Discreet"}' - 'QTreeWidget::item:selected {color: #d9d9d9; background-color: #474747; border: 1px solid #111111}' - 'QHeaderView {color: #9a9a9a; background-color: #393939; font: 14px "Discreet"}' - 'QTreeWidget::item:selected {selection-background-color: #111111}' - 'QMenu {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' - 'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}') - self.verticalScrollBar().setStyleSheet('color: #818181') - self.horizontalScrollBar().setStyleSheet('color: #818181') - self.setHeaderLabels(tree_headers) - - -class FlameButton(QtWidgets.QPushButton): - """ - Custom Qt Flame Button Widget - - To use: - - button = FlameButton('Button Name', do_this_when_pressed, window) - """ - - def __init__(self, button_name, do_when_pressed, parent_window, *args, **kwargs): - super(FlameButton, self).__init__(*args, **kwargs) - - self.setText(button_name) - self.setParent(parent_window) - self.setMinimumSize(QtCore.QSize(110, 28)) - self.setMaximumSize(QtCore.QSize(110, 28)) - self.setFocusPolicy(QtCore.Qt.NoFocus) - self.clicked.connect(do_when_pressed) - self.setStyleSheet('QPushButton {color: #9a9a9a; background-color: #424142; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}' - 'QPushButton:pressed {color: #d9d9d9; background-color: #4f4f4f; border-top: 1px inset #666666; font: italic}' - 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}') - - -def main_window(selection): - def timecode_to_frames(timecode, framerate): - - def _seconds(value): - if isinstance(value, str): - _zip_ft = zip((3600, 60, 1, 1 / framerate), value.split(':')) - return sum(f * float(t) for f, t in _zip_ft) - elif isinstance(value, (int, float)): - return value / framerate - return 0 - - def _frames(seconds): - return seconds * framerate - - def timecode_to_frames(_timecode, start=None): - return _frames(_seconds(_timecode) - _seconds(start)) - - if '+' in timecode: - timecode = timecode.replace('+', ':') - elif '#' in timecode: - timecode = timecode.replace('#', ':') - - frames = int(round(timecode_to_frames(timecode, start='00:00:00:00'))) - - return frames - - def timeline_info(selection): - import flame - - # identificar as informacoes dos segmentos na timeline - for sequence in selection: - frame_rate = float(str(sequence.frame_rate)[:-4]) - for ver in sequence.versions: - for tracks in ver.tracks: - for segment in tracks.segments: - print(segment.type) - # get clip frame duration - record_duration = str(segment.record_duration)[1:-1] - clip_duration = timecode_to_frames( - record_duration, frame_rate) - - # populate shot source metadata - shot_description = "" - for attr in ["tape_name", "source_name", "head", - "tail", "file_path"]: - if not hasattr(segment, attr): - continue - _value = getattr(segment, attr) - _label = attr.replace("_", " ").capitalize() - row = "{}: {}\n".format(_label, _value) - shot_description += row - - # Add timeline segment to tree - QtWidgets.QTreeWidgetItem(tree, [ - str(sequence.name)[1:-1], # seq - str(segment.name)[1:-1], # shot - CREATE_TASK_TYPE, # task type - str(WORKFILE_START_FRAME), # start frame - str(clip_duration), # clip duration - "0:0", # handles - shot_description, # shot description - str(segment.comment)[1:-1] # task description - ]).setFlags( - QtCore.Qt.ItemIsEditable - | QtCore.Qt.ItemIsEnabled - | QtCore.Qt.ItemIsSelectable - ) - - # Select top item in tree - tree.setCurrentItem(tree.topLevelItem(0)) - - def select_all(): - - tree.selectAll() - - def send_to_ftrack(): - import flame - import six - import sys - import re - - def create_ftrack_entity(session, type, name, parent=None): - parent = parent or f_project - entity = session.create(type, { - 'name': name, - 'parent': parent - }) - try: - session.commit() - except Exception: - tp, value, tb = sys.exc_info() - session.rollback() - session._configure_locations() - six.reraise(tp, value, tb) - return entity - - def get_ftrack_entity(session, type, name, parent): - query = '{} where name is "{}" and project_id is "{}"'.format( - type, name, f_project["id"]) - - try: - entity = session.query(query).one() - except Exception: - entity = None - - # if entity doesnt exist then create one - if not entity: - entity = create_ftrack_entity( - session, - type, - name, - parent + elif self.ft_project is None or parent is None: + raise AssertionError( + "Collected items are not in right order!" ) - return entity + # try to find if entity already exists + else: + query = ( + 'TypedContext where name is "{0}" and ' + 'project_id is "{1}"' + ).format(entity_name, self.ft_project["id"]) + try: + entity = self.session.query(query).one() + except Exception: + entity = None - def generate_parents_from_template(template): - parents = [] - t_split = template.split("/") - replace_patern = re.compile(r"(\[.*\])") - type_patern = re.compile(r"\[(.*)\]") - - for t_s in t_split: - match_type = type_patern.findall(t_s) - if not match_type: - raise Exception(( - "Missing correct type flag in : {}" - "/n Example: name[Type]").format( - t_s) - ) - new_name = re.sub(replace_patern, "", t_s) - f_type = match_type.pop() - - parents.append((new_name, f_type)) - - return parents - - def get_all_task_types(): - tasks = {} - proj_template = f_project['project_schema'] - temp_task_types = proj_template['_task_type_schema']['types'] - - for type in temp_task_types: - if type['name'] not in tasks: - tasks[type['name']] = type - - return tasks - - def create_task(task_type, parent): - existing_task = [ - child for child in parent['children'] - if child.entity_type.lower() == 'task' - if child['name'].lower() in task_type.lower() + # Create entity if not exists + if entity is None: + entity = self.create_entity( + name=entity_name, + type=entity_type, + parent=parent + ) + # self.log.info('entity: {}'.format(dict(entity))) + # CUSTOM ATTRIBUTES + custom_attributes = entity_data.get('custom_attributes', []) + instances = [ + i for i in self.context if i.data['asset'] in entity['name'] ] - - if existing_task: - return existing_task - - task = session.create('Task', { - "name": task_type.lower(), - "parent": parent - }) - task_types = get_all_task_types() - task["type"] = task_types[task_type] - - return task - - - # start procedure - with maintained_ftrack_session() as session: - print("Ftrack session is: {}".format(session)) - - # get project name from flame current project - project_name = flame.project.current_project.name - - # get project from ftrack - - # ftrack project name has to be the same as flame project! - query = 'Project where full_name is "{}"'.format(project_name) - f_project = session.query(query).one() - print("Ftrack project is: {}".format(f_project)) - - # Get all selected items from treewidget - for item in tree.selectedItems(): - # solve handle start and end - handles = item.text(5) - if ":" in handles: - _s, _e = handles.split(":") - handle_start = int(_s) - handle_end = int(_e) - else: - handle_start = int(handles) - handle_end = int(handles) - - # frame ranges - frame_start = int(item.text(3)) - frame_duration = int(item.text(4)) - frame_end = frame_start + frame_duration - - # description - shot_description = item.text(6) - task_description = item.text(7) - - # other - task_type = item.text(2) - shot_name = item.text(1) - sequence_name = item.text(0) - - # populate full shot info - shot_attributes = { - "sequence": sequence_name, - "shot": shot_name, - "task": task_type - } - - # format hierarchy template - hierarchy_text = hierarchy_template.text() - hierarchy_text = hierarchy_text.format(**shot_attributes) - print(hierarchy_text) - - # solve parents - parents = generate_parents_from_template(hierarchy_text) - print(parents) - - # obtain shot parents entities - _parent = None - for _name, _type in parents: - p_entity = get_ftrack_entity( - session, - _type, - _name, - _parent + for key in custom_attributes: + hier_attr = hier_attr_by_key.get(key) + # Use simple method if key is not hierarchical + if not hier_attr: + assert (key in entity['custom_attributes']), ( + 'Missing custom attribute key: `{0}` in attrs: ' + '`{1}`'.format(key, entity['custom_attributes'].keys()) ) - print(p_entity) - _parent = p_entity - # obtain shot ftrack entity - f_s_entity = get_ftrack_entity( - session, - "Shot", - item.text(1), - _parent - ) - print("Shot entity is: {}".format(f_s_entity)) + entity['custom_attributes'][key] = custom_attributes[key] - # create custom attributtes - custom_attrs = { - "frameStart": frame_start, - "frameEnd": frame_end, - "handleStart": handle_start, - "handleEnd": handle_end - } + else: + # Use ftrack operations method to set hiearchical + # attribute value. + # - this is because there may be non hiearchical custom + # attributes with different properties + entity_key = collections.OrderedDict() + entity_key["configuration_id"] = hier_attr["id"] + entity_key["entity_id"] = entity["id"] + self.session.recorded_operations.push( + ftrack_api.operation.UpdateEntityOperation( + "ContextCustomAttributeValue", + entity_key, + "value", + ftrack_api.symbol.NOT_SET, + custom_attributes[key] + ) + ) - # update custom attributes on shot entity - for key in custom_attrs: - f_s_entity['custom_attributes'][key] = custom_attrs[key] - - task_entity = create_task(task_type, f_s_entity) - - # Create notes. - user = session.query( - "User where username is \"{}\"".format(session.api_user) - ).first() - - f_s_entity.create_note(shot_description, author=user) - - if task_description: - task_entity.create_note(task_description, user) + for instance in instances: + instance.data['ftrackEntity'] = entity try: - session.commit() + self.session.commit() except Exception: tp, value, tb = sys.exc_info() - session.rollback() - session._configure_locations() + self.session.rollback() + self.session._configure_locations() six.reraise(tp, value, tb) - # creating ui - window = QtWidgets.QWidget() - window.setMinimumSize(1500, 600) - window.setWindowTitle('Sequence Shots to Ftrack') - window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) - window.setAttribute(QtCore.Qt.WA_DeleteOnClose) - window.setStyleSheet('background-color: #313131') + # TASKS + tasks = entity_data.get('tasks', []) + existing_tasks = [] + tasks_to_create = [] + for child in entity['children']: + if child.entity_type.lower() == 'task': + existing_tasks.append(child['name'].lower()) + # existing_tasks.append(child['type']['name']) - # Center window in linux - resolution = QtWidgets.QDesktopWidget().screenGeometry() - window.move((resolution.width() / 2) - (window.frameSize().width() / 2), - (resolution.height() / 2) - (window.frameSize().height() / 2)) + for task_name in tasks: + task_type = tasks[task_name]["type"] + if task_name.lower() in existing_tasks: + print("Task {} already exists".format(task_name)) + continue + tasks_to_create.append((task_name, task_type)) - # TreeWidget - columns = { - "Sequence name": { - "columnWidth": 100, - "order": 0 - }, - "Shot name": { - "columnWidth": 100, - "order": 1 - }, - "Task type": { - "columnWidth": 100, - "order": 2 - }, - "Start frame": { - "columnWidth": 100, - "order": 3 - }, - "Clip duration": { - "columnWidth": 100, - "order": 4 - }, - "Handles": { - "columnWidth": 100, - "order": 5 - }, - "Shot description": { - "columnWidth": 300, - "order": 6 - }, - "Task description": { - "columnWidth": 300, - "order": 7 - }, - } - ordered_column_labels = columns.keys() - for _name, _value in columns.items(): - ordered_column_labels.pop(_value["order"]) - ordered_column_labels.insert(_value["order"], _name) + for task_name, task_type in tasks_to_create: + self.create_task( + name=task_name, + task_type=task_type, + parent=entity + ) + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + self.session._configure_locations() + six.reraise(tp, value, tb) - print(ordered_column_labels) + # Incoming links. + self.create_links(entity_data, entity) + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + self.session._configure_locations() + six.reraise(tp, value, tb) - tree = FlameTreeWidget(ordered_column_labels, window) + # Create notes. + user = self.session.query( + "User where username is \"{}\"".format(self.session.api_user) + ).first() + if user: + for comment in entity_data.get("comments", []): + entity.create_note(comment, user) + else: + self.log.warning( + "Was not able to query current User {}".format( + self.session.api_user + ) + ) + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + self.session._configure_locations() + six.reraise(tp, value, tb) - # Allow multiple items in tree to be selected - tree.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) + # Import children. + if 'childs' in entity_data: + self.import_to_ftrack( + entity_data['childs'], entity) - # Set tree column width - for _name, _val in columns.items(): - tree.setColumnWidth( - _val["order"], - _val["columnWidth"] - ) + def create_links(self, entity_data, entity): + # Clear existing links. + for link in entity.get("incoming_links", []): + self.session.delete(link) + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + self.session._configure_locations() + six.reraise(tp, value, tb) - # Prevent weird characters when shrinking tree columns - tree.setTextElideMode(QtCore.Qt.ElideNone) + # Create new links. + for input in entity_data.get("inputs", []): + input_id = io.find_one({"_id": input})["data"]["ftrackId"] + assetbuild = self.session.get("AssetBuild", input_id) + self.log.debug( + "Creating link from {0} to {1}".format( + assetbuild["name"], entity["name"] + ) + ) + self.session.create( + "TypedContextLink", {"from": assetbuild, "to": entity} + ) - # input fields - hierarchy_label = FlameLabel( - 'Parents template', 'normal', window) - hierarchy_template = FlameLineEdit(HIERARCHY_TEMPLATE, window) + def get_all_task_types(self, project): + tasks = {} + proj_template = project['project_schema'] + temp_task_types = proj_template['_task_type_schema']['types'] - ## Button - select_all_btn = FlameButton('Select All', select_all, window) - ftrack_send_btn = FlameButton('Send to Ftrack', send_to_ftrack, window) + for type in temp_task_types: + if type['name'] not in tasks: + tasks[type['name']] = type - ## Window Layout - gridbox = QtWidgets.QGridLayout() - gridbox.setMargin(20) - gridbox.setHorizontalSpacing(20) - gridbox.addWidget(hierarchy_label, 0, 0) - gridbox.addWidget(hierarchy_template, 0, 1, 1, 4) - gridbox.addWidget(tree, 1, 0, 5, 5) - gridbox.addWidget(select_all_btn, 6, 3) - gridbox.addWidget(ftrack_send_btn, 6, 4) + return tasks - window.setLayout(gridbox) - window.show() + def create_task(self, name, task_type, parent): + task = self.session.create('Task', { + 'name': name, + 'parent': parent + }) + # TODO not secured!!! - check if task_type exists + self.log.info(task_type) + self.log.info(self.task_types) + task['type'] = self.task_types[task_type] - timeline_info(selection) + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + self.session._configure_locations() + six.reraise(tp, value, tb) - return window + return task + def create_entity(self, name, type, parent): + entity = self.session.create(type, { + 'name': name, + 'parent': parent + }) + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + self.session._configure_locations() + six.reraise(tp, value, tb) -def scope_sequence(selection): - import flame - return any(isinstance(item, flame.PySequence) for item in selection) + return entity + def auto_sync_off(self, project): + project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = False -def get_media_panel_custom_ui_actions(): - return [ - { - "name": "OpenPype: Ftrack", - "actions": [ - { - "name": "Create Shots", - "isVisible": scope_sequence, - "execute": main_window - } - ] - } + self.log.info("Ftrack autosync swithed off") - ] + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + self.session._configure_locations() + six.reraise(tp, value, tb) + + def auto_sync_on(self, project): + + project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = True + + self.log.info("Ftrack autosync swithed on") + + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + self.session._configure_locations() + six.reraise(tp, value, tb) From bb0a323be6f789d233a71cb07db257ee4e4649b6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 1 Nov 2021 14:03:42 +0100 Subject: [PATCH 014/307] add module file into folder --- .../{ => openpype_flame_to_ftrack}/openpype_flame_to_ftrack.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/hosts/flame/utility_scripts/{ => openpype_flame_to_ftrack}/openpype_flame_to_ftrack.py (100%) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack.py rename to openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py From 4c574478bd3bc55366276fc381a6c253ac2e38fa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 1 Nov 2021 17:03:41 +0100 Subject: [PATCH 015/307] updating layout and adding global input fields --- .../openpype_flame_to_ftrack.py | 177 +++++++++++------- 1 file changed, 111 insertions(+), 66 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index ba57023edc..402f3918c3 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -1,4 +1,5 @@ from __future__ import print_function +import os from PySide2 import QtWidgets, QtCore from pprint import pformat from contextlib import contextmanager @@ -14,6 +15,9 @@ FTRACK_API_KEY = None FTRACK_API_USER = None FTRACK_SERVER = None +SCRIPT_DIR = os.path.dirname(__file__) +EXPORT_PRESETS_DIR = os.path.join(SCRIPT_DIR, "export_preset") + @contextmanager def maintained_ftrack_session(): @@ -84,6 +88,40 @@ def maintained_ftrack_session(): session.close() +@contextmanager +def make_temp_dir(): + import tempfile + import shutil + + try: + dirpath = tempfile.mkdtemp() + + yield dirpath + + except IOError as _error: + raise IOError( + "Not able to create temp dir file: {}".format( + _error + ) + ) + + finally: + print(dirpath) + # shutil.rmtree(dirpath) + + +def get_all_task_types(project_entity): + tasks = {} + proj_template = project_entity['project_schema'] + temp_task_types = proj_template['_task_type_schema']['types'] + + for type in temp_task_types: + if type['name'] not in tasks: + tasks[type['name']] = type + + return tasks + + class FlameLabel(QtWidgets.QLabel): """ Custom Qt Flame Label Widget @@ -251,10 +289,7 @@ def main_window(selection): QtWidgets.QTreeWidgetItem(tree, [ str(sequence.name)[1:-1], # seq str(segment.name)[1:-1], # shot - CREATE_TASK_TYPE, # task type - str(WORKFILE_START_FRAME), # start frame str(clip_duration), # clip duration - "0:0", # handles shot_description, # shot description str(segment.comment)[1:-1] # task description ]).setFlags( @@ -332,17 +367,6 @@ def main_window(selection): return parents - def get_all_task_types(): - tasks = {} - proj_template = f_project['project_schema'] - temp_task_types = proj_template['_task_type_schema']['types'] - - for type in temp_task_types: - if type['name'] not in tasks: - tasks[type['name']] = type - - return tasks - def create_task(task_type, parent): existing_task = [ child for child in parent['children'] @@ -357,13 +381,23 @@ def main_window(selection): "name": task_type.lower(), "parent": parent }) - task_types = get_all_task_types() + task_types = get_all_task_types(f_project) task["type"] = task_types[task_type] return task + def export_thumbnail(sequence, tempdir_path): + export_preset = os.path.join( + EXPORT_PRESETS_DIR, + "openpype_seg_thumbnails_jpg.xml" + ) + poster_frame_exporter = flame.PyExporter() + poster_frame_exporter.foreground = True + poster_frame_exporter.export(sequence, export_preset, tempdir_path) + # start procedure - with maintained_ftrack_session() as session: + with maintained_ftrack_session() as session, make_temp_dir() as tempdir_path: + print("tempdir_path: {}".format(tempdir_path)) print("Ftrack session is: {}".format(session)) # get project name from flame current project @@ -375,31 +409,30 @@ def main_window(selection): f_project = session.query(query).one() print("Ftrack project is: {}".format(f_project)) + # get hanldes from gui input + handles = handles_input.text() + handle_start = int(handles) + handle_end = int(handles) + + # get frame start from gui input + frame_start = int(start_frame_input.text()) + + # get task type from gui input + task_type = task_type_input.text() + # Get all selected items from treewidget for item in tree.selectedItems(): - # solve handle start and end - handles = item.text(5) - if ":" in handles: - _s, _e = handles.split(":") - handle_start = int(_s) - handle_end = int(_e) - else: - handle_start = int(handles) - handle_end = int(handles) - # frame ranges - frame_start = int(item.text(3)) - frame_duration = int(item.text(4)) + frame_duration = int(item.text(2)) frame_end = frame_start + frame_duration # description - shot_description = item.text(6) - task_description = item.text(7) + shot_description = item.text(3) + task_description = item.text(4) # other - task_type = item.text(2) - shot_name = item.text(1) sequence_name = item.text(0) + shot_name = item.text(1) # populate full shot info shot_attributes = { @@ -409,7 +442,7 @@ def main_window(selection): } # format hierarchy template - hierarchy_text = hierarchy_template.text() + hierarchy_text = hierarchy_template_input.text() hierarchy_text = hierarchy_text.format(**shot_attributes) print(hierarchy_text) @@ -486,36 +519,24 @@ def main_window(selection): # TreeWidget columns = { "Sequence name": { - "columnWidth": 100, + "columnWidth": 200, "order": 0 }, "Shot name": { - "columnWidth": 100, + "columnWidth": 200, "order": 1 }, - "Task type": { - "columnWidth": 100, - "order": 2 - }, - "Start frame": { - "columnWidth": 100, - "order": 3 - }, "Clip duration": { "columnWidth": 100, - "order": 4 - }, - "Handles": { - "columnWidth": 100, - "order": 5 + "order": 2 }, "Shot description": { - "columnWidth": 300, - "order": 6 + "columnWidth": 500, + "order": 3 }, "Task description": { - "columnWidth": 300, - "order": 7 + "columnWidth": 500, + "order": 4 }, } ordered_column_labels = columns.keys() @@ -543,30 +564,54 @@ def main_window(selection): # input fields hierarchy_label = FlameLabel( 'Parents template', 'normal', window) - hierarchy_template = FlameLineEdit(HIERARCHY_TEMPLATE, window) + hierarchy_template_input = FlameLineEdit(HIERARCHY_TEMPLATE, window) - # input fields start_frame_label = FlameLabel( 'Workfile start frame', 'normal', window) - start_frame = FlameLineEdit(WORKFILE_START_FRAME, window) + start_frame_input = FlameLineEdit(str(WORKFILE_START_FRAME), window) + + handles_label = FlameLabel( + 'Shot handles', 'normal', window) + handles_input = FlameLineEdit(str(5), window) + + task_type_label = FlameLabel( + 'Create Task (type)', 'normal', window) + task_type_input = FlameLineEdit(CREATE_TASK_TYPE, window) ## Button select_all_btn = FlameButton('Select All', select_all, window) ftrack_send_btn = FlameButton('Send to Ftrack', send_to_ftrack, window) ## Window Layout - gridbox = QtWidgets.QGridLayout() - gridbox.setMargin(20) - gridbox.setHorizontalSpacing(20) - gridbox.addWidget(hierarchy_label, 0, 0) - gridbox.addWidget(hierarchy_template, 0, 1, 1, 2) - gridbox.addWidget(start_frame_label, 0, 3) - gridbox.addWidget(start_frame, 0, 3, 1, 1) - gridbox.addWidget(tree, 1, 0, 5, 5) - gridbox.addWidget(select_all_btn, 6, 3) - gridbox.addWidget(ftrack_send_btn, 6, 4) + prop_layout = QtWidgets.QGridLayout() + prop_layout.setHorizontalSpacing(30) + prop_layout.addWidget(hierarchy_label, 0, 0) + prop_layout.addWidget(hierarchy_template_input, 0, 1) + prop_layout.addWidget(start_frame_label, 1, 0) + prop_layout.addWidget(start_frame_input, 1, 1) + prop_layout.addWidget(handles_label, 2, 0) + prop_layout.addWidget(handles_input, 2, 1) + prop_layout.addWidget(task_type_label, 3, 0) + prop_layout.addWidget(task_type_input, 3, 1) - window.setLayout(gridbox) + tree_layout = QtWidgets.QGridLayout() + tree_layout.addWidget(tree, 1, 0) + + hbox = QtWidgets.QHBoxLayout() + hbox.addWidget(select_all_btn) + hbox.addWidget(ftrack_send_btn) + + main_frame = QtWidgets.QVBoxLayout() + main_frame.setMargin(20) + main_frame.addStretch(5) + main_frame.addLayout(prop_layout) + main_frame.addStretch(10) + main_frame.addLayout(tree_layout) + main_frame.addStretch(5) + main_frame.addLayout(hbox) + + + window.setLayout(main_frame) window.show() timeline_info(selection) From db875837e72afc0595c25adb8d514011fbafe8a6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 1 Nov 2021 17:27:29 +0100 Subject: [PATCH 016/307] ftrack task types populated to gui offer --- .../openpype_flame_to_ftrack.py | 165 +++++++++++------- 1 file changed, 103 insertions(+), 62 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index 402f3918c3..8247dd0551 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -18,7 +18,6 @@ FTRACK_SERVER = None SCRIPT_DIR = os.path.dirname(__file__) EXPORT_PRESETS_DIR = os.path.join(SCRIPT_DIR, "export_preset") - @contextmanager def maintained_ftrack_session(): import ftrack_api @@ -233,7 +232,52 @@ class FlameButton(QtWidgets.QPushButton): 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}') +class FlamePushButtonMenu(QtWidgets.QPushButton): + """ + Custom Qt Flame Menu Push Button Widget + + To use: + + push_button_menu_options = ['Item 1', 'Item 2', 'Item 3', 'Item 4'] + menu_push_button = FlamePushButtonMenu('push_button_name', push_button_menu_options, window) + + or + + push_button_menu_options = ['Item 1', 'Item 2', 'Item 3', 'Item 4'] + menu_push_button = FlamePushButtonMenu(push_button_menu_options[0], push_button_menu_options, window) + """ + + def __init__(self, button_name, menu_options, parent_window, *args, **kwargs): + super(FlamePushButtonMenu, self).__init__(*args, **kwargs) + from functools import partial + + self.setText(button_name) + self.setParent(parent_window) + self.setMinimumHeight(28) + self.setMinimumWidth(110) + self.setFocusPolicy(QtCore.Qt.NoFocus) + self.setStyleSheet('QPushButton {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' + 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}') + + def create_menu(option): + self.setText(option) + + pushbutton_menu = QtWidgets.QMenu(parent_window) + pushbutton_menu.setFocusPolicy(QtCore.Qt.NoFocus) + pushbutton_menu.setStyleSheet('QMenu {color: #9a9a9a; background-color:#24303d; font: 14px "Discreet"}' + 'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}') + for option in menu_options: + pushbutton_menu.addAction(option, partial(create_menu, option)) + + self.setMenu(pushbutton_menu) + + def main_window(selection): + import flame + import six + import sys + import re + def timecode_to_frames(timecode, framerate): def _seconds(value): @@ -306,13 +350,8 @@ def main_window(selection): tree.selectAll() def send_to_ftrack(): - import flame - import six - import sys - import re - def create_ftrack_entity(session, type, name, parent=None): - parent = parent or f_project + parent = parent or F_PROJ_ENTITY entity = session.create(type, { 'name': name, 'parent': parent @@ -328,7 +367,7 @@ def main_window(selection): def get_ftrack_entity(session, type, name, parent): query = '{} where name is "{}" and project_id is "{}"'.format( - type, name, f_project["id"]) + type, name, F_PROJ_ENTITY["id"]) try: entity = session.query(query).one() @@ -381,8 +420,7 @@ def main_window(selection): "name": task_type.lower(), "parent": parent }) - task_types = get_all_task_types(f_project) - task["type"] = task_types[task_type] + task["type"] = F_PROJ_TASK_TYPES[task_type] return task @@ -400,15 +438,6 @@ def main_window(selection): print("tempdir_path: {}".format(tempdir_path)) print("Ftrack session is: {}".format(session)) - # get project name from flame current project - project_name = flame.project.current_project.name - - # get project from ftrack - - # ftrack project name has to be the same as flame project! - query = 'Project where full_name is "{}"'.format(project_name) - f_project = session.query(query).one() - print("Ftrack project is: {}".format(f_project)) - # get hanldes from gui input handles = handles_input.text() handle_start = int(handles) @@ -561,60 +590,72 @@ def main_window(selection): # Prevent weird characters when shrinking tree columns tree.setTextElideMode(QtCore.Qt.ElideNone) - # input fields - hierarchy_label = FlameLabel( - 'Parents template', 'normal', window) - hierarchy_template_input = FlameLineEdit(HIERARCHY_TEMPLATE, window) + with maintained_ftrack_session() as _session: + # input fields + hierarchy_label = FlameLabel( + 'Parents template', 'normal', window) + hierarchy_template_input = FlameLineEdit(HIERARCHY_TEMPLATE, window) - start_frame_label = FlameLabel( - 'Workfile start frame', 'normal', window) - start_frame_input = FlameLineEdit(str(WORKFILE_START_FRAME), window) + start_frame_label = FlameLabel( + 'Workfile start frame', 'normal', window) + start_frame_input = FlameLineEdit(str(WORKFILE_START_FRAME), window) - handles_label = FlameLabel( - 'Shot handles', 'normal', window) - handles_input = FlameLineEdit(str(5), window) + handles_label = FlameLabel( + 'Shot handles', 'normal', window) + handles_input = FlameLineEdit(str(5), window) - task_type_label = FlameLabel( - 'Create Task (type)', 'normal', window) - task_type_input = FlameLineEdit(CREATE_TASK_TYPE, window) + # get project name from flame current project + project_name = flame.project.current_project.name - ## Button - select_all_btn = FlameButton('Select All', select_all, window) - ftrack_send_btn = FlameButton('Send to Ftrack', send_to_ftrack, window) + # get project from ftrack - + # ftrack project name has to be the same as flame project! + query = 'Project where full_name is "{}"'.format(project_name) - ## Window Layout - prop_layout = QtWidgets.QGridLayout() - prop_layout.setHorizontalSpacing(30) - prop_layout.addWidget(hierarchy_label, 0, 0) - prop_layout.addWidget(hierarchy_template_input, 0, 1) - prop_layout.addWidget(start_frame_label, 1, 0) - prop_layout.addWidget(start_frame_input, 1, 1) - prop_layout.addWidget(handles_label, 2, 0) - prop_layout.addWidget(handles_input, 2, 1) - prop_layout.addWidget(task_type_label, 3, 0) - prop_layout.addWidget(task_type_input, 3, 1) + # globally used variables + F_PROJ_ENTITY = _session.query(query).one() + F_PROJ_TASK_TYPES = get_all_task_types(F_PROJ_ENTITY) - tree_layout = QtWidgets.QGridLayout() - tree_layout.addWidget(tree, 1, 0) + task_type_label = FlameLabel( + 'Create Task (type)', 'normal', window) + task_type_input = FlamePushButtonMenu( + CREATE_TASK_TYPE, F_PROJ_TASK_TYPES, window) - hbox = QtWidgets.QHBoxLayout() - hbox.addWidget(select_all_btn) - hbox.addWidget(ftrack_send_btn) + ## Button + select_all_btn = FlameButton('Select All', select_all, window) + ftrack_send_btn = FlameButton('Send to Ftrack', send_to_ftrack, window) - main_frame = QtWidgets.QVBoxLayout() - main_frame.setMargin(20) - main_frame.addStretch(5) - main_frame.addLayout(prop_layout) - main_frame.addStretch(10) - main_frame.addLayout(tree_layout) - main_frame.addStretch(5) - main_frame.addLayout(hbox) + ## Window Layout + prop_layout = QtWidgets.QGridLayout() + prop_layout.setHorizontalSpacing(30) + prop_layout.addWidget(hierarchy_label, 0, 0) + prop_layout.addWidget(hierarchy_template_input, 0, 1) + prop_layout.addWidget(start_frame_label, 1, 0) + prop_layout.addWidget(start_frame_input, 1, 1) + prop_layout.addWidget(handles_label, 2, 0) + prop_layout.addWidget(handles_input, 2, 1) + prop_layout.addWidget(task_type_label, 3, 0) + prop_layout.addWidget(task_type_input, 3, 1) + tree_layout = QtWidgets.QGridLayout() + tree_layout.addWidget(tree, 1, 0) - window.setLayout(main_frame) - window.show() + hbox = QtWidgets.QHBoxLayout() + hbox.addWidget(select_all_btn) + hbox.addWidget(ftrack_send_btn) - timeline_info(selection) + main_frame = QtWidgets.QVBoxLayout() + main_frame.setMargin(20) + main_frame.addStretch(5) + main_frame.addLayout(prop_layout) + main_frame.addStretch(10) + main_frame.addLayout(tree_layout) + main_frame.addStretch(5) + main_frame.addLayout(hbox) + + window.setLayout(main_frame) + window.show() + + timeline_info(selection) return window From 886bc81b4925db247f37e4428f24b991e6a85a86 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 2 Nov 2021 16:01:30 +0100 Subject: [PATCH 017/307] adding config settings.ini for retreating used data --- .../openpype_flame_to_ftrack.py | 173 +++++++++++++----- 1 file changed, 131 insertions(+), 42 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index 8247dd0551..4b12cc651c 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -3,13 +3,11 @@ import os from PySide2 import QtWidgets, QtCore from pprint import pformat from contextlib import contextmanager +from xml.etree import ElementTree as ET +import ConfigParser as CP +import io -# Constants -WORKFILE_START_FRAME = 1001 -HIERARCHY_TEMPLATE = "shots[Folder]/{sequence}[Sequence]" -CREATE_TASK_TYPE = "Compositing" - # Fill following constants or set them via environment variable FTRACK_API_KEY = None FTRACK_API_USER = None @@ -17,6 +15,8 @@ FTRACK_SERVER = None SCRIPT_DIR = os.path.dirname(__file__) EXPORT_PRESETS_DIR = os.path.join(SCRIPT_DIR, "export_preset") +CONFIG_DIR = os.path.join(os.path.expanduser("~/.openpype"), "config") + @contextmanager def maintained_ftrack_session(): @@ -98,15 +98,82 @@ def make_temp_dir(): yield dirpath except IOError as _error: - raise IOError( - "Not able to create temp dir file: {}".format( - _error - ) - ) + raise IOError("Not able to create temp dir file: {}".format(_error)) finally: print(dirpath) - # shutil.rmtree(dirpath) + shutil.rmtree(dirpath) + + +@contextmanager +def get_config(section=None): + cfg_file_path = os.path.join(CONFIG_DIR, "settings.ini") + + # create config dir + if not os.path.exists(CONFIG_DIR): + print("making dirs at: `{}`".format(CONFIG_DIR)) + os.makedirs(CONFIG_DIR, mode=0o777) + + # write default data to settings.ini + if not os.path.exists(cfg_file_path): + default_cfg = cfg_default() + config = CP.RawConfigParser() + config.readfp(io.BytesIO(default_cfg)) + with open(cfg_file_path, 'wb') as cfg_file: + config.write(cfg_file) + + try: + config = CP.RawConfigParser() + config.read(cfg_file_path) + if section: + _cfg_data = { + k: v + for s in config.sections() + for k, v in config.items(s) + if s == section + } + else: + _cfg_data = {s: dict(config.items(s)) for s in config.sections()} + + yield _cfg_data + + except IOError as _error: + raise IOError('Not able to read settings.ini file: {}'.format(_error)) + + finally: + pass + + +def set_config(cfg_data, section=None): + cfg_file_path = os.path.join(CONFIG_DIR, "settings.ini") + + config = CP.RawConfigParser() + config.read(cfg_file_path) + + try: + if not section: + for section in cfg_data: + for key, value in cfg_data[section].items(): + config.set(section, key, value) + else: + for key, value in cfg_data.items(): + config.set(section, key, value) + + with open(cfg_file_path, 'wb') as cfg_file: + config.write(cfg_file) + + except IOError as _error: + raise IOError('Not able to write settings.ini file: {}'.format(_error)) + + +def cfg_default(): + return """ +[main] +workfile_start_frame = 1001 +shot_handles = 0 +hierarchy_template = shots[Folder]/{sequence}[Sequence] +create_task_type = Compositing +""" def get_all_task_types(project_entity): @@ -121,6 +188,18 @@ def get_all_task_types(project_entity): return tasks +def export_thumbnail(sequence, tempdir_path): + import flame + export_preset = os.path.join( + EXPORT_PRESETS_DIR, + "openpype_seg_thumbnails_jpg.xml" + ) + print(export_preset) + poster_frame_exporter = flame.PyExporter() + poster_frame_exporter.foreground = True + poster_frame_exporter.export(sequence, export_preset, tempdir_path) + + class FlameLabel(QtWidgets.QLabel): """ Custom Qt Flame Label Widget @@ -424,30 +503,39 @@ def main_window(selection): return task - def export_thumbnail(sequence, tempdir_path): - export_preset = os.path.join( - EXPORT_PRESETS_DIR, - "openpype_seg_thumbnails_jpg.xml" - ) - poster_frame_exporter = flame.PyExporter() - poster_frame_exporter.foreground = True - poster_frame_exporter.export(sequence, export_preset, tempdir_path) - # start procedure - with maintained_ftrack_session() as session, make_temp_dir() as tempdir_path: + _cfg_data_back = {} + # get hierarchy from gui input + hierarchy_text = hierarchy_template_input.text() + + # get hanldes from gui input + handles = handles_input.text() + + # get frame start from gui input + frame_start = int(start_frame_input.text()) + + # get task type from gui input + task_type = task_type_input.text() + + _cfg_data_back = { + "workfile_start_frame": str(frame_start), + "shot_handles": handles, + "hierarchy_template": hierarchy_text, + "create_task_type": task_type + } + ######################################################### + print(pformat(_cfg_data_back)) + # add cfg data back to settings.ini + set_config(_cfg_data_back, "main") + + with maintained_ftrack_session() as session, \ + make_temp_dir() as tempdir_path: print("tempdir_path: {}".format(tempdir_path)) print("Ftrack session is: {}".format(session)) - # get hanldes from gui input - handles = handles_input.text() - handle_start = int(handles) - handle_end = int(handles) - - # get frame start from gui input - frame_start = int(start_frame_input.text()) - - # get task type from gui input - task_type = task_type_input.text() + for seq in selection: + export_thumbnail(seq, tempdir_path) + break # Get all selected items from treewidget for item in tree.selectedItems(): @@ -471,12 +559,11 @@ def main_window(selection): } # format hierarchy template - hierarchy_text = hierarchy_template_input.text() - hierarchy_text = hierarchy_text.format(**shot_attributes) - print(hierarchy_text) + _hierarchy_text = hierarchy_text.format(**shot_attributes) + print(_hierarchy_text) # solve parents - parents = generate_parents_from_template(hierarchy_text) + parents = generate_parents_from_template(_hierarchy_text) print(parents) # obtain shot parents entities @@ -504,8 +591,8 @@ def main_window(selection): custom_attrs = { "frameStart": frame_start, "frameEnd": frame_end, - "handleStart": handle_start, - "handleEnd": handle_end + "handleStart": int(handles), + "handleEnd": int(handles) } # update custom attributes on shot entity @@ -590,19 +677,21 @@ def main_window(selection): # Prevent weird characters when shrinking tree columns tree.setTextElideMode(QtCore.Qt.ElideNone) - with maintained_ftrack_session() as _session: + with maintained_ftrack_session() as _session, get_config("main") as cfg_d: # input fields hierarchy_label = FlameLabel( 'Parents template', 'normal', window) - hierarchy_template_input = FlameLineEdit(HIERARCHY_TEMPLATE, window) + hierarchy_template_input = FlameLineEdit( + cfg_d["hierarchy_template"], window) start_frame_label = FlameLabel( 'Workfile start frame', 'normal', window) - start_frame_input = FlameLineEdit(str(WORKFILE_START_FRAME), window) + start_frame_input = FlameLineEdit( + cfg_d["workfile_start_frame"], window) handles_label = FlameLabel( 'Shot handles', 'normal', window) - handles_input = FlameLineEdit(str(5), window) + handles_input = FlameLineEdit(cfg_d["shot_handles"], window) # get project name from flame current project project_name = flame.project.current_project.name @@ -618,7 +707,7 @@ def main_window(selection): task_type_label = FlameLabel( 'Create Task (type)', 'normal', window) task_type_input = FlamePushButtonMenu( - CREATE_TASK_TYPE, F_PROJ_TASK_TYPES, window) + cfg_d["create_task_type"], F_PROJ_TASK_TYPES, window) ## Button select_all_btn = FlameButton('Select All', select_all, window) From 94c343e671e6ee55eb7338f789d2d4915cf0c93a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 2 Nov 2021 16:01:49 +0100 Subject: [PATCH 018/307] removing codec token from mov preset --- .../export_preset/openpype_seg_video_h264.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml index 64447b76e8..3ca185b8b4 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml @@ -27,7 +27,7 @@ QuickTime - <segment name>_<video codec> + <segment name> 0 PCS_709 None From 0948bf89e2d1298e854128f5934eb18693dcf110 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 2 Nov 2021 17:26:26 +0100 Subject: [PATCH 019/307] adding `source resolution` toggle --- .../openpype_flame_to_ftrack.py | 96 ++++++++++++++----- 1 file changed, 71 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index 4b12cc651c..acbd87d91b 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -173,6 +173,7 @@ workfile_start_frame = 1001 shot_handles = 0 hierarchy_template = shots[Folder]/{sequence}[Sequence] create_task_type = Compositing +source_resolution = 0 """ @@ -277,12 +278,14 @@ class FlameTreeWidget(QtWidgets.QTreeWidget): self.sortByColumn(0, QtCore.Qt.AscendingOrder) self.setAlternatingRowColors(True) self.setFocusPolicy(QtCore.Qt.NoFocus) - self.setStyleSheet('QTreeWidget {color: #9a9a9a; background-color: #2a2a2a; alternate-background-color: #2d2d2d; font: 14px "Discreet"}' - 'QTreeWidget::item:selected {color: #d9d9d9; background-color: #474747; border: 1px solid #111111}' - 'QHeaderView {color: #9a9a9a; background-color: #393939; font: 14px "Discreet"}' - 'QTreeWidget::item:selected {selection-background-color: #111111}' - 'QMenu {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' - 'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}') + self.setStyleSheet( + 'QTreeWidget {color: #9a9a9a; background-color: #2a2a2a; alternate-background-color: #2d2d2d; font: 14px "Discreet"}' + 'QTreeWidget::item:selected {color: #d9d9d9; background-color: #474747; border: 1px solid #111111}' + 'QHeaderView {color: #9a9a9a; background-color: #393939; font: 14px "Discreet"}' + 'QTreeWidget::item:selected {selection-background-color: #111111}' + 'QMenu {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' + 'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}' + ) self.verticalScrollBar().setStyleSheet('color: #818181') self.horizontalScrollBar().setStyleSheet('color: #818181') self.setHeaderLabels(tree_headers) @@ -311,6 +314,31 @@ class FlameButton(QtWidgets.QPushButton): 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}') +class FlamePushButton(QtWidgets.QPushButton): + """ + Custom Qt Flame Push Button Widget + + To use: + + pushbutton = FlamePushButton(' Button Name', True_or_False, window) + """ + + def __init__(self, button_name, button_checked, parent_window, *args, **kwargs): + super(FlamePushButton, self).__init__(*args, **kwargs) + + self.setText(button_name) + self.setParent(parent_window) + self.setCheckable(True) + self.setChecked(button_checked) + self.setMinimumSize(155, 28) + self.setMaximumSize(155, 28) + self.setFocusPolicy(QtCore.Qt.NoFocus) + self.setStyleSheet('QPushButton {color: #9a9a9a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #424142, stop: .94 #2e3b48); text-align: left; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}' + 'QPushButton:checked {color: #d9d9d9; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #4f4f4f, stop: .94 #5a7fb4); font: italic; border: 1px inset black; border-bottom: 1px inset #404040; border-right: 1px inset #404040}' + 'QPushButton:disabled {color: #6a6a6a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #383838, stop: .94 #353535); font: light; border-top: 1px solid #575757; border-bottom: 1px solid #242424; border-right: 1px solid #353535; border-left: 1px solid #353535}' + 'QToolTip {color: black; background-color: #ffffde; border: black solid 1px}') + + class FlamePushButtonMenu(QtWidgets.QPushButton): """ Custom Qt Flame Menu Push Button Widget @@ -521,10 +549,12 @@ def main_window(selection): "workfile_start_frame": str(frame_start), "shot_handles": handles, "hierarchy_template": hierarchy_text, - "create_task_type": task_type + "create_task_type": task_type, + "source_resolution": ( + "1" if source_resolution_btn.isChecked() else "0") } ######################################################### - print(pformat(_cfg_data_back)) + # add cfg data back to settings.ini set_config(_cfg_data_back, "main") @@ -693,6 +723,12 @@ def main_window(selection): 'Shot handles', 'normal', window) handles_input = FlameLineEdit(cfg_d["shot_handles"], window) + source_resolution_btn = FlamePushButton( + 'Source resolutuion', bool(int(cfg_d["source_resolution"])), + window + ) + + # get project name from flame current project project_name = flame.project.current_project.name @@ -713,35 +749,45 @@ def main_window(selection): select_all_btn = FlameButton('Select All', select_all, window) ftrack_send_btn = FlameButton('Send to Ftrack', send_to_ftrack, window) - ## Window Layout - prop_layout = QtWidgets.QGridLayout() - prop_layout.setHorizontalSpacing(30) - prop_layout.addWidget(hierarchy_label, 0, 0) - prop_layout.addWidget(hierarchy_template_input, 0, 1) - prop_layout.addWidget(start_frame_label, 1, 0) - prop_layout.addWidget(start_frame_input, 1, 1) - prop_layout.addWidget(handles_label, 2, 0) - prop_layout.addWidget(handles_input, 2, 1) - prop_layout.addWidget(task_type_label, 3, 0) - prop_layout.addWidget(task_type_input, 3, 1) + ## left props + prop_layout_l = QtWidgets.QGridLayout() + prop_layout_l.setHorizontalSpacing(30) + prop_layout_l.addWidget(hierarchy_label, 0, 0) + prop_layout_l.addWidget(hierarchy_template_input, 0, 1) + prop_layout_l.addWidget(start_frame_label, 1, 0) + prop_layout_l.addWidget(start_frame_input, 1, 1) + prop_layout_l.addWidget(handles_label, 2, 0) + prop_layout_l.addWidget(handles_input, 2, 1) + prop_layout_l.addWidget(task_type_label, 3, 0) + prop_layout_l.addWidget(task_type_input, 3, 1) - tree_layout = QtWidgets.QGridLayout() - tree_layout.addWidget(tree, 1, 0) + # right props + prop_widget_r = QtWidgets.QWidget(window) + prop_layout_r = QtWidgets.QGridLayout(prop_widget_r) + prop_layout_r.setHorizontalSpacing(30) + prop_layout_r.setAlignment( + QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + prop_layout_r.setContentsMargins(0, 0, 0, 0) + prop_layout_r.addWidget(source_resolution_btn, 0, 0) + + prop_main_layout = QtWidgets.QHBoxLayout() + prop_main_layout.addLayout(prop_layout_l, 1) + prop_main_layout.addSpacing(20) + prop_main_layout.addWidget(prop_widget_r, 1) hbox = QtWidgets.QHBoxLayout() hbox.addWidget(select_all_btn) hbox.addWidget(ftrack_send_btn) - main_frame = QtWidgets.QVBoxLayout() + main_frame = QtWidgets.QVBoxLayout(window) main_frame.setMargin(20) main_frame.addStretch(5) - main_frame.addLayout(prop_layout) + main_frame.addLayout(prop_main_layout) main_frame.addStretch(10) - main_frame.addLayout(tree_layout) + main_frame.addWidget(tree) main_frame.addStretch(5) main_frame.addLayout(hbox) - window.setLayout(main_frame) window.show() timeline_info(selection) From fd435043c42a9e7d571feb4db1dc6246fd47a5c5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 2 Nov 2021 18:16:59 +0100 Subject: [PATCH 020/307] adding resolution and fps attributes --- .../openpype_flame_to_ftrack.py | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index acbd87d91b..54455c7139 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -419,7 +419,7 @@ def main_window(selection): for ver in sequence.versions: for tracks in ver.tracks: for segment in tracks.segments: - print(segment.type) + print(segment.attributes) # get clip frame duration record_duration = str(segment.record_duration)[1:-1] clip_duration = timecode_to_frames( @@ -545,6 +545,12 @@ def main_window(selection): # get task type from gui input task_type = task_type_input.text() + # get resolution from gui inputs + width = width_input.text() + height = height_input.text() + pixel_aspect = pixel_aspect_input.text() + fps = fps_input.text() + _cfg_data_back = { "workfile_start_frame": str(frame_start), "shot_handles": handles, @@ -622,7 +628,11 @@ def main_window(selection): "frameStart": frame_start, "frameEnd": frame_end, "handleStart": int(handles), - "handleEnd": int(handles) + "handleEnd": int(handles), + "resolutionWidth": int(width), + "resolutionHeight": int(height), + "pixelAspect": float(pixel_aspect), + "fps": float(fps) } # update custom attributes on shot entity @@ -708,6 +718,13 @@ def main_window(selection): tree.setTextElideMode(QtCore.Qt.ElideNone) with maintained_ftrack_session() as _session, get_config("main") as cfg_d: + + for select in selection: + seq_height = select.height + seq_width = select.width + fps = float(str(select.frame_rate)[:-4]) + break + # input fields hierarchy_label = FlameLabel( 'Parents template', 'normal', window) @@ -728,6 +745,21 @@ def main_window(selection): window ) + width_label = FlameLabel( + 'Sequence width', 'normal', window) + width_input = FlameLineEdit(str(seq_width), window) + + height_label = FlameLabel( + 'Sequence height', 'normal', window) + height_input = FlameLineEdit(str(seq_height), window) + + pixel_aspect_label = FlameLabel( + 'Pixel aspect ratio', 'normal', window) + pixel_aspect_input = FlameLineEdit(str(1.00), window) + + fps_label = FlameLabel( + 'Frame rate', 'normal', window) + fps_input = FlameLineEdit(str(fps), window) # get project name from flame current project project_name = flame.project.current_project.name @@ -769,6 +801,14 @@ def main_window(selection): QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) prop_layout_r.setContentsMargins(0, 0, 0, 0) prop_layout_r.addWidget(source_resolution_btn, 0, 0) + prop_layout_r.addWidget(width_label, 1, 0) + prop_layout_r.addWidget(width_input, 1, 1) + prop_layout_r.addWidget(height_label, 2, 0) + prop_layout_r.addWidget(height_input, 2, 1) + prop_layout_r.addWidget(pixel_aspect_label, 3, 0) + prop_layout_r.addWidget(pixel_aspect_input, 3, 1) + prop_layout_r.addWidget(fps_label, 4, 0) + prop_layout_r.addWidget(fps_input, 4, 1) prop_main_layout = QtWidgets.QHBoxLayout() prop_main_layout.addLayout(prop_layout_l, 1) From 56656b5e2d4cd9fdaf5a4d3e43f7c85c99cca963 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 2 Nov 2021 21:04:09 +0100 Subject: [PATCH 021/307] shot name template --- .../openpype_flame_to_ftrack.py | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index 54455c7139..1c9ed0583e 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -171,6 +171,7 @@ def cfg_default(): [main] workfile_start_frame = 1001 shot_handles = 0 +shot_name_template = {sequence}_{shot} hierarchy_template = shots[Folder]/{sequence}[Sequence] create_task_type = Compositing source_resolution = 0 @@ -521,7 +522,7 @@ def main_window(selection): ] if existing_task: - return existing_task + return existing_task.pop() task = session.create('Task', { "name": task_type.lower(), @@ -533,6 +534,10 @@ def main_window(selection): # start procedure _cfg_data_back = {} + + # get shot name template from gui input + shot_name_template = shot_name_template_input.text() + # get hierarchy from gui input hierarchy_text = hierarchy_template_input.text() @@ -552,6 +557,7 @@ def main_window(selection): fps = fps_input.text() _cfg_data_back = { + "shot_name_template": shot_name_template, "workfile_start_frame": str(frame_start), "shot_handles": handles, "hierarchy_template": hierarchy_text, @@ -594,6 +600,9 @@ def main_window(selection): "task": task_type } + # format shot name template + _shot_name = shot_name_template.format(**shot_attributes) + # format hierarchy template _hierarchy_text = hierarchy_text.format(**shot_attributes) print(_hierarchy_text) @@ -618,7 +627,7 @@ def main_window(selection): f_s_entity = get_ftrack_entity( session, "Shot", - item.text(1), + _shot_name, _parent ) print("Shot entity is: {}".format(f_s_entity)) @@ -726,6 +735,11 @@ def main_window(selection): break # input fields + shot_name_label = FlameLabel( + 'Shot name template', 'normal', window) + shot_name_template_input = FlameLineEdit( + cfg_d["shot_name_template"], window) + hierarchy_label = FlameLabel( 'Parents template', 'normal', window) hierarchy_template_input = FlameLineEdit( @@ -784,14 +798,16 @@ def main_window(selection): ## left props prop_layout_l = QtWidgets.QGridLayout() prop_layout_l.setHorizontalSpacing(30) - prop_layout_l.addWidget(hierarchy_label, 0, 0) - prop_layout_l.addWidget(hierarchy_template_input, 0, 1) - prop_layout_l.addWidget(start_frame_label, 1, 0) - prop_layout_l.addWidget(start_frame_input, 1, 1) - prop_layout_l.addWidget(handles_label, 2, 0) - prop_layout_l.addWidget(handles_input, 2, 1) - prop_layout_l.addWidget(task_type_label, 3, 0) - prop_layout_l.addWidget(task_type_input, 3, 1) + prop_layout_l.addWidget(shot_name_label, 0, 0) + prop_layout_l.addWidget(shot_name_template_input, 0, 1) + prop_layout_l.addWidget(hierarchy_label, 1, 0) + prop_layout_l.addWidget(hierarchy_template_input, 1, 1) + prop_layout_l.addWidget(start_frame_label, 2, 0) + prop_layout_l.addWidget(start_frame_input, 2, 1) + prop_layout_l.addWidget(handles_label, 3, 0) + prop_layout_l.addWidget(handles_input, 3, 1) + prop_layout_l.addWidget(task_type_label, 4, 0) + prop_layout_l.addWidget(task_type_input, 4, 1) # right props prop_widget_r = QtWidgets.QWidget(window) @@ -821,11 +837,8 @@ def main_window(selection): main_frame = QtWidgets.QVBoxLayout(window) main_frame.setMargin(20) - main_frame.addStretch(5) main_frame.addLayout(prop_main_layout) - main_frame.addStretch(10) main_frame.addWidget(tree) - main_frame.addStretch(5) main_frame.addLayout(hbox) window.show() @@ -852,5 +865,4 @@ def get_media_panel_custom_ui_actions(): } ] } - ] From c3e1c81318abe4da49d2b3de4ae4c715eca1883e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 2 Nov 2021 21:05:07 +0100 Subject: [PATCH 022/307] ftrack module path import from environment specific path --- .../openpype_flame_to_ftrack.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index 1c9ed0583e..0e25200ac4 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -7,21 +7,34 @@ from xml.etree import ElementTree as ET import ConfigParser as CP import io - # Fill following constants or set them via environment variable +FTRACK_MODULE_PATH = None FTRACK_API_KEY = None FTRACK_API_USER = None FTRACK_SERVER = None SCRIPT_DIR = os.path.dirname(__file__) EXPORT_PRESETS_DIR = os.path.join(SCRIPT_DIR, "export_preset") -CONFIG_DIR = os.path.join(os.path.expanduser("~/.openpype"), "config") +CONFIG_DIR = os.path.join(os.path.expanduser( + "~/.openpype"), "openpype_flame_to_ftrack") + + +def import_ftrack_api(): + try: + import ftrack_api + return ftrack_api + except ImportError: + import sys + ftrk_m_p = FTRACK_MODULE_PATH or os.getenv("FTRACK_MODULE_PATH") + sys.path.append(ftrk_m_p) + import ftrack_api + return ftrack_api @contextmanager def maintained_ftrack_session(): - import ftrack_api import os + ftrack_api = import_ftrack_api() def validate_credentials(url, user, api): first_validation = True From faee1ba80813645e6192faf9d25796bed308a44a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 2 Nov 2021 21:06:43 +0100 Subject: [PATCH 023/307] style improvements --- .../openpype_flame_to_ftrack/openpype_flame_to_ftrack.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index 0e25200ac4..76719fa509 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -804,11 +804,11 @@ def main_window(selection): task_type_input = FlamePushButtonMenu( cfg_d["create_task_type"], F_PROJ_TASK_TYPES, window) - ## Button + # Button select_all_btn = FlameButton('Select All', select_all, window) ftrack_send_btn = FlameButton('Send to Ftrack', send_to_ftrack, window) - ## left props + # left props prop_layout_l = QtWidgets.QGridLayout() prop_layout_l.setHorizontalSpacing(30) prop_layout_l.addWidget(shot_name_label, 0, 0) From ca3e0280d5a2103f7f1b0990e78a0cdc73ed4296 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 2 Nov 2021 21:36:26 +0100 Subject: [PATCH 024/307] thumbnails and videos wip --- .../openpype_flame_to_ftrack.py | 89 ++++++++++++------- 1 file changed, 59 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index 76719fa509..f2f96281a8 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -209,12 +209,47 @@ def export_thumbnail(sequence, tempdir_path): EXPORT_PRESETS_DIR, "openpype_seg_thumbnails_jpg.xml" ) - print(export_preset) poster_frame_exporter = flame.PyExporter() poster_frame_exporter.foreground = True poster_frame_exporter.export(sequence, export_preset, tempdir_path) +def export_video(sequence, tempdir_path): + import flame + export_preset = os.path.join( + EXPORT_PRESETS_DIR, + "openpype_seg_video_h264.xml" + ) + poster_frame_exporter = flame.PyExporter() + poster_frame_exporter.foreground = True + poster_frame_exporter.export(sequence, export_preset, tempdir_path) + + +def timecode_to_frames(timecode, framerate): + def _seconds(value): + if isinstance(value, str): + _zip_ft = zip((3600, 60, 1, 1 / framerate), value.split(':')) + return sum(f * float(t) for f, t in _zip_ft) + elif isinstance(value, (int, float)): + return value / framerate + return 0 + + def _frames(seconds): + return seconds * framerate + + def tc_to_frames(_timecode, start=None): + return _frames(_seconds(_timecode) - _seconds(start)) + + if '+' in timecode: + timecode = timecode.replace('+', ':') + elif '#' in timecode: + timecode = timecode.replace('#', ':') + + frames = int(round(tc_to_frames(timecode, start='00:00:00:00'))) + + return frames + + class FlameLabel(QtWidgets.QLabel): """ Custom Qt Flame Label Widget @@ -399,34 +434,7 @@ def main_window(selection): import sys import re - def timecode_to_frames(timecode, framerate): - - def _seconds(value): - if isinstance(value, str): - _zip_ft = zip((3600, 60, 1, 1 / framerate), value.split(':')) - return sum(f * float(t) for f, t in _zip_ft) - elif isinstance(value, (int, float)): - return value / framerate - return 0 - - def _frames(seconds): - return seconds * framerate - - def timecode_to_frames(_timecode, start=None): - return _frames(_seconds(_timecode) - _seconds(start)) - - if '+' in timecode: - timecode = timecode.replace('+', ':') - elif '#' in timecode: - timecode = timecode.replace('#', ':') - - frames = int(round(timecode_to_frames(timecode, start='00:00:00:00'))) - - return frames - def timeline_info(selection): - import flame - # identificar as informacoes dos segmentos na timeline for sequence in selection: frame_rate = float(str(sequence.frame_rate)[:-4]) @@ -545,7 +553,9 @@ def main_window(selection): return task - # start procedure + ''' + ##################### start procedure + ''' _cfg_data_back = {} # get shot name template from gui input @@ -578,7 +588,6 @@ def main_window(selection): "source_resolution": ( "1" if source_resolution_btn.isChecked() else "0") } - ######################################################### # add cfg data back to settings.ini set_config(_cfg_data_back, "main") @@ -590,8 +599,17 @@ def main_window(selection): for seq in selection: export_thumbnail(seq, tempdir_path) + export_video(seq, tempdir_path) break + temp_files = os.listdir(tempdir_path) + thumbnails = [f for f in temp_files if "jpg" in f] + videos = [f for f in temp_files if "mov" in f] + + print(temp_files) + print(thumbnails) + print(videos) + # Get all selected items from treewidget for item in tree.selectedItems(): # frame ranges @@ -606,6 +624,17 @@ def main_window(selection): sequence_name = item.text(0) shot_name = item.text(1) + # get component files + thumb_f = next((f for f in thumbnails if shot_name in f), None) + video_f = next((f for f in videos if shot_name in f), None) + print(thumb_f) + print(video_f) + + thumb_fp = os.path.join(tempdir_path, thumb_f) + video_fp = os.path.join(tempdir_path, video_f) + print(thumb_fp) + print(video_fp) + # populate full shot info shot_attributes = { "sequence": sequence_name, From 9ec5d2f5a624bc6a57013e59f7230f5642bd5317 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 3 Nov 2021 14:42:48 +0100 Subject: [PATCH 025/307] project select gui --- .../openpype_flame_to_ftrack.py | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index f2f96281a8..ad2e66d477 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -556,6 +556,15 @@ def main_window(selection): ''' ##################### start procedure ''' + # resolve active project and add it to F_PROJ_ENTITY + if proj_selector: + selected_project_name = project_select_input.text() + F_PROJ_ENTITY = next( + (p for p in all_projects + if p["full_name"] is selected_project_name), + None + ) + _cfg_data_back = {} # get shot name template from gui input @@ -800,7 +809,6 @@ def main_window(selection): 'Source resolutuion', bool(int(cfg_d["source_resolution"])), window ) - width_label = FlameLabel( 'Sequence width', 'normal', window) width_input = FlameLineEdit(str(seq_width), window) @@ -826,6 +834,19 @@ def main_window(selection): # globally used variables F_PROJ_ENTITY = _session.query(query).one() + + proj_selector = bool(not F_PROJ_ENTITY) + + if proj_selector: + all_projects = _session.query( + "Project where status is active").all() + F_PROJ_ENTITY = all_projects[0] + project_names = [p["full_name"] for p in all_projects] + project_select_label = FlameLabel( + 'Select Ftrack project', 'normal', window) + project_select_input = FlamePushButtonMenu( + F_PROJ_ENTITY["full_name"], project_names, window) + F_PROJ_TASK_TYPES = get_all_task_types(F_PROJ_ENTITY) task_type_label = FlameLabel( @@ -838,18 +859,23 @@ def main_window(selection): ftrack_send_btn = FlameButton('Send to Ftrack', send_to_ftrack, window) # left props + v_shift = 0 prop_layout_l = QtWidgets.QGridLayout() prop_layout_l.setHorizontalSpacing(30) - prop_layout_l.addWidget(shot_name_label, 0, 0) - prop_layout_l.addWidget(shot_name_template_input, 0, 1) - prop_layout_l.addWidget(hierarchy_label, 1, 0) - prop_layout_l.addWidget(hierarchy_template_input, 1, 1) - prop_layout_l.addWidget(start_frame_label, 2, 0) - prop_layout_l.addWidget(start_frame_input, 2, 1) - prop_layout_l.addWidget(handles_label, 3, 0) - prop_layout_l.addWidget(handles_input, 3, 1) - prop_layout_l.addWidget(task_type_label, 4, 0) - prop_layout_l.addWidget(task_type_input, 4, 1) + if proj_selector: + prop_layout_l.addWidget(project_select_label, v_shift, 0) + prop_layout_l.addWidget(project_select_input, v_shift, 1) + v_shift += 1 + prop_layout_l.addWidget(shot_name_label, (v_shift + 0), 0) + prop_layout_l.addWidget(shot_name_template_input, (v_shift + 0), 1) + prop_layout_l.addWidget(hierarchy_label, (v_shift + 1), 0) + prop_layout_l.addWidget(hierarchy_template_input, (v_shift + 1), 1) + prop_layout_l.addWidget(start_frame_label, (v_shift + 2), 0) + prop_layout_l.addWidget(start_frame_input, (v_shift + 2), 1) + prop_layout_l.addWidget(handles_label, (v_shift + 3), 0) + prop_layout_l.addWidget(handles_input, (v_shift + 3), 1) + prop_layout_l.addWidget(task_type_label, (v_shift + 4), 0) + prop_layout_l.addWidget(task_type_input, (v_shift + 4), 1) # right props prop_widget_r = QtWidgets.QWidget(window) @@ -868,15 +894,18 @@ def main_window(selection): prop_layout_r.addWidget(fps_label, 4, 0) prop_layout_r.addWidget(fps_input, 4, 1) + # prop layout prop_main_layout = QtWidgets.QHBoxLayout() prop_main_layout.addLayout(prop_layout_l, 1) prop_main_layout.addSpacing(20) prop_main_layout.addWidget(prop_widget_r, 1) + # buttons layout hbox = QtWidgets.QHBoxLayout() hbox.addWidget(select_all_btn) hbox.addWidget(ftrack_send_btn) + # put all layouts together main_frame = QtWidgets.QVBoxLayout(window) main_frame.setMargin(20) main_frame.addLayout(prop_main_layout) From 90d4d4f4016f426a35b286db38cc78552b99b23e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 3 Nov 2021 16:36:13 +0100 Subject: [PATCH 026/307] interactive way of task type offer based on selected project --- .../openpype_flame_to_ftrack.py | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index ad2e66d477..053dff46ba 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -187,7 +187,6 @@ shot_handles = 0 shot_name_template = {sequence}_{shot} hierarchy_template = shots[Folder]/{sequence}[Sequence] create_task_type = Compositing -source_resolution = 0 """ @@ -402,12 +401,11 @@ class FlamePushButtonMenu(QtWidgets.QPushButton): push_button_menu_options = ['Item 1', 'Item 2', 'Item 3', 'Item 4'] menu_push_button = FlamePushButtonMenu(push_button_menu_options[0], push_button_menu_options, window) """ + selection_changed = QtCore.Signal(str) def __init__(self, button_name, menu_options, parent_window, *args, **kwargs): super(FlamePushButtonMenu, self).__init__(*args, **kwargs) - from functools import partial - self.setText(button_name) self.setParent(parent_window) self.setMinimumHeight(28) self.setMinimumWidth(110) @@ -415,17 +413,31 @@ class FlamePushButtonMenu(QtWidgets.QPushButton): self.setStyleSheet('QPushButton {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}') - def create_menu(option): - self.setText(option) pushbutton_menu = QtWidgets.QMenu(parent_window) pushbutton_menu.setFocusPolicy(QtCore.Qt.NoFocus) pushbutton_menu.setStyleSheet('QMenu {color: #9a9a9a; background-color:#24303d; font: 14px "Discreet"}' 'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}') - for option in menu_options: - pushbutton_menu.addAction(option, partial(create_menu, option)) + self._pushbutton_menu = pushbutton_menu self.setMenu(pushbutton_menu) + self.set_menu_options(menu_options, button_name) + + def set_menu_options(self, menu_options, current_option=None): + self._pushbutton_menu.clear() + current_option = current_option or menu_options[0] + + for option in menu_options: + action = self._pushbutton_menu.addAction(option) + action.triggered.connect(self._on_action_trigger) + + if current_option is not None: + self.setText(current_option) + + def _on_action_trigger(self): + action = self.sender() + self.setText(action.text()) + self.selection_changed.emit(action.text()) def main_window(selection): @@ -434,6 +446,10 @@ def main_window(selection): import sys import re + def _on_project_changed(project_name): + task_types = TASK_TYPES_ALL[project_name] + task_type_input.set_menu_options(task_types) + def timeline_info(selection): # identificar as informacoes dos segmentos na timeline for sequence in selection: @@ -593,9 +609,7 @@ def main_window(selection): "workfile_start_frame": str(frame_start), "shot_handles": handles, "hierarchy_template": hierarchy_text, - "create_task_type": task_type, - "source_resolution": ( - "1" if source_resolution_btn.isChecked() else "0") + "create_task_type": task_type } # add cfg data back to settings.ini @@ -805,10 +819,6 @@ def main_window(selection): 'Shot handles', 'normal', window) handles_input = FlameLineEdit(cfg_d["shot_handles"], window) - source_resolution_btn = FlamePushButton( - 'Source resolutuion', bool(int(cfg_d["source_resolution"])), - window - ) width_label = FlameLabel( 'Sequence width', 'normal', window) width_input = FlameLineEdit(str(seq_width), window) @@ -833,7 +843,7 @@ def main_window(selection): query = 'Project where full_name is "{}"'.format(project_name) # globally used variables - F_PROJ_ENTITY = _session.query(query).one() + F_PROJ_ENTITY = _session.query(query).first() proj_selector = bool(not F_PROJ_ENTITY) @@ -842,17 +852,19 @@ def main_window(selection): "Project where status is active").all() F_PROJ_ENTITY = all_projects[0] project_names = [p["full_name"] for p in all_projects] + TASK_TYPES_ALL = {p["full_name"]: get_all_task_types(p).keys() for p in all_projects} project_select_label = FlameLabel( 'Select Ftrack project', 'normal', window) project_select_input = FlamePushButtonMenu( F_PROJ_ENTITY["full_name"], project_names, window) + project_select_input.selection_changed.connect(_on_project_changed) F_PROJ_TASK_TYPES = get_all_task_types(F_PROJ_ENTITY) task_type_label = FlameLabel( 'Create Task (type)', 'normal', window) task_type_input = FlamePushButtonMenu( - cfg_d["create_task_type"], F_PROJ_TASK_TYPES, window) + cfg_d["create_task_type"], F_PROJ_TASK_TYPES.keys(), window) # Button select_all_btn = FlameButton('Select All', select_all, window) @@ -884,7 +896,6 @@ def main_window(selection): prop_layout_r.setAlignment( QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) prop_layout_r.setContentsMargins(0, 0, 0, 0) - prop_layout_r.addWidget(source_resolution_btn, 0, 0) prop_layout_r.addWidget(width_label, 1, 0) prop_layout_r.addWidget(width_input, 1, 1) prop_layout_r.addWidget(height_label, 2, 0) From 6d89bbc7a73c60f43804cd64fb8f3c7178d515fe Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 3 Nov 2021 17:24:56 +0100 Subject: [PATCH 027/307] improving way of working with temp data --- .../openpype_flame_to_ftrack.py | 67 +++++++++++++------ 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index 053dff46ba..54935ccb97 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -1,5 +1,8 @@ from __future__ import print_function import os +import six +import sys +import re from PySide2 import QtWidgets, QtCore from pprint import pformat from contextlib import contextmanager @@ -13,6 +16,9 @@ FTRACK_API_KEY = None FTRACK_API_USER = None FTRACK_SERVER = None +TEMP_DIR_DATA_PATH = None +F_PROJ_ENTITY = None + SCRIPT_DIR = os.path.dirname(__file__) EXPORT_PRESETS_DIR = os.path.join(SCRIPT_DIR, "export_preset") CONFIG_DIR = os.path.join(os.path.expanduser( @@ -92,9 +98,9 @@ def maintained_ftrack_session(): api_key=api ) yield session - except Exception as _E: - print( - "ERROR: {}".format(_E)) + except Exception: + tp, value, tb = sys.exc_info() + six.reraise(tp, value, tb) finally: # close the session session.close() @@ -103,7 +109,6 @@ def maintained_ftrack_session(): @contextmanager def make_temp_dir(): import tempfile - import shutil try: dirpath = tempfile.mkdtemp() @@ -114,8 +119,7 @@ def make_temp_dir(): raise IOError("Not able to create temp dir file: {}".format(_error)) finally: - print(dirpath) - shutil.rmtree(dirpath) + pass @contextmanager @@ -413,7 +417,6 @@ class FlamePushButtonMenu(QtWidgets.QPushButton): self.setStyleSheet('QPushButton {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}') - pushbutton_menu = QtWidgets.QMenu(parent_window) pushbutton_menu.setFocusPolicy(QtCore.Qt.NoFocus) pushbutton_menu.setStyleSheet('QMenu {color: #9a9a9a; background-color:#24303d; font: 14px "Discreet"}' @@ -442,9 +445,8 @@ class FlamePushButtonMenu(QtWidgets.QPushButton): def main_window(selection): import flame - import six - import sys - import re + global TEMP_DIR_DATA_PATH + global F_PROJ_ENTITY def _on_project_changed(project_name): task_types = TASK_TYPES_ALL[project_name] @@ -494,8 +496,28 @@ def main_window(selection): tree.selectAll() + def remove_temp_data(): + global TEMP_DIR_DATA_PATH + import shutil + if TEMP_DIR_DATA_PATH: + shutil.rmtree(TEMP_DIR_DATA_PATH) + TEMP_DIR_DATA_PATH = None + + def generate_temp_data(): + global TEMP_DIR_DATA_PATH + if TEMP_DIR_DATA_PATH: + return True + + with make_temp_dir() as tempdir_path: + for seq in selection: + export_thumbnail(seq, tempdir_path) + export_video(seq, tempdir_path) + TEMP_DIR_DATA_PATH = tempdir_path + break + def send_to_ftrack(): def create_ftrack_entity(session, type, name, parent=None): + global F_PROJ_ENTITY parent = parent or F_PROJ_ENTITY entity = session.create(type, { 'name': name, @@ -511,6 +533,7 @@ def main_window(selection): return entity def get_ftrack_entity(session, type, name, parent): + global F_PROJ_ENTITY query = '{} where name is "{}" and project_id is "{}"'.format( type, name, F_PROJ_ENTITY["id"]) @@ -577,7 +600,7 @@ def main_window(selection): selected_project_name = project_select_input.text() F_PROJ_ENTITY = next( (p for p in all_projects - if p["full_name"] is selected_project_name), + if p["full_name"] in selected_project_name), None ) @@ -615,17 +638,12 @@ def main_window(selection): # add cfg data back to settings.ini set_config(_cfg_data_back, "main") - with maintained_ftrack_session() as session, \ - make_temp_dir() as tempdir_path: - print("tempdir_path: {}".format(tempdir_path)) + with maintained_ftrack_session() as session: print("Ftrack session is: {}".format(session)) - for seq in selection: - export_thumbnail(seq, tempdir_path) - export_video(seq, tempdir_path) - break + generate_temp_data() - temp_files = os.listdir(tempdir_path) + temp_files = os.listdir(TEMP_DIR_DATA_PATH) thumbnails = [f for f in temp_files if "jpg" in f] videos = [f for f in temp_files if "mov" in f] @@ -653,8 +671,8 @@ def main_window(selection): print(thumb_f) print(video_f) - thumb_fp = os.path.join(tempdir_path, thumb_f) - video_fp = os.path.join(tempdir_path, video_f) + thumb_fp = os.path.join(TEMP_DIR_DATA_PATH, thumb_f) + video_fp = os.path.join(TEMP_DIR_DATA_PATH, video_f) print(thumb_fp) print(video_fp) @@ -852,7 +870,8 @@ def main_window(selection): "Project where status is active").all() F_PROJ_ENTITY = all_projects[0] project_names = [p["full_name"] for p in all_projects] - TASK_TYPES_ALL = {p["full_name"]: get_all_task_types(p).keys() for p in all_projects} + TASK_TYPES_ALL = {p["full_name"]: get_all_task_types( + p).keys() for p in all_projects} project_select_label = FlameLabel( 'Select Ftrack project', 'normal', window) project_select_input = FlamePushButtonMenu( @@ -868,6 +887,9 @@ def main_window(selection): # Button select_all_btn = FlameButton('Select All', select_all, window) + remove_temp_data_btn = FlameButton( + 'Remove temp data', remove_temp_data, window) + ftrack_send_btn = FlameButton('Send to Ftrack', send_to_ftrack, window) # left props @@ -913,6 +935,7 @@ def main_window(selection): # buttons layout hbox = QtWidgets.QHBoxLayout() + hbox.addWidget(remove_temp_data_btn) hbox.addWidget(select_all_btn) hbox.addWidget(ftrack_send_btn) From 718f334752ebd2b223e5b13f29e082dcb7782f51 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 4 Nov 2021 06:41:30 +0100 Subject: [PATCH 028/307] adding flame wiretap env into settings and allowing them in prelaunch hook --- openpype/hosts/flame/hooks/pre_flame_setup.py | 9 ++++++--- .../settings/defaults/system_settings/applications.json | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py index 368a70f395..9043eb6cc6 100644 --- a/openpype/hosts/flame/hooks/pre_flame_setup.py +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -2,6 +2,7 @@ import os import json import tempfile import contextlib +import socket from openpype.lib import ( PreLaunchHook, get_openpype_username) from openpype.hosts import flame as opflame @@ -32,6 +33,7 @@ class FlamePrelaunch(PreLaunchHook): """Hook entry method.""" project_doc = self.data["project_doc"] user_name = get_openpype_username() + hostname = socket.gethostname() self.log.debug("Collected user \"{}\"".format(user_name)) self.log.info(pformat(project_doc)) @@ -53,11 +55,12 @@ class FlamePrelaunch(PreLaunchHook): "FieldDominance": "PROGRESSIVE" } + data_to_script = { # from settings - "host_name": "localhost", - "volume_name": "stonefs", - "group_name": "staff", + "host_name": os.getenv("FLAME_WIRETAP_HOSTNAME") or hostname, + "volume_name": os.getenv("FLAME_WIRETAP_VOLUME"), + "group_name": os.getenv("FLAME_WIRETAP_GROUP"), "color_policy": "ACES 1.1", # from project diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 79711f3067..c8871c338c 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -107,7 +107,10 @@ "windows": "", "darwin": "", "linux": "" - } + }, + "FLAME_WIRETAP_HOSTNAME": "", + "FLAME_WIRETAP_VOLUME": "stonefs", + "FLAME_WIRETAP_GROUP": "staff" }, "variants": { "2021": { From 9cc61f1b83629c87a2d0cfd962e1a323a1a2a2fd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 4 Nov 2021 08:50:40 +0100 Subject: [PATCH 029/307] wip commit components of thumb and video --- .../openpype_flame_to_ftrack.py | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index 54935ccb97..23bebe7c2e 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -3,6 +3,7 @@ import os import six import sys import re +import json from PySide2 import QtWidgets, QtCore from pprint import pformat from contextlib import contextmanager @@ -442,6 +443,95 @@ class FlamePushButtonMenu(QtWidgets.QPushButton): self.setText(action.text()) self.selection_changed.emit(action.text()) +class FtrackComponentCreator: + default_location = "ftrack.server" + ftrack_locations = {} + + def __init__(self, session): + self.session = session + self.get_ftrack_location() + + def create_comonent(self, parent, component_data): + location = self.get_ftrack_location + + file_path = component_data["file_path"] + name = component_data["name"] + + # get extension + file = os.path.basename(file_path) + _name, ext = os.path.splitext(file) + + _component_data = { + "name": name, + "file_path": file_path, + "file_type": ext, + "location": location, + "overwrite": True + + } + + if name != "thumnail": + duration = component_data["duration"] + handles = component_data["handles"] + fps = component_data["fps"] + _component_data.update({ + "name": "ftrackreview-mp4", + "metadata": {'ftr_meta': json.dumps({ + 'frameIn': int(0), + 'frameOut': int(duration + handles), + 'frameRate': float(fps)})} + }) + + component_item = { + "component_data": _component_data, + "thumbnail": bool(name == "thumbnail") + } + + # get assettype entity from session + assettype_entity = self.get_assettype({"short": "reference"}) + + # get or create asset entity from session + asset_entity = self.get_asset( + assettype_entity, {"name": "plateReference"}) + # commit if created + + # get or create assetversion entity from session + assetversion_entity = self.get_assetversion( + asset_entity, {"version": 1}) + # commit if created + + # get or create component entity + # overwrite existing members in component enity + # - get data for member from `ftrack.origin` location + + def get_assettype(self, parent, data): + pass + + def get_asset(self, parent, data): + pass + + def get_assetversion(self, parent, data): + pass + + def commit(self): + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + self.session._configure_locations() + six.reraise(tp, value, tb) + + def get_ftrack_location(self, name=None): + if name in self.ftrack_locations: + return self.ftrack_locations[name] + + location = self.session.query( + 'Location where name is "{}"'.format(name) + ).one() + self.ftrack_locations[name] = location + return location + def main_window(selection): import flame From e80518facbac0f268d433ecb00472b17b45ea078 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 4 Nov 2021 22:08:30 +0100 Subject: [PATCH 030/307] component generator finish --- .../openpype_flame_to_ftrack.py | 243 ++++++++++++++---- 1 file changed, 199 insertions(+), 44 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index 23bebe7c2e..c12c1afb72 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -443,86 +443,181 @@ class FlamePushButtonMenu(QtWidgets.QPushButton): self.setText(action.text()) self.selection_changed.emit(action.text()) + class FtrackComponentCreator: default_location = "ftrack.server" ftrack_locations = {} def __init__(self, session): self.session = session - self.get_ftrack_location() + self._get_ftrack_location() - def create_comonent(self, parent, component_data): - location = self.get_ftrack_location + def create_comonent(self, parent, data, assetversion_entity=None): + location = self._get_ftrack_location() - file_path = component_data["file_path"] - name = component_data["name"] + file_path = data["file_path"] # get extension file = os.path.basename(file_path) - _name, ext = os.path.splitext(file) + _n, ext = os.path.splitext(file) - _component_data = { + name = "ftrackreview-mp4" if "mov" in ext else "thumbnail" + + component_data = { "name": name, "file_path": file_path, "file_type": ext, - "location": location, - "overwrite": True - + "location": location } - if name != "thumnail": - duration = component_data["duration"] - handles = component_data["handles"] - fps = component_data["fps"] - _component_data.update({ - "name": "ftrackreview-mp4", + if name == "ftrackreview-mp4": + duration = data["duration"] + handles = data["handles"] + fps = data["fps"] + component_data.update({ "metadata": {'ftr_meta': json.dumps({ 'frameIn': int(0), 'frameOut': int(duration + handles), 'frameRate': float(fps)})} }) - component_item = { - "component_data": _component_data, - "thumbnail": bool(name == "thumbnail") - } + if not assetversion_entity: + # get assettype entity from session + assettype_entity = self._get_assettype({"short": "reference"}) - # get assettype entity from session - assettype_entity = self.get_assettype({"short": "reference"}) + # get or create asset entity from session + asset_entity = self._get_asset({ + "name": "plateReference", + "type": assettype_entity, + "parent": parent + }) - # get or create asset entity from session - asset_entity = self.get_asset( - assettype_entity, {"name": "plateReference"}) - # commit if created - - # get or create assetversion entity from session - assetversion_entity = self.get_assetversion( - asset_entity, {"version": 1}) - # commit if created + # get or create assetversion entity from session + assetversion_entity = self._get_assetversion({ + "version": 1, + "asset": asset_entity + }) # get or create component entity - # overwrite existing members in component enity - # - get data for member from `ftrack.origin` location + self._set_component(component_data, { + "name": name, + "version": assetversion_entity, + }) - def get_assettype(self, parent, data): - pass + return assetversion_entity - def get_asset(self, parent, data): - pass + def _overwrite_members(self, entity, data): + origin_location = self._get_ftrack_location("ftrack.origin") + location = data.pop("location") - def get_assetversion(self, parent, data): - pass + # Removing existing members from location + components = list(entity.get("members", [])) + components += [entity] + for component in components: + for loc in component["component_locations"]: + if location["id"] == loc["location_id"]: + location.remove_component( + component, recursive=False + ) - def commit(self): + # Deleting existing members on component entity + for member in entity.get("members", []): + self.session.delete(member) + del(member) + + self._commit() + + # Reset members in memory + if "members" in entity.keys(): + entity["members"] = [] + + entity["file_type"] = data["file_type"] + + origin_location.add_component( + entity, data["file_path"] + ) + + # Add components to location. + location.add_component( + entity, origin_location, recursive=True) + + def _get_assettype(self, data): + return self.session.query( + self._query("AssetType", data)).first() + + def _set_component(self, comp_data, base_data): + component_metadata = comp_data.pop("metadata", {}) + + component_entity = self.session.query( + self._query("Component", base_data) + ).first() + + if component_entity: + # overwrite existing members in component enity + # - get data for member from `ftrack.origin` location + self._overwrite_members(component_entity, comp_data) + return + + assetversion_entity = base_data["version"] + location = comp_data.pop("location") + + component_entity = assetversion_entity.create_component( + comp_data["file_path"], + data=comp_data, + location=location + ) + + # Adding metadata + existing_component_metadata = component_entity["metadata"] + existing_component_metadata.update(component_metadata) + component_entity["metadata"] = existing_component_metadata + + if comp_data["name"] == "thumbnail": + assetversion_entity["thumbnail_id"] = component_entity["id"] + + self._commit() + + def _get_asset(self, data): + # first find already created + asset_entity = self.session.query( + self._query("Asset", data) + ).first() + + if asset_entity: + return asset_entity + + asset_entity = self.session.create("Asset", data) + + # _commit if created + self._commit() + + return asset_entity + + def _get_assetversion(self, data): + assetversion_entity = self.session.query( + self._query("AssetVersion", data) + ).first() + + if assetversion_entity: + return assetversion_entity + + assetversion_entity = self.session.create("AssetVersion", data) + + # _commit if created + self._commit() + + return assetversion_entity + + def _commit(self): try: - self.session.commit() + self.session._commit() except Exception: tp, value, tb = sys.exc_info() self.session.rollback() self.session._configure_locations() six.reraise(tp, value, tb) - def get_ftrack_location(self, name=None): + def _get_ftrack_location(self, name=None): if name in self.ftrack_locations: return self.ftrack_locations[name] @@ -532,6 +627,48 @@ class FtrackComponentCreator: self.ftrack_locations[name] = location return location + def _query(self, entitytype, data): + """ Generate a query expression from data supplied. + + If a value is not a string, we'll add the id of the entity to the + query. + + Args: + entitytype (str): The type of entity to query. + data (dict): The data to identify the entity. + exclusions (list): All keys to exclude from the query. + + Returns: + str: String query to use with "session.query" + """ + queries = [] + if sys.version_info[0] < 3: + for key, value in data.iteritems(): + if not isinstance(value, (basestring, int)): + print("value: {}".format(value)) + if "id" in value.keys(): + queries.append( + "{0}.id is \"{1}\"".format(key, value["id"]) + ) + else: + queries.append("{0} is \"{1}\"".format(key, value)) + else: + for key, value in data.items(): + if not isinstance(value, (str, int)): + print("value: {}".format(value)) + if "id" in value.keys(): + queries.append( + "{0}.id is \"{1}\"".format(key, value["id"]) + ) + else: + queries.append("{0} is \"{1}\"".format(key, value)) + + query = ( + "select id from " + entitytype + " where " + " and ".join(queries) + ) + print(query) + return query + def main_window(selection): import flame @@ -614,7 +751,7 @@ def main_window(selection): 'parent': parent }) try: - session.commit() + session._commit() except Exception: tp, value, tb = sys.exc_info() session.rollback() @@ -731,6 +868,8 @@ def main_window(selection): with maintained_ftrack_session() as session: print("Ftrack session is: {}".format(session)) + component_creator = FtrackComponentCreator(session) + generate_temp_data() temp_files = os.listdir(TEMP_DIR_DATA_PATH) @@ -805,6 +944,22 @@ def main_window(selection): ) print("Shot entity is: {}".format(f_s_entity)) + # first create thumbnail and get version entity + assetversion_entity = component_creator.create_comonent( + f_s_entity, { + "file_path": thumb_fp + } + ) + # secondly add video to version entity + component_creator.create_comonent( + f_s_entity, { + "file_path": video_fp, + "duration": frame_duration, + "handles": int(handles), + "fps": float(fps) + }, assetversion_entity + ) + # create custom attributtes custom_attrs = { "frameStart": frame_start, @@ -834,7 +989,7 @@ def main_window(selection): task_entity.create_note(task_description, user) try: - session.commit() + session._commit() except Exception: tp, value, tb = sys.exc_info() session.rollback() From 77262990b4d98c61880cc422900e7cebc03dfc83 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 5 Nov 2021 07:52:49 +0100 Subject: [PATCH 031/307] debugging component creator --- openpype/hosts/flame/hooks/pre_flame_setup.py | 2 +- .../openpype_flame_to_ftrack.py | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py index 9043eb6cc6..718c4b574c 100644 --- a/openpype/hosts/flame/hooks/pre_flame_setup.py +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -33,7 +33,7 @@ class FlamePrelaunch(PreLaunchHook): """Hook entry method.""" project_doc = self.data["project_doc"] user_name = get_openpype_username() - hostname = socket.gethostname() + hostname = socket.gethostname() # not returning wiretap host name self.log.debug("Collected user \"{}\"".format(user_name)) self.log.info(pformat(project_doc)) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index c12c1afb72..e19402914d 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -452,7 +452,8 @@ class FtrackComponentCreator: self.session = session self._get_ftrack_location() - def create_comonent(self, parent, data, assetversion_entity=None): + def create_comonent(self, shot_entity, data, assetversion_entity=None): + self.shot_entity = shot_entity location = self._get_ftrack_location() file_path = data["file_path"] @@ -489,7 +490,7 @@ class FtrackComponentCreator: asset_entity = self._get_asset({ "name": "plateReference", "type": assettype_entity, - "parent": parent + "parent": self.shot_entity }) # get or create assetversion entity from session @@ -573,6 +574,7 @@ class FtrackComponentCreator: component_entity["metadata"] = existing_component_metadata if comp_data["name"] == "thumbnail": + self.shot_entity["thumbnail_id"] = component_entity["id"] assetversion_entity["thumbnail_id"] = component_entity["id"] self._commit() @@ -595,7 +597,7 @@ class FtrackComponentCreator: def _get_assetversion(self, data): assetversion_entity = self.session.query( - self._query("AssetVersion", data) + self._query("AssetVersion", data) ).first() if assetversion_entity: @@ -610,7 +612,7 @@ class FtrackComponentCreator: def _commit(self): try: - self.session._commit() + self.session.commit() except Exception: tp, value, tb = sys.exc_info() self.session.rollback() @@ -618,6 +620,8 @@ class FtrackComponentCreator: six.reraise(tp, value, tb) def _get_ftrack_location(self, name=None): + name = name or self.default_location + if name in self.ftrack_locations: return self.ftrack_locations[name] @@ -751,7 +755,7 @@ def main_window(selection): 'parent': parent }) try: - session._commit() + session.commit() except Exception: tp, value, tb = sys.exc_info() session.rollback() @@ -989,7 +993,7 @@ def main_window(selection): task_entity.create_note(task_description, user) try: - session._commit() + session.commit() except Exception: tp, value, tb = sys.exc_info() session.rollback() From f68d23b3f8db40dcb6b5f1c4da3cd7e980f0618d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 5 Nov 2021 11:00:07 +0100 Subject: [PATCH 032/307] breaking into multimodule --- .../flame_to_ftrack_modules/__init__.py | 0 .../flame_to_ftrack_modules/uiwidgets.py | 194 ++++++++++++++ .../openpype_flame_to_ftrack.py | 243 ++---------------- 3 files changed, 222 insertions(+), 215 deletions(-) create mode 100644 openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/flame_to_ftrack_modules/__init__.py create mode 100644 openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/flame_to_ftrack_modules/uiwidgets.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/flame_to_ftrack_modules/__init__.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/flame_to_ftrack_modules/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/flame_to_ftrack_modules/uiwidgets.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/flame_to_ftrack_modules/uiwidgets.py new file mode 100644 index 0000000000..c04801da6f --- /dev/null +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/flame_to_ftrack_modules/uiwidgets.py @@ -0,0 +1,194 @@ +from PySide2 import QtWidgets, QtCore + + +class FlameLabel(QtWidgets.QLabel): + """ + Custom Qt Flame Label Widget + + For different label looks set label_type as: 'normal', 'background', or 'outline' + + To use: + + label = FlameLabel('Label Name', 'normal', window) + """ + + def __init__(self, label_name, label_type, parent_window, *args, **kwargs): + super(FlameLabel, self).__init__(*args, **kwargs) + + self.setText(label_name) + self.setParent(parent_window) + self.setMinimumSize(130, 28) + self.setMaximumHeight(28) + self.setFocusPolicy(QtCore.Qt.NoFocus) + + # Set label stylesheet based on label_type + + if label_type == 'normal': + self.setStyleSheet('QLabel {color: #9a9a9a; border-bottom: 1px inset #282828; font: 14px "Discreet"}' + 'QLabel:disabled {color: #6a6a6a}') + elif label_type == 'background': + self.setAlignment(QtCore.Qt.AlignCenter) + self.setStyleSheet( + 'color: #9a9a9a; background-color: #393939; font: 14px "Discreet"') + elif label_type == 'outline': + self.setAlignment(QtCore.Qt.AlignCenter) + self.setStyleSheet( + 'color: #9a9a9a; background-color: #212121; border: 1px solid #404040; font: 14px "Discreet"') + + +class FlameLineEdit(QtWidgets.QLineEdit): + """ + Custom Qt Flame Line Edit Widget + + Main window should include this: window.setFocusPolicy(QtCore.Qt.StrongFocus) + + To use: + + line_edit = FlameLineEdit('Some text here', window) + """ + + def __init__(self, text, parent_window, *args, **kwargs): + super(FlameLineEdit, self).__init__(*args, **kwargs) + + self.setText(text) + self.setParent(parent_window) + self.setMinimumHeight(28) + self.setMinimumWidth(110) + self.setStyleSheet('QLineEdit {color: #9a9a9a; background-color: #373e47; selection-color: #262626; selection-background-color: #b8b1a7; font: 14px "Discreet"}' + 'QLineEdit:focus {background-color: #474e58}' + 'QLineEdit:disabled {color: #6a6a6a; background-color: #373737}') + + +class FlameTreeWidget(QtWidgets.QTreeWidget): + """ + Custom Qt Flame Tree Widget + + To use: + + tree_headers = ['Header1', 'Header2', 'Header3', 'Header4'] + tree = FlameTreeWidget(tree_headers, window) + """ + + def __init__(self, tree_headers, parent_window, *args, **kwargs): + super(FlameTreeWidget, self).__init__(*args, **kwargs) + + self.setMinimumWidth(1000) + self.setMinimumHeight(300) + self.setSortingEnabled(True) + self.sortByColumn(0, QtCore.Qt.AscendingOrder) + self.setAlternatingRowColors(True) + self.setFocusPolicy(QtCore.Qt.NoFocus) + self.setStyleSheet( + 'QTreeWidget {color: #9a9a9a; background-color: #2a2a2a; alternate-background-color: #2d2d2d; font: 14px "Discreet"}' + 'QTreeWidget::item:selected {color: #d9d9d9; background-color: #474747; border: 1px solid #111111}' + 'QHeaderView {color: #9a9a9a; background-color: #393939; font: 14px "Discreet"}' + 'QTreeWidget::item:selected {selection-background-color: #111111}' + 'QMenu {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' + 'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}' + ) + self.verticalScrollBar().setStyleSheet('color: #818181') + self.horizontalScrollBar().setStyleSheet('color: #818181') + self.setHeaderLabels(tree_headers) + + +class FlameButton(QtWidgets.QPushButton): + """ + Custom Qt Flame Button Widget + + To use: + + button = FlameButton('Button Name', do_this_when_pressed, window) + """ + + def __init__(self, button_name, do_when_pressed, parent_window, + *args, **kwargs): + super(FlameButton, self).__init__(*args, **kwargs) + + self.setText(button_name) + self.setParent(parent_window) + self.setMinimumSize(QtCore.QSize(110, 28)) + self.setMaximumSize(QtCore.QSize(110, 28)) + self.setFocusPolicy(QtCore.Qt.NoFocus) + self.clicked.connect(do_when_pressed) + self.setStyleSheet('QPushButton {color: #9a9a9a; background-color: #424142; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}' + 'QPushButton:pressed {color: #d9d9d9; background-color: #4f4f4f; border-top: 1px inset #666666; font: italic}' + 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}') + + +class FlamePushButton(QtWidgets.QPushButton): + """ + Custom Qt Flame Push Button Widget + + To use: + + pushbutton = FlamePushButton(' Button Name', True_or_False, window) + """ + + def __init__(self, button_name, button_checked, parent_window, + *args, **kwargs): + super(FlamePushButton, self).__init__(*args, **kwargs) + + self.setText(button_name) + self.setParent(parent_window) + self.setCheckable(True) + self.setChecked(button_checked) + self.setMinimumSize(155, 28) + self.setMaximumSize(155, 28) + self.setFocusPolicy(QtCore.Qt.NoFocus) + self.setStyleSheet('QPushButton {color: #9a9a9a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #424142, stop: .94 #2e3b48); text-align: left; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}' + 'QPushButton:checked {color: #d9d9d9; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #4f4f4f, stop: .94 #5a7fb4); font: italic; border: 1px inset black; border-bottom: 1px inset #404040; border-right: 1px inset #404040}' + 'QPushButton:disabled {color: #6a6a6a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #383838, stop: .94 #353535); font: light; border-top: 1px solid #575757; border-bottom: 1px solid #242424; border-right: 1px solid #353535; border-left: 1px solid #353535}' + 'QToolTip {color: black; background-color: #ffffde; border: black solid 1px}') + + +class FlamePushButtonMenu(QtWidgets.QPushButton): + """ + Custom Qt Flame Menu Push Button Widget + + To use: + + push_button_menu_options = ['Item 1', 'Item 2', 'Item 3', 'Item 4'] + menu_push_button = FlamePushButtonMenu('push_button_name', push_button_menu_options, window) + + or + + push_button_menu_options = ['Item 1', 'Item 2', 'Item 3', 'Item 4'] + menu_push_button = FlamePushButtonMenu(push_button_menu_options[0], push_button_menu_options, window) + """ + selection_changed = QtCore.Signal(str) + + def __init__(self, button_name, menu_options, parent_window, + *args, **kwargs): + super(FlamePushButtonMenu, self).__init__(*args, **kwargs) + + self.setParent(parent_window) + self.setMinimumHeight(28) + self.setMinimumWidth(110) + self.setFocusPolicy(QtCore.Qt.NoFocus) + self.setStyleSheet('QPushButton {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' + 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}') + + pushbutton_menu = QtWidgets.QMenu(parent_window) + pushbutton_menu.setFocusPolicy(QtCore.Qt.NoFocus) + pushbutton_menu.setStyleSheet('QMenu {color: #9a9a9a; background-color:#24303d; font: 14px "Discreet"}' + 'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}') + + self._pushbutton_menu = pushbutton_menu + self.setMenu(pushbutton_menu) + self.set_menu_options(menu_options, button_name) + + def set_menu_options(self, menu_options, current_option=None): + self._pushbutton_menu.clear() + current_option = current_option or menu_options[0] + + for option in menu_options: + action = self._pushbutton_menu.addAction(option) + action.triggered.connect(self._on_action_trigger) + + if current_option is not None: + self.setText(current_option) + + def _on_action_trigger(self): + action = self.sender() + self.setText(action.text()) + self.selection_changed.emit(action.text()) \ No newline at end of file diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index e19402914d..d7daa28b95 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -21,10 +21,12 @@ TEMP_DIR_DATA_PATH = None F_PROJ_ENTITY = None SCRIPT_DIR = os.path.dirname(__file__) +PACKAGE_DIR = os.pah.join(SCRIPT_DIR, "flame_to_ftrack_modules") EXPORT_PRESETS_DIR = os.path.join(SCRIPT_DIR, "export_preset") CONFIG_DIR = os.path.join(os.path.expanduser( "~/.openpype"), "openpype_flame_to_ftrack") +sys.path.append(PACKAGE_DIR) def import_ftrack_api(): try: @@ -253,197 +255,6 @@ def timecode_to_frames(timecode, framerate): return frames - -class FlameLabel(QtWidgets.QLabel): - """ - Custom Qt Flame Label Widget - - For different label looks set label_type as: 'normal', 'background', or 'outline' - - To use: - - label = FlameLabel('Label Name', 'normal', window) - """ - - def __init__(self, label_name, label_type, parent_window, *args, **kwargs): - super(FlameLabel, self).__init__(*args, **kwargs) - - self.setText(label_name) - self.setParent(parent_window) - self.setMinimumSize(130, 28) - self.setMaximumHeight(28) - self.setFocusPolicy(QtCore.Qt.NoFocus) - - # Set label stylesheet based on label_type - - if label_type == 'normal': - self.setStyleSheet('QLabel {color: #9a9a9a; border-bottom: 1px inset #282828; font: 14px "Discreet"}' - 'QLabel:disabled {color: #6a6a6a}') - elif label_type == 'background': - self.setAlignment(QtCore.Qt.AlignCenter) - self.setStyleSheet( - 'color: #9a9a9a; background-color: #393939; font: 14px "Discreet"') - elif label_type == 'outline': - self.setAlignment(QtCore.Qt.AlignCenter) - self.setStyleSheet( - 'color: #9a9a9a; background-color: #212121; border: 1px solid #404040; font: 14px "Discreet"') - - -class FlameLineEdit(QtWidgets.QLineEdit): - """ - Custom Qt Flame Line Edit Widget - - Main window should include this: window.setFocusPolicy(QtCore.Qt.StrongFocus) - - To use: - - line_edit = FlameLineEdit('Some text here', window) - """ - - def __init__(self, text, parent_window, *args, **kwargs): - super(FlameLineEdit, self).__init__(*args, **kwargs) - - self.setText(text) - self.setParent(parent_window) - self.setMinimumHeight(28) - self.setMinimumWidth(110) - self.setStyleSheet('QLineEdit {color: #9a9a9a; background-color: #373e47; selection-color: #262626; selection-background-color: #b8b1a7; font: 14px "Discreet"}' - 'QLineEdit:focus {background-color: #474e58}' - 'QLineEdit:disabled {color: #6a6a6a; background-color: #373737}') - - -class FlameTreeWidget(QtWidgets.QTreeWidget): - """ - Custom Qt Flame Tree Widget - - To use: - - tree_headers = ['Header1', 'Header2', 'Header3', 'Header4'] - tree = FlameTreeWidget(tree_headers, window) - """ - - def __init__(self, tree_headers, parent_window, *args, **kwargs): - super(FlameTreeWidget, self).__init__(*args, **kwargs) - - self.setMinimumWidth(1000) - self.setMinimumHeight(300) - self.setSortingEnabled(True) - self.sortByColumn(0, QtCore.Qt.AscendingOrder) - self.setAlternatingRowColors(True) - self.setFocusPolicy(QtCore.Qt.NoFocus) - self.setStyleSheet( - 'QTreeWidget {color: #9a9a9a; background-color: #2a2a2a; alternate-background-color: #2d2d2d; font: 14px "Discreet"}' - 'QTreeWidget::item:selected {color: #d9d9d9; background-color: #474747; border: 1px solid #111111}' - 'QHeaderView {color: #9a9a9a; background-color: #393939; font: 14px "Discreet"}' - 'QTreeWidget::item:selected {selection-background-color: #111111}' - 'QMenu {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' - 'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}' - ) - self.verticalScrollBar().setStyleSheet('color: #818181') - self.horizontalScrollBar().setStyleSheet('color: #818181') - self.setHeaderLabels(tree_headers) - - -class FlameButton(QtWidgets.QPushButton): - """ - Custom Qt Flame Button Widget - - To use: - - button = FlameButton('Button Name', do_this_when_pressed, window) - """ - - def __init__(self, button_name, do_when_pressed, parent_window, *args, **kwargs): - super(FlameButton, self).__init__(*args, **kwargs) - - self.setText(button_name) - self.setParent(parent_window) - self.setMinimumSize(QtCore.QSize(110, 28)) - self.setMaximumSize(QtCore.QSize(110, 28)) - self.setFocusPolicy(QtCore.Qt.NoFocus) - self.clicked.connect(do_when_pressed) - self.setStyleSheet('QPushButton {color: #9a9a9a; background-color: #424142; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}' - 'QPushButton:pressed {color: #d9d9d9; background-color: #4f4f4f; border-top: 1px inset #666666; font: italic}' - 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}') - - -class FlamePushButton(QtWidgets.QPushButton): - """ - Custom Qt Flame Push Button Widget - - To use: - - pushbutton = FlamePushButton(' Button Name', True_or_False, window) - """ - - def __init__(self, button_name, button_checked, parent_window, *args, **kwargs): - super(FlamePushButton, self).__init__(*args, **kwargs) - - self.setText(button_name) - self.setParent(parent_window) - self.setCheckable(True) - self.setChecked(button_checked) - self.setMinimumSize(155, 28) - self.setMaximumSize(155, 28) - self.setFocusPolicy(QtCore.Qt.NoFocus) - self.setStyleSheet('QPushButton {color: #9a9a9a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #424142, stop: .94 #2e3b48); text-align: left; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}' - 'QPushButton:checked {color: #d9d9d9; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #4f4f4f, stop: .94 #5a7fb4); font: italic; border: 1px inset black; border-bottom: 1px inset #404040; border-right: 1px inset #404040}' - 'QPushButton:disabled {color: #6a6a6a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #383838, stop: .94 #353535); font: light; border-top: 1px solid #575757; border-bottom: 1px solid #242424; border-right: 1px solid #353535; border-left: 1px solid #353535}' - 'QToolTip {color: black; background-color: #ffffde; border: black solid 1px}') - - -class FlamePushButtonMenu(QtWidgets.QPushButton): - """ - Custom Qt Flame Menu Push Button Widget - - To use: - - push_button_menu_options = ['Item 1', 'Item 2', 'Item 3', 'Item 4'] - menu_push_button = FlamePushButtonMenu('push_button_name', push_button_menu_options, window) - - or - - push_button_menu_options = ['Item 1', 'Item 2', 'Item 3', 'Item 4'] - menu_push_button = FlamePushButtonMenu(push_button_menu_options[0], push_button_menu_options, window) - """ - selection_changed = QtCore.Signal(str) - - def __init__(self, button_name, menu_options, parent_window, *args, **kwargs): - super(FlamePushButtonMenu, self).__init__(*args, **kwargs) - - self.setParent(parent_window) - self.setMinimumHeight(28) - self.setMinimumWidth(110) - self.setFocusPolicy(QtCore.Qt.NoFocus) - self.setStyleSheet('QPushButton {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' - 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}') - - pushbutton_menu = QtWidgets.QMenu(parent_window) - pushbutton_menu.setFocusPolicy(QtCore.Qt.NoFocus) - pushbutton_menu.setStyleSheet('QMenu {color: #9a9a9a; background-color:#24303d; font: 14px "Discreet"}' - 'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}') - - self._pushbutton_menu = pushbutton_menu - self.setMenu(pushbutton_menu) - self.set_menu_options(menu_options, button_name) - - def set_menu_options(self, menu_options, current_option=None): - self._pushbutton_menu.clear() - current_option = current_option or menu_options[0] - - for option in menu_options: - action = self._pushbutton_menu.addAction(option) - action.triggered.connect(self._on_action_trigger) - - if current_option is not None: - self.setText(current_option) - - def _on_action_trigger(self): - action = self.sender() - self.setText(action.text()) - self.selection_changed.emit(action.text()) - - class FtrackComponentCreator: default_location = "ftrack.server" ftrack_locations = {} @@ -676,6 +487,8 @@ class FtrackComponentCreator: def main_window(selection): import flame + import uiwidgets + global TEMP_DIR_DATA_PATH global F_PROJ_ENTITY @@ -1043,7 +856,7 @@ def main_window(selection): print(ordered_column_labels) - tree = FlameTreeWidget(ordered_column_labels, window) + tree = uiwidgets.FlameTreeWidget(ordered_column_labels, window) # Allow multiple items in tree to be selected tree.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) @@ -1067,40 +880,40 @@ def main_window(selection): break # input fields - shot_name_label = FlameLabel( + shot_name_label = uiwidgets.FlameLabel( 'Shot name template', 'normal', window) - shot_name_template_input = FlameLineEdit( + shot_name_template_input = uiwidgets.FlameLineEdit( cfg_d["shot_name_template"], window) - hierarchy_label = FlameLabel( + hierarchy_label = uiwidgets.FlameLabel( 'Parents template', 'normal', window) - hierarchy_template_input = FlameLineEdit( + hierarchy_template_input = uiwidgets.FlameLineEdit( cfg_d["hierarchy_template"], window) - start_frame_label = FlameLabel( + start_frame_label = uiwidgets.FlameLabel( 'Workfile start frame', 'normal', window) - start_frame_input = FlameLineEdit( + start_frame_input = uiwidgets.FlameLineEdit( cfg_d["workfile_start_frame"], window) - handles_label = FlameLabel( + handles_label = uiwidgets.FlameLabel( 'Shot handles', 'normal', window) - handles_input = FlameLineEdit(cfg_d["shot_handles"], window) + handles_input = uiwidgets.FlameLineEdit(cfg_d["shot_handles"], window) - width_label = FlameLabel( + width_label = uiwidgets.FlameLabel( 'Sequence width', 'normal', window) - width_input = FlameLineEdit(str(seq_width), window) + width_input = uiwidgets.FlameLineEdit(str(seq_width), window) - height_label = FlameLabel( + height_label = uiwidgets.FlameLabel( 'Sequence height', 'normal', window) - height_input = FlameLineEdit(str(seq_height), window) + height_input = uiwidgets.FlameLineEdit(str(seq_height), window) - pixel_aspect_label = FlameLabel( + pixel_aspect_label = uiwidgets.FlameLabel( 'Pixel aspect ratio', 'normal', window) - pixel_aspect_input = FlameLineEdit(str(1.00), window) + pixel_aspect_input = uiwidgets.FlameLineEdit(str(1.00), window) - fps_label = FlameLabel( + fps_label = uiwidgets.FlameLabel( 'Frame rate', 'normal', window) - fps_input = FlameLineEdit(str(fps), window) + fps_input = uiwidgets.FlameLineEdit(str(fps), window) # get project name from flame current project project_name = flame.project.current_project.name @@ -1121,25 +934,25 @@ def main_window(selection): project_names = [p["full_name"] for p in all_projects] TASK_TYPES_ALL = {p["full_name"]: get_all_task_types( p).keys() for p in all_projects} - project_select_label = FlameLabel( + project_select_label = uiwidgets.FlameLabel( 'Select Ftrack project', 'normal', window) - project_select_input = FlamePushButtonMenu( + project_select_input = uiwidgets.FlamePushButtonMenu( F_PROJ_ENTITY["full_name"], project_names, window) project_select_input.selection_changed.connect(_on_project_changed) F_PROJ_TASK_TYPES = get_all_task_types(F_PROJ_ENTITY) - task_type_label = FlameLabel( + task_type_label = uiwidgets.FlameLabel( 'Create Task (type)', 'normal', window) - task_type_input = FlamePushButtonMenu( + task_type_input = uiwidgets.FlamePushButtonMenu( cfg_d["create_task_type"], F_PROJ_TASK_TYPES.keys(), window) # Button - select_all_btn = FlameButton('Select All', select_all, window) - remove_temp_data_btn = FlameButton( + select_all_btn = uiwidgets.FlameButton('Select All', select_all, window) + remove_temp_data_btn = uiwidgets.FlameButton( 'Remove temp data', remove_temp_data, window) - ftrack_send_btn = FlameButton('Send to Ftrack', send_to_ftrack, window) + ftrack_send_btn = uiwidgets.FlameButton('Send to Ftrack', send_to_ftrack, window) # left props v_shift = 0 From 1fae4704f18f9a310fe9bea4a2996f9cef89ae8e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 5 Nov 2021 12:11:24 +0100 Subject: [PATCH 033/307] adding alter xml for export preset --- .../openpype_flame_to_ftrack.py | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index d7daa28b95..41ddb05046 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -28,6 +28,7 @@ CONFIG_DIR = os.path.join(os.path.expanduser( sys.path.append(PACKAGE_DIR) + def import_ftrack_api(): try: import ftrack_api @@ -208,27 +209,41 @@ def get_all_task_types(project_entity): return tasks +def configure_preset(file_path, data): + split_fp = os.path.splitext(file_path) + new_file_path = split_fp[0] + "_tmp" + split_fp[-1] + with open(file_path, "r") as datafile: + tree = ET.parse(datafile) + for key, value in data.items(): + for element in tree.findall(".//{}".format(key)): + print(element) + element.text = str(value) + tree.write(new_file_path) -def export_thumbnail(sequence, tempdir_path): + return new_file_path + +def export_thumbnail(sequence, tempdir_path, data): import flame export_preset = os.path.join( EXPORT_PRESETS_DIR, "openpype_seg_thumbnails_jpg.xml" ) + new_path = configure_preset(export_preset, data) poster_frame_exporter = flame.PyExporter() poster_frame_exporter.foreground = True - poster_frame_exporter.export(sequence, export_preset, tempdir_path) + poster_frame_exporter.export(sequence, new_path, tempdir_path) -def export_video(sequence, tempdir_path): +def export_video(sequence, tempdir_path, data): import flame export_preset = os.path.join( EXPORT_PRESETS_DIR, "openpype_seg_video_h264.xml" ) + new_path = configure_preset(export_preset, data) poster_frame_exporter = flame.PyExporter() poster_frame_exporter.foreground = True - poster_frame_exporter.export(sequence, export_preset, tempdir_path) + poster_frame_exporter.export(sequence, new_path, tempdir_path) def timecode_to_frames(timecode, framerate): @@ -547,15 +562,15 @@ def main_window(selection): shutil.rmtree(TEMP_DIR_DATA_PATH) TEMP_DIR_DATA_PATH = None - def generate_temp_data(): + def generate_temp_data(change_preset_data): global TEMP_DIR_DATA_PATH if TEMP_DIR_DATA_PATH: return True with make_temp_dir() as tempdir_path: for seq in selection: - export_thumbnail(seq, tempdir_path) - export_video(seq, tempdir_path) + export_thumbnail(seq, tempdir_path, change_preset_data) + export_video(seq, tempdir_path, change_preset_data) TEMP_DIR_DATA_PATH = tempdir_path break @@ -687,7 +702,9 @@ def main_window(selection): component_creator = FtrackComponentCreator(session) - generate_temp_data() + generate_temp_data({ + "nbHandles": handles + }) temp_files = os.listdir(TEMP_DIR_DATA_PATH) thumbnails = [f for f in temp_files if "jpg" in f] From 2e3c3e78aee2a097f85bb0abb77d89c873cdfd5e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 5 Nov 2021 12:15:20 +0100 Subject: [PATCH 034/307] fix on updating metadata of ftrack componet --- .../openpype_flame_to_ftrack/openpype_flame_to_ftrack.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index 41ddb05046..8e51320b00 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -380,6 +380,11 @@ class FtrackComponentCreator: ).first() if component_entity: + # Adding metadata + existing_component_metadata = component_entity["metadata"] + existing_component_metadata.update(component_metadata) + component_entity["metadata"] = existing_component_metadata + # overwrite existing members in component enity # - get data for member from `ftrack.origin` location self._overwrite_members(component_entity, comp_data) From 04523a2e94ce8e512a398a2fbf1d85c0fffbd77b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 5 Nov 2021 13:27:59 +0100 Subject: [PATCH 035/307] fix typo --- .../openpype_flame_to_ftrack/openpype_flame_to_ftrack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index 8e51320b00..0501c24b48 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -21,7 +21,7 @@ TEMP_DIR_DATA_PATH = None F_PROJ_ENTITY = None SCRIPT_DIR = os.path.dirname(__file__) -PACKAGE_DIR = os.pah.join(SCRIPT_DIR, "flame_to_ftrack_modules") +PACKAGE_DIR = os.path.join(SCRIPT_DIR, "flame_to_ftrack_modules") EXPORT_PRESETS_DIR = os.path.join(SCRIPT_DIR, "export_preset") CONFIG_DIR = os.path.join(os.path.expanduser( "~/.openpype"), "openpype_flame_to_ftrack") From 835419d839351c954c6fc96b2a9336568663c490 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 9 Nov 2021 11:00:22 +0100 Subject: [PATCH 036/307] little improvements --- .../openpype_flame_to_ftrack.py | 88 ++++++++++++------- 1 file changed, 58 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index 0501c24b48..a598c52cc0 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -1,6 +1,5 @@ from __future__ import print_function import os -import six import sys import re import json @@ -19,6 +18,7 @@ FTRACK_SERVER = None TEMP_DIR_DATA_PATH = None F_PROJ_ENTITY = None +COMPONENTS_DONE = [] SCRIPT_DIR = os.path.dirname(__file__) PACKAGE_DIR = os.path.join(SCRIPT_DIR, "flame_to_ftrack_modules") @@ -209,6 +209,7 @@ def get_all_task_types(project_entity): return tasks + def configure_preset(file_path, data): split_fp = os.path.splitext(file_path) new_file_path = split_fp[0] + "_tmp" + split_fp[-1] @@ -222,6 +223,7 @@ def configure_preset(file_path, data): return new_file_path + def export_thumbnail(sequence, tempdir_path, data): import flame export_preset = os.path.join( @@ -270,6 +272,7 @@ def timecode_to_frames(timecode, framerate): return frames + class FtrackComponentCreator: default_location = "ftrack.server" ftrack_locations = {} @@ -278,6 +281,10 @@ class FtrackComponentCreator: self.session = session self._get_ftrack_location() + def close(self): + self.ftrack_locations = {} + self.session = None + def create_comonent(self, shot_entity, data, assetversion_entity=None): self.shot_entity = shot_entity location = self._get_ftrack_location() @@ -304,7 +311,7 @@ class FtrackComponentCreator: component_data.update({ "metadata": {'ftr_meta': json.dumps({ 'frameIn': int(0), - 'frameOut': int(duration + handles), + 'frameOut': int(duration + (handles * 2)), 'frameRate': float(fps)})} }) @@ -380,14 +387,14 @@ class FtrackComponentCreator: ).first() if component_entity: + # overwrite existing members in component enity + # - get data for member from `ftrack.origin` location + self._overwrite_members(component_entity, comp_data) + # Adding metadata existing_component_metadata = component_entity["metadata"] existing_component_metadata.update(component_metadata) component_entity["metadata"] = existing_component_metadata - - # overwrite existing members in component enity - # - get data for member from `ftrack.origin` location - self._overwrite_members(component_entity, comp_data) return assetversion_entity = base_data["version"] @@ -511,6 +518,7 @@ def main_window(selection): global TEMP_DIR_DATA_PATH global F_PROJ_ENTITY + global COMPONENTS_DONE def _on_project_changed(project_name): task_types = TASK_TYPES_ALL[project_name] @@ -524,6 +532,8 @@ def main_window(selection): for tracks in ver.tracks: for segment in tracks.segments: print(segment.attributes) + if str(segment.name)[1:-1] == "": + continue # get clip frame duration record_duration = str(segment.record_duration)[1:-1] clip_duration = timecode_to_frames( @@ -561,12 +571,17 @@ def main_window(selection): tree.selectAll() def remove_temp_data(): - global TEMP_DIR_DATA_PATH import shutil + global TEMP_DIR_DATA_PATH + global COMPONENTS_DONE + + COMPONENTS_DONE = [] + if TEMP_DIR_DATA_PATH: shutil.rmtree(TEMP_DIR_DATA_PATH) TEMP_DIR_DATA_PATH = None + def generate_temp_data(change_preset_data): global TEMP_DIR_DATA_PATH if TEMP_DIR_DATA_PATH: @@ -707,9 +722,10 @@ def main_window(selection): component_creator = FtrackComponentCreator(session) + generate_temp_data({ - "nbHandles": handles - }) + "nbHandles": handles + }) temp_files = os.listdir(TEMP_DIR_DATA_PATH) thumbnails = [f for f in temp_files if "jpg" in f] @@ -736,15 +752,21 @@ def main_window(selection): # get component files thumb_f = next((f for f in thumbnails if shot_name in f), None) video_f = next((f for f in videos if shot_name in f), None) - print(thumb_f) - print(video_f) - thumb_fp = os.path.join(TEMP_DIR_DATA_PATH, thumb_f) video_fp = os.path.join(TEMP_DIR_DATA_PATH, video_f) print(thumb_fp) print(video_fp) - # populate full shot info + print("processed comps: {}".format(COMPONENTS_DONE)) + processed = False + if thumb_f not in COMPONENTS_DONE: + COMPONENTS_DONE.append(thumb_f) + else: + processed = True + + print("processed: {}".format(processed)) + + # populate full shot info shot_attributes = { "sequence": sequence_name, "shot": shot_name, @@ -783,21 +805,23 @@ def main_window(selection): ) print("Shot entity is: {}".format(f_s_entity)) - # first create thumbnail and get version entity - assetversion_entity = component_creator.create_comonent( - f_s_entity, { - "file_path": thumb_fp - } - ) - # secondly add video to version entity - component_creator.create_comonent( - f_s_entity, { - "file_path": video_fp, - "duration": frame_duration, - "handles": int(handles), - "fps": float(fps) - }, assetversion_entity - ) + if not processed: + # first create thumbnail and get version entity + assetversion_entity = component_creator.create_comonent( + f_s_entity, { + "file_path": thumb_fp + } + ) + + # secondly add video to version entity + component_creator.create_comonent( + f_s_entity, { + "file_path": video_fp, + "duration": frame_duration, + "handles": int(handles), + "fps": float(fps) + }, assetversion_entity + ) # create custom attributtes custom_attrs = { @@ -835,6 +859,8 @@ def main_window(selection): session._configure_locations() six.reraise(tp, value, tb) + component_creator.close() + # creating ui window = QtWidgets.QWidget() window.setMinimumSize(1500, 600) @@ -970,11 +996,13 @@ def main_window(selection): cfg_d["create_task_type"], F_PROJ_TASK_TYPES.keys(), window) # Button - select_all_btn = uiwidgets.FlameButton('Select All', select_all, window) + select_all_btn = uiwidgets.FlameButton( + 'Select All', select_all, window) remove_temp_data_btn = uiwidgets.FlameButton( 'Remove temp data', remove_temp_data, window) - ftrack_send_btn = uiwidgets.FlameButton('Send to Ftrack', send_to_ftrack, window) + ftrack_send_btn = uiwidgets.FlameButton( + 'Send to Ftrack', send_to_ftrack, window) # left props v_shift = 0 From fabfaac3f56df68e7096402f9223623ce6e35e58 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 16 Nov 2021 09:43:42 +0100 Subject: [PATCH 037/307] OP-2019 - extracted headless_publish function to lib --- openpype/lib/remote_publish.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index f7d7955b79..79b130913f 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -11,6 +11,21 @@ from openpype.lib.mongo import OpenPypeMongoConnection from openpype.lib.plugin_tools import parse_json +def headless_publish(log): + """Runs publish in a opened host with a context and closes Python process. + + Host is being closed via ClosePS pyblish plugin which triggers 'exit' + method in ConsoleTrayApp. + """ + dbcon = get_webpublish_conn() + _id = os.environ.get("BATCH_LOG_ID") + if not _id: + log.warning("Unable to store log records, batch will be unfinished!") + return + + publish_and_log(dbcon, _id, log, 'CloseAE') + + def get_webpublish_conn(): """Get connection to OP 'webpublishes' collection.""" mongo_client = OpenPypeMongoConnection.get_mongo_client() From 939eabfc41f6e284c826cd225fc718865bf952cf Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 16 Nov 2021 09:44:16 +0100 Subject: [PATCH 038/307] OP-2019 - remote publish needs to have order of methods flipped --- .../hosts/aftereffects/plugins/publish/extract_local_render.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index 37337e7fee..b36ab24bde 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -19,10 +19,9 @@ class ExtractLocalRender(openpype.api.Extractor): staging_dir = instance.data["stagingDir"] self.log.info("staging_dir::{}".format(staging_dir)) - stub.render(staging_dir) - # pull file name from Render Queue Output module render_q = stub.get_render_info() + stub.render(staging_dir) if not render_q: raise ValueError("No file extension set in Render Queue") _, ext = os.path.splitext(os.path.basename(render_q.file_name)) From 6b45101ed087e8a49d59a8efa01477a6e725a120 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 16 Nov 2021 09:44:49 +0100 Subject: [PATCH 039/307] OP-2019 - added test classes for remote publish --- .../aftereffects/plugins/publish/closeAE.py | 29 ++++++ .../test_publish_in_aftereffects.py | 96 +++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 openpype/hosts/aftereffects/plugins/publish/closeAE.py create mode 100644 tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py diff --git a/openpype/hosts/aftereffects/plugins/publish/closeAE.py b/openpype/hosts/aftereffects/plugins/publish/closeAE.py new file mode 100644 index 0000000000..e6e9623474 --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/publish/closeAE.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +"""Close AE after publish. For Webpublishing only.""" +import os + +import pyblish.api + +from avalon import aftereffects + + +class CloseAE(pyblish.api.ContextPlugin): + """Close AE after publish. For Webpublishing only. + """ + + order = pyblish.api.IntegratorOrder + 14 + label = "Close AE" + optional = True + active = True + + hosts = ["aftereffects"] + targets = ["remotepublish"] + + def process(self, context): + self.log.info("CloseAE") + + stub = aftereffects.stub() + self.log.info("Shutting down AE") + stub.save() + stub.close() + self.log.info("AE closed") diff --git a/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py b/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py new file mode 100644 index 0000000000..f709e40120 --- /dev/null +++ b/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py @@ -0,0 +1,96 @@ +import pytest +import os +import shutil + +from tests.lib.testing_classes import PublishTest + + +class TestPublishInAfterEffects(PublishTest): + """Basic test case for publishing in AfterEffects + + Uses generic TestCase to prepare fixtures for test data, testing DBs, + env vars. + + Opens AfterEffects, run publish on prepared workile. + + Then checks content of DB (if subset, version, representations were + created. + Checks tmp folder if all expected files were published. + + """ + PERSIST = True + + TEST_FILES = [ + ("1qsrq6OJWVpOeXE2LTWrdbsLqEVu155Uf", + "test_aftereffects_publish.zip", + "") + ] + + APP = "aftereffects" + APP_VARIANT = "2021" + + APP_NAME = "{}/{}".format(APP, APP_VARIANT) + + TIMEOUT = 120 # publish timeout + + @pytest.fixture(scope="module") + def last_workfile_path(self, download_test_data): + """Get last_workfile_path from source data. + + Maya expects workfile in proper folder, so copy is done first. + """ + src_path = os.path.join(download_test_data, + "input", + "workfile", + "test_project_test_asset_TestTask_v001.aep") + dest_folder = os.path.join(download_test_data, + self.PROJECT, + self.ASSET, + "work", + self.TASK) + os.makedirs(dest_folder) + dest_path = os.path.join(dest_folder, + "test_project_test_asset_TestTask_v001.aep") + shutil.copy(src_path, dest_path) + + yield dest_path + + @pytest.fixture(scope="module") + def startup_scripts(self, monkeypatch_session, download_test_data): + """Points AfterEffects to userSetup file from input data""" + os.environ["HEADLESS_PUBLISH"] = "true" + + def test_db_asserts(self, dbcon, publish_finished): + """Host and input data dependent expected results in DB.""" + print("test_db_asserts") + assert 5 == dbcon.count_documents({"type": "version"}), \ + "Not expected no of versions" + + assert 0 == dbcon.count_documents({"type": "version", + "name": {"$ne": 1}}), \ + "Only versions with 1 expected" + + assert 1 == dbcon.count_documents({"type": "subset", + "name": "modelMain"}), \ + "modelMain subset must be present" + + assert 1 == dbcon.count_documents({"type": "subset", + "name": "workfileTest_task"}), \ + "workfileTest_task subset must be present" + + assert 11 == dbcon.count_documents({"type": "representation"}), \ + "Not expected no of representations" + + assert 2 == dbcon.count_documents({"type": "representation", + "context.subset": "modelMain", + "context.ext": "abc"}), \ + "Not expected no of representations with ext 'abc'" + + assert 2 == dbcon.count_documents({"type": "representation", + "context.subset": "modelMain", + "context.ext": "ma"}), \ + "Not expected no of representations with ext 'abc'" + + +if __name__ == "__main__": + test_case = TestPublishInAfterEffects() From 4c57c117dfd0ddd15708d6c6f5fe0ba68654e290 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 18 Nov 2021 11:21:17 +0100 Subject: [PATCH 040/307] rename folder to simple modules --- .../{flame_to_ftrack_modules => modules}/__init__.py | 0 .../{flame_to_ftrack_modules => modules}/uiwidgets.py | 0 .../openpype_flame_to_ftrack/openpype_flame_to_ftrack.py | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/{flame_to_ftrack_modules => modules}/__init__.py (100%) rename openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/{flame_to_ftrack_modules => modules}/uiwidgets.py (100%) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/flame_to_ftrack_modules/__init__.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/__init__.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/flame_to_ftrack_modules/__init__.py rename to openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/__init__.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/flame_to_ftrack_modules/uiwidgets.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/flame_to_ftrack_modules/uiwidgets.py rename to openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index a598c52cc0..daab7c1754 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -21,7 +21,7 @@ F_PROJ_ENTITY = None COMPONENTS_DONE = [] SCRIPT_DIR = os.path.dirname(__file__) -PACKAGE_DIR = os.path.join(SCRIPT_DIR, "flame_to_ftrack_modules") +PACKAGE_DIR = os.path.join(SCRIPT_DIR, "modules") EXPORT_PRESETS_DIR = os.path.join(SCRIPT_DIR, "export_preset") CONFIG_DIR = os.path.join(os.path.expanduser( "~/.openpype"), "openpype_flame_to_ftrack") From af1a06c00052743f928cbe67d75ce448bc86fe6d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 18 Nov 2021 18:02:06 +0100 Subject: [PATCH 041/307] OP-2019 - bump up version of PS --- tests/integration/hosts/photoshop/test_publish_in_photoshop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py index 396468a966..3fdfce7cc0 100644 --- a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py @@ -25,7 +25,7 @@ class TestPublishInPhotoshop(PublishTest): ] APP = "photoshop" - APP_VARIANT = "2020" + APP_VARIANT = "2021" APP_NAME = "{}/{}".format(APP, APP_VARIANT) From 04fd46f9e91e6b42089957581c25b18ad51062b0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 18 Nov 2021 18:02:42 +0100 Subject: [PATCH 042/307] OP-2019 - added AE 2022 --- .../defaults/system_settings/applications.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index cc80a94d3f..8c119658be 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1098,6 +1098,23 @@ "linux": [] }, "environment": {} + }, + "2022": { + "enabled": true, + "variant_label": "2022", + "executables": { + "windows": [ + "C:\\Program Files\\Adobe\\Adobe After Effects 2022\\Support Files\\AfterFX.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": {} } } }, From 1369098454ac40bc474283706e6508d756dd8bcb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 18 Nov 2021 18:07:07 +0100 Subject: [PATCH 043/307] OP-2019 - added IS_TEST env var Used to differentiate between regular REMOTE_PUBLISH and automatic tests --- tests/integration/hosts/photoshop/test_publish_in_photoshop.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py index 3fdfce7cc0..f8be8599ee 100644 --- a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py @@ -57,6 +57,7 @@ class TestPublishInPhotoshop(PublishTest): def startup_scripts(self, monkeypatch_session, download_test_data): """Points Maya to userSetup file from input data""" os.environ["IS_HEADLESS"] = "true" + os.environ["IS_TEST"] = "true" def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" From 30e39bebabc7352bcc998850931a58b7bc95ec65 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 18 Nov 2021 18:15:08 +0100 Subject: [PATCH 044/307] OP-2019 - removed setting env vars in class Necessary env vars should be configured in testing zip file --- tests/integration/hosts/photoshop/test_publish_in_photoshop.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py index f8be8599ee..b634d422f3 100644 --- a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py @@ -56,8 +56,7 @@ class TestPublishInPhotoshop(PublishTest): @pytest.fixture(scope="module") def startup_scripts(self, monkeypatch_session, download_test_data): """Points Maya to userSetup file from input data""" - os.environ["IS_HEADLESS"] = "true" - os.environ["IS_TEST"] = "true" + pass def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" From 35153fca3ae5d83a64334f9bda14bcd40e011a60 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 18 Nov 2021 18:16:56 +0100 Subject: [PATCH 045/307] OP-2019 - added working test case for After Effects --- .../hosts/aftereffects/test_publish_in_aftereffects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py b/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py index f709e40120..d4e88dfd4c 100644 --- a/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py +++ b/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py @@ -58,7 +58,7 @@ class TestPublishInAfterEffects(PublishTest): @pytest.fixture(scope="module") def startup_scripts(self, monkeypatch_session, download_test_data): """Points AfterEffects to userSetup file from input data""" - os.environ["HEADLESS_PUBLISH"] = "true" + pass def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" From 7456d0c3805f7d26e867ba46a08c75ccef97e632 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 18 Nov 2021 18:17:21 +0100 Subject: [PATCH 046/307] OP-2019 - added remote publishing method for automatic tests --- openpype/lib/remote_publish.py | 45 ++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index 79b130913f..3483898af7 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -11,19 +11,23 @@ from openpype.lib.mongo import OpenPypeMongoConnection from openpype.lib.plugin_tools import parse_json -def headless_publish(log): +def headless_publish(log, close_plugin_name=None, is_test=False): """Runs publish in a opened host with a context and closes Python process. Host is being closed via ClosePS pyblish plugin which triggers 'exit' method in ConsoleTrayApp. """ - dbcon = get_webpublish_conn() - _id = os.environ.get("BATCH_LOG_ID") - if not _id: - log.warning("Unable to store log records, batch will be unfinished!") - return + if not is_test: + dbcon = get_webpublish_conn() + _id = os.environ.get("BATCH_LOG_ID") + if not _id: + log.warning("Unable to store log records, " + "batch will be unfinished!") + return - publish_and_log(dbcon, _id, log, 'CloseAE') + publish_and_log(dbcon, _id, log, close_plugin_name) + else: + publish(log, 'CloseAE') def get_webpublish_conn(): @@ -51,6 +55,33 @@ def start_webpublish_log(dbcon, batch_id, user): }).inserted_id +def publish(log, close_plugin_name=None): + """Loops through all plugins, logs to console. Used for tests. + + Args: + log (OpenPypeLogger) + close_plugin_name (str): name of plugin with responsibility to + close host app + """ + # Error exit as soon as any error occurs. + error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}" + + close_plugin = _get_close_plugin(close_plugin_name, log) + + for result in pyblish.util.publish_iter(): + for record in result["records"]: + log.info("{}: {}".format( + result["plugin"].label, record.msg)) + + if result["error"]: + log.error(error_format.format(**result)) + uninstall() + if close_plugin: # close host app explicitly after error + context = pyblish.api.Context() + close_plugin().process(context) + sys.exit(1) + + def publish_and_log(dbcon, _id, log, close_plugin_name=None): """Loops through all plugins, logs ok and fails into OP DB. From bcdca93cacbcc015d7b09babf3d23ca446b5cf8a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 18 Nov 2021 18:17:56 +0100 Subject: [PATCH 047/307] OP-2019 - removed unwanted host --- openpype/hooks/pre_foundry_apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hooks/pre_foundry_apps.py b/openpype/hooks/pre_foundry_apps.py index 7df1a6a833..85f68c6b60 100644 --- a/openpype/hooks/pre_foundry_apps.py +++ b/openpype/hooks/pre_foundry_apps.py @@ -13,7 +13,7 @@ class LaunchFoundryAppsWindows(PreLaunchHook): # Should be as last hook because must change launch arguments to string order = 1000 - app_groups = ["nuke", "nukex", "hiero", "nukestudio", "photoshop"] + app_groups = ["nuke", "nukex", "hiero", "nukestudio"] platforms = ["windows"] def execute(self): From 1154f61ac1a0d9382b3f9cd4304d77e2be9c65da Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 19 Nov 2021 12:26:28 +0100 Subject: [PATCH 048/307] OP-2019 - added details into documentation --- tests/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/README.md b/tests/README.md index 6317b2ab3c..d0578f8059 100644 --- a/tests/README.md +++ b/tests/README.md @@ -14,12 +14,12 @@ How to run: ---------- - single test class could be run by PyCharm and its pytest runner directly - OR -- use Openpype command 'runtests' from command line --- `${OPENPYPE_ROOT}/start.py runtests` +- use Openpype command 'runtests' from command line (`.venv` in ${OPENPYPE_ROOT} must be activated to use configured Python!) +-- `${OPENPYPE_ROOT}/python start.py runtests` By default, this command will run all tests in ${OPENPYPE_ROOT}/tests. Specific location could be provided to this command as an argument, either as absolute path, or relative path to ${OPENPYPE_ROOT}. -(eg. `${OPENPYPE_ROOT}/start.py runtests ../tests/integration`) will trigger only tests in `integration` folder. +(eg. `${OPENPYPE_ROOT}/python start.py runtests ../tests/integration`) will trigger only tests in `integration` folder. See `${OPENPYPE_ROOT}/cli.py:runtests` for other arguments. From 9c60961a61e97a5a5d2bd449c06ccc84ec7aba76 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 19 Nov 2021 15:57:10 +0100 Subject: [PATCH 049/307] converting to class based code --- .../openpype_flame_to_ftrack/modules/app.py | 513 ++++++++ .../modules/ftrack_lib.py | 420 +++++++ .../openpype_flame_to_ftrack/modules/utils.py | 174 +++ .../openpype_flame_to_ftrack.py | 1055 ----------------- 4 files changed, 1107 insertions(+), 1055 deletions(-) create mode 100644 openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app.py create mode 100644 openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py create mode 100644 openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/utils.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app.py new file mode 100644 index 0000000000..a13df25035 --- /dev/null +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app.py @@ -0,0 +1,513 @@ +import os +from PySide2 import QtWidgets, QtCore +import uiwidgets +import flame + +from .ftrack_lib import ( + maintained_ftrack_session, + FtrackEntityOperator, + FtrackComponentCreator +) +from .utils import ( + get_config, + set_config, + get_all_task_types, + make_temp_dir, + export_thumbnail, + export_video, + timecode_to_frames +) +from pprint import pformat + + +class FlameToFtrackPanel(QtWidgets.QWidget()): + temp_data_dir = None + project_entity = None + task_types = {} + all_task_types = {} + processed_components = [] + + # TreeWidget + columns = { + "Sequence name": { + "columnWidth": 200, + "order": 0 + }, + "Shot name": { + "columnWidth": 200, + "order": 1 + }, + "Clip duration": { + "columnWidth": 100, + "order": 2 + }, + "Shot description": { + "columnWidth": 500, + "order": 3 + }, + "Task description": { + "columnWidth": 500, + "order": 4 + }, + } + + def __init__(self, selection, *args, **kwargs): + super(FlameToFtrackPanel, self).__init__(*args, **kwargs) + + self.selection = selection + # creating ui + self.setMinimumSize(1500, 600) + self.setWindowTitle('Sequence Shots to Ftrack') + self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setStyleSheet('background-color: #313131') + + + self._create_tree_widget() + self._set_sequence_params() + + self._create_project_widget() + + self._generate_widgets() + + self._generate_layouts() + + self._timeline_info() + + self._fix_resolution() + + def _generate_widgets(self): + with get_config("main") as cfg_d: + self._create_task_type_widget(cfg_d) + + # input fields + self.shot_name_label = uiwidgets.FlameLabel( + 'Shot name template', 'normal', self) + self.shot_name_template_input = uiwidgets.FlameLineEdit( + cfg_d["shot_name_template"], self) + + self.hierarchy_label = uiwidgets.FlameLabel( + 'Parents template', 'normal', self) + self.hierarchy_template_input = uiwidgets.FlameLineEdit( + cfg_d["hierarchy_template"], self) + + self.start_frame_label = uiwidgets.FlameLabel( + 'Workfile start frame', 'normal', self) + self.start_frame_input = uiwidgets.FlameLineEdit( + cfg_d["workfile_start_frame"], self) + + self.handles_label = uiwidgets.FlameLabel( + 'Shot handles', 'normal', self) + self.handles_input = uiwidgets.FlameLineEdit( + cfg_d["shot_handles"], self) + + self.width_label = uiwidgets.FlameLabel( + 'Sequence width', 'normal', self) + self.width_input = uiwidgets.FlameLineEdit( + str(self.seq_width), self) + + self.height_label = uiwidgets.FlameLabel( + 'Sequence height', 'normal', self) + self.height_input = uiwidgets.FlameLineEdit( + str(self.seq_height), self) + + self.pixel_aspect_label = uiwidgets.FlameLabel( + 'Pixel aspect ratio', 'normal', self) + self.pixel_aspect_input = uiwidgets.FlameLineEdit( + str(1.00), self) + + self.fps_label = uiwidgets.FlameLabel( + 'Frame rate', 'normal', self) + self.fps_input = uiwidgets.FlameLineEdit( + str(self.fps), self) + + # Button + self.select_all_btn = uiwidgets.FlameButton( + 'Select All', self.select_all, self) + + self.remove_temp_data_btn = uiwidgets.FlameButton( + 'Remove temp data', self.remove_temp_data, self) + + self.ftrack_send_btn = uiwidgets.FlameButton( + 'Send to Ftrack', self._send_to_ftrack, self) + + def _generate_layouts(self): + # left props + v_shift = 0 + prop_layout_l = QtWidgets.QGridLayout() + prop_layout_l.setHorizontalSpacing(30) + if self.project_selector_enabled: + prop_layout_l.addWidget(self.project_select_label, v_shift, 0) + prop_layout_l.addWidget(self.project_select_input, v_shift, 1) + v_shift += 1 + prop_layout_l.addWidget(self.shot_name_label, (v_shift + 0), 0) + prop_layout_l.addWidget( + self.shot_name_template_input, (v_shift + 0), 1) + prop_layout_l.addWidget(self.hierarchy_label, (v_shift + 1), 0) + prop_layout_l.addWidget( + self.hierarchy_template_input, (v_shift + 1), 1) + prop_layout_l.addWidget(self.start_frame_label, (v_shift + 2), 0) + prop_layout_l.addWidget(self.start_frame_input, (v_shift + 2), 1) + prop_layout_l.addWidget(self.handles_label, (v_shift + 3), 0) + prop_layout_l.addWidget(self.handles_input, (v_shift + 3), 1) + prop_layout_l.addWidget(self.task_type_label, (v_shift + 4), 0) + prop_layout_l.addWidget( + self.task_type_input, (v_shift + 4), 1) + + # right props + prop_widget_r = QtWidgets.QWidget(self) + prop_layout_r = QtWidgets.QGridLayout(prop_widget_r) + prop_layout_r.setHorizontalSpacing(30) + prop_layout_r.setAlignment( + QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + prop_layout_r.setContentsMargins(0, 0, 0, 0) + prop_layout_r.addWidget(self.width_label, 1, 0) + prop_layout_r.addWidget(self.width_input, 1, 1) + prop_layout_r.addWidget(self.height_label, 2, 0) + prop_layout_r.addWidget(self.height_input, 2, 1) + prop_layout_r.addWidget(self.pixel_aspect_label, 3, 0) + prop_layout_r.addWidget(self.pixel_aspect_input, 3, 1) + prop_layout_r.addWidget(self.fps_label, 4, 0) + prop_layout_r.addWidget(self.fps_input, 4, 1) + + # prop layout + prop_main_layout = QtWidgets.QHBoxLayout() + prop_main_layout.addLayout(prop_layout_l, 1) + prop_main_layout.addSpacing(20) + prop_main_layout.addWidget(prop_widget_r, 1) + + # buttons layout + hbox = QtWidgets.QHBoxLayout() + hbox.addWidget(self.remove_temp_data_btn) + hbox.addWidget(self.select_all_btn) + hbox.addWidget(self.ftrack_send_btn) + + # put all layouts together + main_frame = QtWidgets.QVBoxLayout(self) + main_frame.setMargin(20) + main_frame.addLayout(prop_main_layout) + main_frame.addWidget(self.tree) + main_frame.addLayout(hbox) + + def _set_sequence_params(self): + for select in self.selection: + self.seq_height = select.height + self.seq_width = select.width + self.fps = float(str(select.frame_rate)[:-4]) + break + + def _create_task_type_widget(self, cfg_d): + self.task_types = get_all_task_types(self.project_entity) + + self.task_type_label = uiwidgets.FlameLabel( + 'Create Task (type)', 'normal', self) + self.task_type_input = uiwidgets.FlamePushButtonMenu( + cfg_d["create_task_type"], self.task_types.keys(), self) + + def _create_project_widget(self): + + with maintained_ftrack_session() as session: + # get project name from flame current project + self.project_name = flame.project.current_project.name + + # get project from ftrack - + # ftrack project name has to be the same as flame project! + query = 'Project where full_name is "{}"'.format(self.project_name) + + # globally used variables + self.project_entity = session.query(query).first() + + self.project_selector_enabled = bool(not self.project_entity) + + if self.project_selector_enabled: + self.all_projects = session.query( + "Project where status is active").all() + self.project_entity = self.all_projects[0] + project_names = [p["full_name"] for p in self.all_projects] + self.all_task_types = {p["full_name"]: get_all_task_types( + p).keys() for p in self.all_projects} + self.project_select_label = uiwidgets.FlameLabel( + 'Select Ftrack project', 'normal', self) + self.project_select_input = uiwidgets.FlamePushButtonMenu( + self.project_entity["full_name"], project_names, self) + self.project_select_input.selection_changed.connect( + self._on_project_changed) + + def _create_tree_widget(self): + ordered_column_labels = self.columns.keys() + for _name, _value in self.columns.items(): + ordered_column_labels.pop(_value["order"]) + ordered_column_labels.insert(_value["order"], _name) + + self.tree = uiwidgets.FlameTreeWidget(ordered_column_labels, self) + + # Allow multiple items in tree to be selected + self.tree.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) + + # Set tree column width + for _name, _val in self.columns.items(): + self.tree.setColumnWidth( + _val["order"], + _val["columnWidth"] + ) + + # Prevent weird characters when shrinking tree columns + self.tree.setTextElideMode(QtCore.Qt.ElideNone) + + def _send_to_ftrack(self): + # resolve active project and add it to self.project_entity + if self.project_selector_enabled: + selected_project_name = self.project_select_input.text() + self.project_entity = next( + (p for p in self.all_projects + if p["full_name"] in selected_project_name), + None + ) + + _cfg_data_back = {} + + # get shot name template from gui input + shot_name_template = self.shot_name_template_input.text() + + # get hierarchy from gui input + hierarchy_text = self.hierarchy_template_input.text() + + # get hanldes from gui input + handles = self.handles_input.text() + + # get frame start from gui input + frame_start = int(self.start_frame_input.text()) + + # get task type from gui input + task_type = self.task_type_input.text() + + # get resolution from gui inputs + width = self.width_input.text() + height = self.height_input.text() + pixel_aspect = self.pixel_aspect_input.text() + fps = self.fps_input.text() + + _cfg_data_back = { + "shot_name_template": shot_name_template, + "workfile_start_frame": str(frame_start), + "shot_handles": handles, + "hierarchy_template": hierarchy_text, + "create_task_type": task_type + } + + # add cfg data back to settings.ini + set_config(_cfg_data_back, "main") + + with maintained_ftrack_session() as session: + print("Ftrack session is: {}".format(session)) + + entity_operator = FtrackEntityOperator( + session, self.project_entity) + component_creator = FtrackComponentCreator(session) + + self.generate_temp_data({ + "nbHandles": handles + }) + + temp_files = os.listdir(self.temp_data_dir) + thumbnails = [f for f in temp_files if "jpg" in f] + videos = [f for f in temp_files if "mov" in f] + + print(temp_files) + print(thumbnails) + print(videos) + + # Get all selected items from treewidget + for item in self.tree.selectedItems(): + # frame ranges + frame_duration = int(item.text(2)) + frame_end = frame_start + frame_duration + + # description + shot_description = item.text(3) + task_description = item.text(4) + + # other + sequence_name = item.text(0) + shot_name = item.text(1) + + # get component files + thumb_f = next((f for f in thumbnails if shot_name in f), None) + video_f = next((f for f in videos if shot_name in f), None) + thumb_fp = os.path.join(self.temp_data_dir, thumb_f) + video_fp = os.path.join(self.temp_data_dir, video_f) + print(thumb_fp) + print(video_fp) + + print("processed comps: {}".format(self.processed_components)) + processed = False + if thumb_f not in self.processed_components: + self.processed_components.append(thumb_f) + else: + processed = True + + print("processed: {}".format(processed)) + + # populate full shot info + shot_attributes = { + "sequence": sequence_name, + "shot": shot_name, + "task": task_type + } + + # format shot name template + _shot_name = shot_name_template.format(**shot_attributes) + + # format hierarchy template + _hierarchy_text = hierarchy_text.format(**shot_attributes) + print(_hierarchy_text) + + # solve parents + parents = entity_operator.create_parents(_hierarchy_text) + print(parents) + + # obtain shot parents entities + _parent = None + for _name, _type in parents: + p_entity = entity_operator.get_ftrack_entity( + session, + _type, + _name, + _parent + ) + print(p_entity) + _parent = p_entity + + # obtain shot ftrack entity + f_s_entity = entity_operator.get_ftrack_entity( + session, + "Shot", + _shot_name, + _parent + ) + print("Shot entity is: {}".format(f_s_entity)) + + if not processed: + # first create thumbnail and get version entity + assetversion_entity = component_creator.create_comonent( + f_s_entity, { + "file_path": thumb_fp + } + ) + + # secondly add video to version entity + component_creator.create_comonent( + f_s_entity, { + "file_path": video_fp, + "duration": frame_duration, + "handles": int(handles), + "fps": float(fps) + }, assetversion_entity + ) + + # create custom attributtes + custom_attrs = { + "frameStart": frame_start, + "frameEnd": frame_end, + "handleStart": int(handles), + "handleEnd": int(handles), + "resolutionWidth": int(width), + "resolutionHeight": int(height), + "pixelAspect": float(pixel_aspect), + "fps": float(fps) + } + + # update custom attributes on shot entity + for key in custom_attrs: + f_s_entity['custom_attributes'][key] = custom_attrs[key] + + task_entity = entity_operator.create_task( + task_type, self.task_types, f_s_entity) + + # Create notes. + user = session.query( + "User where username is \"{}\"".format(session.api_user) + ).first() + + f_s_entity.create_note(shot_description, author=user) + + if task_description: + task_entity.create_note(task_description, user) + + entity_operator.commit() + + component_creator.close() + + def _fix_resolution(self): + # Center window in linux + resolution = QtWidgets.QDesktopWidget().screenGeometry() + self.move( + (resolution.width() / 2) - (self.frameSize().width() / 2), + (resolution.height() / 2) - (self.frameSize().height() / 2)) + + def _on_project_changed(self): + task_types = self.all_task_types[self.project_name] + self.task_type_input.set_menu_options(task_types) + + def _timeline_info(self): + # identificar as informacoes dos segmentos na timeline + for sequence in self.selection: + frame_rate = float(str(sequence.frame_rate)[:-4]) + for ver in sequence.versions: + for tracks in ver.tracks: + for segment in tracks.segments: + print(segment.attributes) + if str(segment.name)[1:-1] == "": + continue + # get clip frame duration + record_duration = str(segment.record_duration)[1:-1] + clip_duration = timecode_to_frames( + record_duration, frame_rate) + + # populate shot source metadata + shot_description = "" + for attr in ["tape_name", "source_name", "head", + "tail", "file_path"]: + if not hasattr(segment, attr): + continue + _value = getattr(segment, attr) + _label = attr.replace("_", " ").capitalize() + row = "{}: {}\n".format(_label, _value) + shot_description += row + + # Add timeline segment to tree + QtWidgets.QTreeWidgetItem(self.tree, [ + str(sequence.name)[1:-1], # seq + str(segment.name)[1:-1], # shot + str(clip_duration), # clip duration + shot_description, # shot description + str(segment.comment)[1:-1] # task description + ]).setFlags( + QtCore.Qt.ItemIsEditable + | QtCore.Qt.ItemIsEnabled + | QtCore.Qt.ItemIsSelectable + ) + + # Select top item in tree + self.tree.setCurrentItem(self.tree.topLevelItem(0)) + + def select_all(self, ): + self.tree.selectAll() + + def remove_temp_data(self, ): + import shutil + + if self.temp_data_dir: + shutil.rmtree(self.temp_data_dir) + self.temp_data_dir = None + + def generate_temp_data(self, change_preset_data): + if self.temp_data_dir: + return True + + with make_temp_dir() as tempdir_path: + for seq in self.selection: + export_thumbnail(seq, tempdir_path, change_preset_data) + export_video(seq, tempdir_path, change_preset_data) + self.temp_data_dir = tempdir_path + break diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py new file mode 100644 index 0000000000..8ea1cfc775 --- /dev/null +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py @@ -0,0 +1,420 @@ +import os +import sys +import six +import re +from contextlib import contextmanager + +# Fill following constants or set them via environment variable +FTRACK_MODULE_PATH = None +FTRACK_API_KEY = None +FTRACK_API_USER = None +FTRACK_SERVER = None + + +def import_ftrack_api(): + try: + import ftrack_api + return ftrack_api + except ImportError: + import sys + ftrk_m_p = FTRACK_MODULE_PATH or os.getenv("FTRACK_MODULE_PATH") + sys.path.append(ftrk_m_p) + import ftrack_api + return ftrack_api + + +@contextmanager +def maintained_ftrack_session(): + import os + ftrack_api = import_ftrack_api() + + def validate_credentials(url, user, api): + first_validation = True + if not user: + print('- Ftrack Username is not set') + first_validation = False + if not api: + print('- Ftrack API key is not set') + first_validation = False + if not first_validation: + return False + + try: + session = ftrack_api.Session( + server_url=url, + api_user=user, + api_key=api + ) + session.close() + except Exception as _e: + print( + "Can't log into Ftrack with used credentials: {}".format( + _e) + ) + ftrack_cred = { + 'Ftrack server': str(url), + 'Username': str(user), + 'API key': str(api), + } + + item_lens = [len(key) + 1 for key in ftrack_cred] + justify_len = max(*item_lens) + for key, value in ftrack_cred.items(): + print('{} {}'.format((key + ':').ljust( + justify_len, ' '), value)) + return False + print( + 'Credentials Username: "{}", API key: "{}" are valid.'.format( + user, api) + ) + return True + + # fill your own credentials + url = FTRACK_SERVER or os.getenv("FTRACK_SERVER") or "" + user = FTRACK_API_USER or os.getenv("FTRACK_API_USER") or "" + api = FTRACK_API_KEY or os.getenv("FTRACK_API_KEY") or "" + + try: + assert validate_credentials(url, user, api), ( + "Ftrack credentials failed") + # open ftrack session + session = ftrack_api.Session( + server_url=url, + api_user=user, + api_key=api + ) + yield session + except Exception: + tp, value, tb = sys.exc_info() + six.reraise(tp, value, tb) + finally: + # close the session + session.close() + + +class FtrackComponentCreator: + default_location = "ftrack.server" + ftrack_locations = {} + + def __init__(self, session): + self.session = session + self._get_ftrack_location() + + def close(self): + self.ftrack_locations = {} + self.session = None + + def create_comonent(self, shot_entity, data, assetversion_entity=None): + self.shot_entity = shot_entity + location = self._get_ftrack_location() + + file_path = data["file_path"] + + # get extension + file = os.path.basename(file_path) + _n, ext = os.path.splitext(file) + + name = "ftrackreview-mp4" if "mov" in ext else "thumbnail" + + component_data = { + "name": name, + "file_path": file_path, + "file_type": ext, + "location": location + } + + if name == "ftrackreview-mp4": + duration = data["duration"] + handles = data["handles"] + fps = data["fps"] + component_data.update({ + "metadata": {'ftr_meta': json.dumps({ + 'frameIn': int(0), + 'frameOut': int(duration + (handles * 2)), + 'frameRate': float(fps)})} + }) + + if not assetversion_entity: + # get assettype entity from session + assettype_entity = self._get_assettype({"short": "reference"}) + + # get or create asset entity from session + asset_entity = self._get_asset({ + "name": "plateReference", + "type": assettype_entity, + "parent": self.shot_entity + }) + + # get or create assetversion entity from session + assetversion_entity = self._get_assetversion({ + "version": 1, + "asset": asset_entity + }) + + # get or create component entity + self._set_component(component_data, { + "name": name, + "version": assetversion_entity, + }) + + return assetversion_entity + + def _overwrite_members(self, entity, data): + origin_location = self._get_ftrack_location("ftrack.origin") + location = data.pop("location") + + # Removing existing members from location + components = list(entity.get("members", [])) + components += [entity] + for component in components: + for loc in component["component_locations"]: + if location["id"] == loc["location_id"]: + location.remove_component( + component, recursive=False + ) + + # Deleting existing members on component entity + for member in entity.get("members", []): + self.session.delete(member) + del(member) + + self._commit() + + # Reset members in memory + if "members" in entity.keys(): + entity["members"] = [] + + entity["file_type"] = data["file_type"] + + origin_location.add_component( + entity, data["file_path"] + ) + + # Add components to location. + location.add_component( + entity, origin_location, recursive=True) + + def _get_assettype(self, data): + return self.session.query( + self._query("AssetType", data)).first() + + def _set_component(self, comp_data, base_data): + component_metadata = comp_data.pop("metadata", {}) + + component_entity = self.session.query( + self._query("Component", base_data) + ).first() + + if component_entity: + # overwrite existing members in component enity + # - get data for member from `ftrack.origin` location + self._overwrite_members(component_entity, comp_data) + + # Adding metadata + existing_component_metadata = component_entity["metadata"] + existing_component_metadata.update(component_metadata) + component_entity["metadata"] = existing_component_metadata + return + + assetversion_entity = base_data["version"] + location = comp_data.pop("location") + + component_entity = assetversion_entity.create_component( + comp_data["file_path"], + data=comp_data, + location=location + ) + + # Adding metadata + existing_component_metadata = component_entity["metadata"] + existing_component_metadata.update(component_metadata) + component_entity["metadata"] = existing_component_metadata + + if comp_data["name"] == "thumbnail": + self.shot_entity["thumbnail_id"] = component_entity["id"] + assetversion_entity["thumbnail_id"] = component_entity["id"] + + self._commit() + + def _get_asset(self, data): + # first find already created + asset_entity = self.session.query( + self._query("Asset", data) + ).first() + + if asset_entity: + return asset_entity + + asset_entity = self.session.create("Asset", data) + + # _commit if created + self._commit() + + return asset_entity + + def _get_assetversion(self, data): + assetversion_entity = self.session.query( + self._query("AssetVersion", data) + ).first() + + if assetversion_entity: + return assetversion_entity + + assetversion_entity = self.session.create("AssetVersion", data) + + # _commit if created + self._commit() + + return assetversion_entity + + def _commit(self): + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + self.session._configure_locations() + six.reraise(tp, value, tb) + + def _get_ftrack_location(self, name=None): + name = name or self.default_location + + if name in self.ftrack_locations: + return self.ftrack_locations[name] + + location = self.session.query( + 'Location where name is "{}"'.format(name) + ).one() + self.ftrack_locations[name] = location + return location + + def _query(self, entitytype, data): + """ Generate a query expression from data supplied. + + If a value is not a string, we'll add the id of the entity to the + query. + + Args: + entitytype (str): The type of entity to query. + data (dict): The data to identify the entity. + exclusions (list): All keys to exclude from the query. + + Returns: + str: String query to use with "session.query" + """ + queries = [] + if sys.version_info[0] < 3: + for key, value in data.iteritems(): + if not isinstance(value, (basestring, int)): + print("value: {}".format(value)) + if "id" in value.keys(): + queries.append( + "{0}.id is \"{1}\"".format(key, value["id"]) + ) + else: + queries.append("{0} is \"{1}\"".format(key, value)) + else: + for key, value in data.items(): + if not isinstance(value, (str, int)): + print("value: {}".format(value)) + if "id" in value.keys(): + queries.append( + "{0}.id is \"{1}\"".format(key, value["id"]) + ) + else: + queries.append("{0} is \"{1}\"".format(key, value)) + + query = ( + "select id from " + entitytype + " where " + " and ".join(queries) + ) + print(query) + return query + + +class FtrackEntityOperator: + def __init__(self, session, project_entity): + self.session = session + self.project_entity = project_entity + + def commit(self): + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + self.session._configure_locations() + six.reraise(tp, value, tb) + + def create_ftrack_entity(self, session, type, name, parent=None): + parent = parent or self.project_entity + entity = session.create(type, { + 'name': name, + 'parent': parent + }) + try: + session.commit() + except Exception: + tp, value, tb = sys.exc_info() + session.rollback() + session._configure_locations() + six.reraise(tp, value, tb) + return entity + + def get_ftrack_entity(self, session, type, name, parent): + query = '{} where name is "{}" and project_id is "{}"'.format( + type, name, self.project_entity["id"]) + + try: + entity = session.query(query).one() + except Exception: + entity = None + + # if entity doesnt exist then create one + if not entity: + entity = self.create_ftrack_entity( + session, + type, + name, + parent + ) + + return entity + + def create_parents(self, template): + parents = [] + t_split = template.split("/") + replace_patern = re.compile(r"(\[.*\])") + type_patern = re.compile(r"\[(.*)\]") + + for t_s in t_split: + match_type = type_patern.findall(t_s) + if not match_type: + raise Exception(( + "Missing correct type flag in : {}" + "/n Example: name[Type]").format( + t_s) + ) + new_name = re.sub(replace_patern, "", t_s) + f_type = match_type.pop() + + parents.append((new_name, f_type)) + + return parents + + def create_task(self, task_type, task_types, parent): + existing_task = [ + child for child in parent['children'] + if child.entity_type.lower() == 'task' + if child['name'].lower() in task_type.lower() + ] + + if existing_task: + return existing_task.pop() + + task = self.session.create('Task', { + "name": task_type.lower(), + "parent": parent + }) + task["type"] = task_types[task_type] + + return task diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/utils.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/utils.py new file mode 100644 index 0000000000..2aa6577325 --- /dev/null +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/utils.py @@ -0,0 +1,174 @@ +import os +import io +import ConfigParser as CP +from xml.etree import ElementTree as ET +from contextlib import contextmanager + +PLUGIN_DIR = os.path.dirname(os.path.dirname(__file__)) +EXPORT_PRESETS_DIR = os.path.join(PLUGIN_DIR, "export_preset") + +CONFIG_DIR = os.path.join(os.path.expanduser( + "~/.openpype"), "openpype_flame_to_ftrack") + + +@contextmanager +def make_temp_dir(): + import tempfile + + try: + dirpath = tempfile.mkdtemp() + + yield dirpath + + except IOError as _error: + raise IOError("Not able to create temp dir file: {}".format(_error)) + + finally: + pass + + +@contextmanager +def get_config(section=None): + cfg_file_path = os.path.join(CONFIG_DIR, "settings.ini") + + # create config dir + if not os.path.exists(CONFIG_DIR): + print("making dirs at: `{}`".format(CONFIG_DIR)) + os.makedirs(CONFIG_DIR, mode=0o777) + + # write default data to settings.ini + if not os.path.exists(cfg_file_path): + default_cfg = cfg_default() + config = CP.RawConfigParser() + config.readfp(io.BytesIO(default_cfg)) + with open(cfg_file_path, 'wb') as cfg_file: + config.write(cfg_file) + + try: + config = CP.RawConfigParser() + config.read(cfg_file_path) + if section: + _cfg_data = { + k: v + for s in config.sections() + for k, v in config.items(s) + if s == section + } + else: + _cfg_data = {s: dict(config.items(s)) for s in config.sections()} + + yield _cfg_data + + except IOError as _error: + raise IOError('Not able to read settings.ini file: {}'.format(_error)) + + finally: + pass + + +def set_config(cfg_data, section=None): + cfg_file_path = os.path.join(CONFIG_DIR, "settings.ini") + + config = CP.RawConfigParser() + config.read(cfg_file_path) + + try: + if not section: + for section in cfg_data: + for key, value in cfg_data[section].items(): + config.set(section, key, value) + else: + for key, value in cfg_data.items(): + config.set(section, key, value) + + with open(cfg_file_path, 'wb') as cfg_file: + config.write(cfg_file) + + except IOError as _error: + raise IOError('Not able to write settings.ini file: {}'.format(_error)) + + +def cfg_default(): + return """ +[main] +workfile_start_frame = 1001 +shot_handles = 0 +shot_name_template = {sequence}_{shot} +hierarchy_template = shots[Folder]/{sequence}[Sequence] +create_task_type = Compositing +""" + + +def get_all_task_types(project_entity): + tasks = {} + proj_template = project_entity['project_schema'] + temp_task_types = proj_template['_task_type_schema']['types'] + + for type in temp_task_types: + if type['name'] not in tasks: + tasks[type['name']] = type + + return tasks + + +def configure_preset(file_path, data): + split_fp = os.path.splitext(file_path) + new_file_path = split_fp[0] + "_tmp" + split_fp[-1] + with open(file_path, "r") as datafile: + tree = ET.parse(datafile) + for key, value in data.items(): + for element in tree.findall(".//{}".format(key)): + print(element) + element.text = str(value) + tree.write(new_file_path) + + return new_file_path + + +def export_thumbnail(sequence, tempdir_path, data): + import flame + export_preset = os.path.join( + EXPORT_PRESETS_DIR, + "openpype_seg_thumbnails_jpg.xml" + ) + new_path = configure_preset(export_preset, data) + poster_frame_exporter = flame.PyExporter() + poster_frame_exporter.foreground = True + poster_frame_exporter.export(sequence, new_path, tempdir_path) + + +def export_video(sequence, tempdir_path, data): + import flame + export_preset = os.path.join( + EXPORT_PRESETS_DIR, + "openpype_seg_video_h264.xml" + ) + new_path = configure_preset(export_preset, data) + poster_frame_exporter = flame.PyExporter() + poster_frame_exporter.foreground = True + poster_frame_exporter.export(sequence, new_path, tempdir_path) + + +def timecode_to_frames(timecode, framerate): + def _seconds(value): + if isinstance(value, str): + _zip_ft = zip((3600, 60, 1, 1 / framerate), value.split(':')) + return sum(f * float(t) for f, t in _zip_ft) + elif isinstance(value, (int, float)): + return value / framerate + return 0 + + def _frames(seconds): + return seconds * framerate + + def tc_to_frames(_timecode, start=None): + return _frames(_seconds(_timecode) - _seconds(start)) + + if '+' in timecode: + timecode = timecode.replace('+', ':') + elif '#' in timecode: + timecode = timecode.replace('#', ':') + + frames = int(round(tc_to_frames(timecode, start='00:00:00:00'))) + + return frames diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index daab7c1754..bfdaf75385 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -1,1070 +1,15 @@ from __future__ import print_function import os import sys -import re import json -from PySide2 import QtWidgets, QtCore -from pprint import pformat -from contextlib import contextmanager -from xml.etree import ElementTree as ET -import ConfigParser as CP -import io -# Fill following constants or set them via environment variable -FTRACK_MODULE_PATH = None -FTRACK_API_KEY = None -FTRACK_API_USER = None -FTRACK_SERVER = None - -TEMP_DIR_DATA_PATH = None -F_PROJ_ENTITY = None -COMPONENTS_DONE = [] SCRIPT_DIR = os.path.dirname(__file__) PACKAGE_DIR = os.path.join(SCRIPT_DIR, "modules") -EXPORT_PRESETS_DIR = os.path.join(SCRIPT_DIR, "export_preset") -CONFIG_DIR = os.path.join(os.path.expanduser( - "~/.openpype"), "openpype_flame_to_ftrack") sys.path.append(PACKAGE_DIR) -def import_ftrack_api(): - try: - import ftrack_api - return ftrack_api - except ImportError: - import sys - ftrk_m_p = FTRACK_MODULE_PATH or os.getenv("FTRACK_MODULE_PATH") - sys.path.append(ftrk_m_p) - import ftrack_api - return ftrack_api - - -@contextmanager -def maintained_ftrack_session(): - import os - ftrack_api = import_ftrack_api() - - def validate_credentials(url, user, api): - first_validation = True - if not user: - print('- Ftrack Username is not set') - first_validation = False - if not api: - print('- Ftrack API key is not set') - first_validation = False - if not first_validation: - return False - - try: - session = ftrack_api.Session( - server_url=url, - api_user=user, - api_key=api - ) - session.close() - except Exception as _e: - print( - "Can't log into Ftrack with used credentials: {}".format( - _e) - ) - ftrack_cred = { - 'Ftrack server': str(url), - 'Username': str(user), - 'API key': str(api), - } - - item_lens = [len(key) + 1 for key in ftrack_cred] - justify_len = max(*item_lens) - for key, value in ftrack_cred.items(): - print('{} {}'.format((key + ':').ljust( - justify_len, ' '), value)) - return False - print( - 'Credentials Username: "{}", API key: "{}" are valid.'.format( - user, api) - ) - return True - - # fill your own credentials - url = FTRACK_SERVER or os.getenv("FTRACK_SERVER") or "" - user = FTRACK_API_USER or os.getenv("FTRACK_API_USER") or "" - api = FTRACK_API_KEY or os.getenv("FTRACK_API_KEY") or "" - - try: - assert validate_credentials(url, user, api), ( - "Ftrack credentials failed") - # open ftrack session - session = ftrack_api.Session( - server_url=url, - api_user=user, - api_key=api - ) - yield session - except Exception: - tp, value, tb = sys.exc_info() - six.reraise(tp, value, tb) - finally: - # close the session - session.close() - - -@contextmanager -def make_temp_dir(): - import tempfile - - try: - dirpath = tempfile.mkdtemp() - - yield dirpath - - except IOError as _error: - raise IOError("Not able to create temp dir file: {}".format(_error)) - - finally: - pass - - -@contextmanager -def get_config(section=None): - cfg_file_path = os.path.join(CONFIG_DIR, "settings.ini") - - # create config dir - if not os.path.exists(CONFIG_DIR): - print("making dirs at: `{}`".format(CONFIG_DIR)) - os.makedirs(CONFIG_DIR, mode=0o777) - - # write default data to settings.ini - if not os.path.exists(cfg_file_path): - default_cfg = cfg_default() - config = CP.RawConfigParser() - config.readfp(io.BytesIO(default_cfg)) - with open(cfg_file_path, 'wb') as cfg_file: - config.write(cfg_file) - - try: - config = CP.RawConfigParser() - config.read(cfg_file_path) - if section: - _cfg_data = { - k: v - for s in config.sections() - for k, v in config.items(s) - if s == section - } - else: - _cfg_data = {s: dict(config.items(s)) for s in config.sections()} - - yield _cfg_data - - except IOError as _error: - raise IOError('Not able to read settings.ini file: {}'.format(_error)) - - finally: - pass - - -def set_config(cfg_data, section=None): - cfg_file_path = os.path.join(CONFIG_DIR, "settings.ini") - - config = CP.RawConfigParser() - config.read(cfg_file_path) - - try: - if not section: - for section in cfg_data: - for key, value in cfg_data[section].items(): - config.set(section, key, value) - else: - for key, value in cfg_data.items(): - config.set(section, key, value) - - with open(cfg_file_path, 'wb') as cfg_file: - config.write(cfg_file) - - except IOError as _error: - raise IOError('Not able to write settings.ini file: {}'.format(_error)) - - -def cfg_default(): - return """ -[main] -workfile_start_frame = 1001 -shot_handles = 0 -shot_name_template = {sequence}_{shot} -hierarchy_template = shots[Folder]/{sequence}[Sequence] -create_task_type = Compositing -""" - - -def get_all_task_types(project_entity): - tasks = {} - proj_template = project_entity['project_schema'] - temp_task_types = proj_template['_task_type_schema']['types'] - - for type in temp_task_types: - if type['name'] not in tasks: - tasks[type['name']] = type - - return tasks - - -def configure_preset(file_path, data): - split_fp = os.path.splitext(file_path) - new_file_path = split_fp[0] + "_tmp" + split_fp[-1] - with open(file_path, "r") as datafile: - tree = ET.parse(datafile) - for key, value in data.items(): - for element in tree.findall(".//{}".format(key)): - print(element) - element.text = str(value) - tree.write(new_file_path) - - return new_file_path - - -def export_thumbnail(sequence, tempdir_path, data): - import flame - export_preset = os.path.join( - EXPORT_PRESETS_DIR, - "openpype_seg_thumbnails_jpg.xml" - ) - new_path = configure_preset(export_preset, data) - poster_frame_exporter = flame.PyExporter() - poster_frame_exporter.foreground = True - poster_frame_exporter.export(sequence, new_path, tempdir_path) - - -def export_video(sequence, tempdir_path, data): - import flame - export_preset = os.path.join( - EXPORT_PRESETS_DIR, - "openpype_seg_video_h264.xml" - ) - new_path = configure_preset(export_preset, data) - poster_frame_exporter = flame.PyExporter() - poster_frame_exporter.foreground = True - poster_frame_exporter.export(sequence, new_path, tempdir_path) - - -def timecode_to_frames(timecode, framerate): - def _seconds(value): - if isinstance(value, str): - _zip_ft = zip((3600, 60, 1, 1 / framerate), value.split(':')) - return sum(f * float(t) for f, t in _zip_ft) - elif isinstance(value, (int, float)): - return value / framerate - return 0 - - def _frames(seconds): - return seconds * framerate - - def tc_to_frames(_timecode, start=None): - return _frames(_seconds(_timecode) - _seconds(start)) - - if '+' in timecode: - timecode = timecode.replace('+', ':') - elif '#' in timecode: - timecode = timecode.replace('#', ':') - - frames = int(round(tc_to_frames(timecode, start='00:00:00:00'))) - - return frames - - -class FtrackComponentCreator: - default_location = "ftrack.server" - ftrack_locations = {} - - def __init__(self, session): - self.session = session - self._get_ftrack_location() - - def close(self): - self.ftrack_locations = {} - self.session = None - - def create_comonent(self, shot_entity, data, assetversion_entity=None): - self.shot_entity = shot_entity - location = self._get_ftrack_location() - - file_path = data["file_path"] - - # get extension - file = os.path.basename(file_path) - _n, ext = os.path.splitext(file) - - name = "ftrackreview-mp4" if "mov" in ext else "thumbnail" - - component_data = { - "name": name, - "file_path": file_path, - "file_type": ext, - "location": location - } - - if name == "ftrackreview-mp4": - duration = data["duration"] - handles = data["handles"] - fps = data["fps"] - component_data.update({ - "metadata": {'ftr_meta': json.dumps({ - 'frameIn': int(0), - 'frameOut': int(duration + (handles * 2)), - 'frameRate': float(fps)})} - }) - - if not assetversion_entity: - # get assettype entity from session - assettype_entity = self._get_assettype({"short": "reference"}) - - # get or create asset entity from session - asset_entity = self._get_asset({ - "name": "plateReference", - "type": assettype_entity, - "parent": self.shot_entity - }) - - # get or create assetversion entity from session - assetversion_entity = self._get_assetversion({ - "version": 1, - "asset": asset_entity - }) - - # get or create component entity - self._set_component(component_data, { - "name": name, - "version": assetversion_entity, - }) - - return assetversion_entity - - def _overwrite_members(self, entity, data): - origin_location = self._get_ftrack_location("ftrack.origin") - location = data.pop("location") - - # Removing existing members from location - components = list(entity.get("members", [])) - components += [entity] - for component in components: - for loc in component["component_locations"]: - if location["id"] == loc["location_id"]: - location.remove_component( - component, recursive=False - ) - - # Deleting existing members on component entity - for member in entity.get("members", []): - self.session.delete(member) - del(member) - - self._commit() - - # Reset members in memory - if "members" in entity.keys(): - entity["members"] = [] - - entity["file_type"] = data["file_type"] - - origin_location.add_component( - entity, data["file_path"] - ) - - # Add components to location. - location.add_component( - entity, origin_location, recursive=True) - - def _get_assettype(self, data): - return self.session.query( - self._query("AssetType", data)).first() - - def _set_component(self, comp_data, base_data): - component_metadata = comp_data.pop("metadata", {}) - - component_entity = self.session.query( - self._query("Component", base_data) - ).first() - - if component_entity: - # overwrite existing members in component enity - # - get data for member from `ftrack.origin` location - self._overwrite_members(component_entity, comp_data) - - # Adding metadata - existing_component_metadata = component_entity["metadata"] - existing_component_metadata.update(component_metadata) - component_entity["metadata"] = existing_component_metadata - return - - assetversion_entity = base_data["version"] - location = comp_data.pop("location") - - component_entity = assetversion_entity.create_component( - comp_data["file_path"], - data=comp_data, - location=location - ) - - # Adding metadata - existing_component_metadata = component_entity["metadata"] - existing_component_metadata.update(component_metadata) - component_entity["metadata"] = existing_component_metadata - - if comp_data["name"] == "thumbnail": - self.shot_entity["thumbnail_id"] = component_entity["id"] - assetversion_entity["thumbnail_id"] = component_entity["id"] - - self._commit() - - def _get_asset(self, data): - # first find already created - asset_entity = self.session.query( - self._query("Asset", data) - ).first() - - if asset_entity: - return asset_entity - - asset_entity = self.session.create("Asset", data) - - # _commit if created - self._commit() - - return asset_entity - - def _get_assetversion(self, data): - assetversion_entity = self.session.query( - self._query("AssetVersion", data) - ).first() - - if assetversion_entity: - return assetversion_entity - - assetversion_entity = self.session.create("AssetVersion", data) - - # _commit if created - self._commit() - - return assetversion_entity - - def _commit(self): - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - def _get_ftrack_location(self, name=None): - name = name or self.default_location - - if name in self.ftrack_locations: - return self.ftrack_locations[name] - - location = self.session.query( - 'Location where name is "{}"'.format(name) - ).one() - self.ftrack_locations[name] = location - return location - - def _query(self, entitytype, data): - """ Generate a query expression from data supplied. - - If a value is not a string, we'll add the id of the entity to the - query. - - Args: - entitytype (str): The type of entity to query. - data (dict): The data to identify the entity. - exclusions (list): All keys to exclude from the query. - - Returns: - str: String query to use with "session.query" - """ - queries = [] - if sys.version_info[0] < 3: - for key, value in data.iteritems(): - if not isinstance(value, (basestring, int)): - print("value: {}".format(value)) - if "id" in value.keys(): - queries.append( - "{0}.id is \"{1}\"".format(key, value["id"]) - ) - else: - queries.append("{0} is \"{1}\"".format(key, value)) - else: - for key, value in data.items(): - if not isinstance(value, (str, int)): - print("value: {}".format(value)) - if "id" in value.keys(): - queries.append( - "{0}.id is \"{1}\"".format(key, value["id"]) - ) - else: - queries.append("{0} is \"{1}\"".format(key, value)) - - query = ( - "select id from " + entitytype + " where " + " and ".join(queries) - ) - print(query) - return query - - -def main_window(selection): - import flame - import uiwidgets - - global TEMP_DIR_DATA_PATH - global F_PROJ_ENTITY - global COMPONENTS_DONE - - def _on_project_changed(project_name): - task_types = TASK_TYPES_ALL[project_name] - task_type_input.set_menu_options(task_types) - - def timeline_info(selection): - # identificar as informacoes dos segmentos na timeline - for sequence in selection: - frame_rate = float(str(sequence.frame_rate)[:-4]) - for ver in sequence.versions: - for tracks in ver.tracks: - for segment in tracks.segments: - print(segment.attributes) - if str(segment.name)[1:-1] == "": - continue - # get clip frame duration - record_duration = str(segment.record_duration)[1:-1] - clip_duration = timecode_to_frames( - record_duration, frame_rate) - - # populate shot source metadata - shot_description = "" - for attr in ["tape_name", "source_name", "head", - "tail", "file_path"]: - if not hasattr(segment, attr): - continue - _value = getattr(segment, attr) - _label = attr.replace("_", " ").capitalize() - row = "{}: {}\n".format(_label, _value) - shot_description += row - - # Add timeline segment to tree - QtWidgets.QTreeWidgetItem(tree, [ - str(sequence.name)[1:-1], # seq - str(segment.name)[1:-1], # shot - str(clip_duration), # clip duration - shot_description, # shot description - str(segment.comment)[1:-1] # task description - ]).setFlags( - QtCore.Qt.ItemIsEditable - | QtCore.Qt.ItemIsEnabled - | QtCore.Qt.ItemIsSelectable - ) - - # Select top item in tree - tree.setCurrentItem(tree.topLevelItem(0)) - - def select_all(): - - tree.selectAll() - - def remove_temp_data(): - import shutil - global TEMP_DIR_DATA_PATH - global COMPONENTS_DONE - - COMPONENTS_DONE = [] - - if TEMP_DIR_DATA_PATH: - shutil.rmtree(TEMP_DIR_DATA_PATH) - TEMP_DIR_DATA_PATH = None - - - def generate_temp_data(change_preset_data): - global TEMP_DIR_DATA_PATH - if TEMP_DIR_DATA_PATH: - return True - - with make_temp_dir() as tempdir_path: - for seq in selection: - export_thumbnail(seq, tempdir_path, change_preset_data) - export_video(seq, tempdir_path, change_preset_data) - TEMP_DIR_DATA_PATH = tempdir_path - break - - def send_to_ftrack(): - def create_ftrack_entity(session, type, name, parent=None): - global F_PROJ_ENTITY - parent = parent or F_PROJ_ENTITY - entity = session.create(type, { - 'name': name, - 'parent': parent - }) - try: - session.commit() - except Exception: - tp, value, tb = sys.exc_info() - session.rollback() - session._configure_locations() - six.reraise(tp, value, tb) - return entity - - def get_ftrack_entity(session, type, name, parent): - global F_PROJ_ENTITY - query = '{} where name is "{}" and project_id is "{}"'.format( - type, name, F_PROJ_ENTITY["id"]) - - try: - entity = session.query(query).one() - except Exception: - entity = None - - # if entity doesnt exist then create one - if not entity: - entity = create_ftrack_entity( - session, - type, - name, - parent - ) - - return entity - - def generate_parents_from_template(template): - parents = [] - t_split = template.split("/") - replace_patern = re.compile(r"(\[.*\])") - type_patern = re.compile(r"\[(.*)\]") - - for t_s in t_split: - match_type = type_patern.findall(t_s) - if not match_type: - raise Exception(( - "Missing correct type flag in : {}" - "/n Example: name[Type]").format( - t_s) - ) - new_name = re.sub(replace_patern, "", t_s) - f_type = match_type.pop() - - parents.append((new_name, f_type)) - - return parents - - def create_task(task_type, parent): - existing_task = [ - child for child in parent['children'] - if child.entity_type.lower() == 'task' - if child['name'].lower() in task_type.lower() - ] - - if existing_task: - return existing_task.pop() - - task = session.create('Task', { - "name": task_type.lower(), - "parent": parent - }) - task["type"] = F_PROJ_TASK_TYPES[task_type] - - return task - - ''' - ##################### start procedure - ''' - # resolve active project and add it to F_PROJ_ENTITY - if proj_selector: - selected_project_name = project_select_input.text() - F_PROJ_ENTITY = next( - (p for p in all_projects - if p["full_name"] in selected_project_name), - None - ) - - _cfg_data_back = {} - - # get shot name template from gui input - shot_name_template = shot_name_template_input.text() - - # get hierarchy from gui input - hierarchy_text = hierarchy_template_input.text() - - # get hanldes from gui input - handles = handles_input.text() - - # get frame start from gui input - frame_start = int(start_frame_input.text()) - - # get task type from gui input - task_type = task_type_input.text() - - # get resolution from gui inputs - width = width_input.text() - height = height_input.text() - pixel_aspect = pixel_aspect_input.text() - fps = fps_input.text() - - _cfg_data_back = { - "shot_name_template": shot_name_template, - "workfile_start_frame": str(frame_start), - "shot_handles": handles, - "hierarchy_template": hierarchy_text, - "create_task_type": task_type - } - - # add cfg data back to settings.ini - set_config(_cfg_data_back, "main") - - with maintained_ftrack_session() as session: - print("Ftrack session is: {}".format(session)) - - component_creator = FtrackComponentCreator(session) - - - generate_temp_data({ - "nbHandles": handles - }) - - temp_files = os.listdir(TEMP_DIR_DATA_PATH) - thumbnails = [f for f in temp_files if "jpg" in f] - videos = [f for f in temp_files if "mov" in f] - - print(temp_files) - print(thumbnails) - print(videos) - - # Get all selected items from treewidget - for item in tree.selectedItems(): - # frame ranges - frame_duration = int(item.text(2)) - frame_end = frame_start + frame_duration - - # description - shot_description = item.text(3) - task_description = item.text(4) - - # other - sequence_name = item.text(0) - shot_name = item.text(1) - - # get component files - thumb_f = next((f for f in thumbnails if shot_name in f), None) - video_f = next((f for f in videos if shot_name in f), None) - thumb_fp = os.path.join(TEMP_DIR_DATA_PATH, thumb_f) - video_fp = os.path.join(TEMP_DIR_DATA_PATH, video_f) - print(thumb_fp) - print(video_fp) - - print("processed comps: {}".format(COMPONENTS_DONE)) - processed = False - if thumb_f not in COMPONENTS_DONE: - COMPONENTS_DONE.append(thumb_f) - else: - processed = True - - print("processed: {}".format(processed)) - - # populate full shot info - shot_attributes = { - "sequence": sequence_name, - "shot": shot_name, - "task": task_type - } - - # format shot name template - _shot_name = shot_name_template.format(**shot_attributes) - - # format hierarchy template - _hierarchy_text = hierarchy_text.format(**shot_attributes) - print(_hierarchy_text) - - # solve parents - parents = generate_parents_from_template(_hierarchy_text) - print(parents) - - # obtain shot parents entities - _parent = None - for _name, _type in parents: - p_entity = get_ftrack_entity( - session, - _type, - _name, - _parent - ) - print(p_entity) - _parent = p_entity - - # obtain shot ftrack entity - f_s_entity = get_ftrack_entity( - session, - "Shot", - _shot_name, - _parent - ) - print("Shot entity is: {}".format(f_s_entity)) - - if not processed: - # first create thumbnail and get version entity - assetversion_entity = component_creator.create_comonent( - f_s_entity, { - "file_path": thumb_fp - } - ) - - # secondly add video to version entity - component_creator.create_comonent( - f_s_entity, { - "file_path": video_fp, - "duration": frame_duration, - "handles": int(handles), - "fps": float(fps) - }, assetversion_entity - ) - - # create custom attributtes - custom_attrs = { - "frameStart": frame_start, - "frameEnd": frame_end, - "handleStart": int(handles), - "handleEnd": int(handles), - "resolutionWidth": int(width), - "resolutionHeight": int(height), - "pixelAspect": float(pixel_aspect), - "fps": float(fps) - } - - # update custom attributes on shot entity - for key in custom_attrs: - f_s_entity['custom_attributes'][key] = custom_attrs[key] - - task_entity = create_task(task_type, f_s_entity) - - # Create notes. - user = session.query( - "User where username is \"{}\"".format(session.api_user) - ).first() - - f_s_entity.create_note(shot_description, author=user) - - if task_description: - task_entity.create_note(task_description, user) - - try: - session.commit() - except Exception: - tp, value, tb = sys.exc_info() - session.rollback() - session._configure_locations() - six.reraise(tp, value, tb) - - component_creator.close() - - # creating ui - window = QtWidgets.QWidget() - window.setMinimumSize(1500, 600) - window.setWindowTitle('Sequence Shots to Ftrack') - window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) - window.setAttribute(QtCore.Qt.WA_DeleteOnClose) - window.setStyleSheet('background-color: #313131') - - # Center window in linux - resolution = QtWidgets.QDesktopWidget().screenGeometry() - window.move((resolution.width() / 2) - (window.frameSize().width() / 2), - (resolution.height() / 2) - (window.frameSize().height() / 2)) - - # TreeWidget - columns = { - "Sequence name": { - "columnWidth": 200, - "order": 0 - }, - "Shot name": { - "columnWidth": 200, - "order": 1 - }, - "Clip duration": { - "columnWidth": 100, - "order": 2 - }, - "Shot description": { - "columnWidth": 500, - "order": 3 - }, - "Task description": { - "columnWidth": 500, - "order": 4 - }, - } - ordered_column_labels = columns.keys() - for _name, _value in columns.items(): - ordered_column_labels.pop(_value["order"]) - ordered_column_labels.insert(_value["order"], _name) - - print(ordered_column_labels) - - tree = uiwidgets.FlameTreeWidget(ordered_column_labels, window) - - # Allow multiple items in tree to be selected - tree.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) - - # Set tree column width - for _name, _val in columns.items(): - tree.setColumnWidth( - _val["order"], - _val["columnWidth"] - ) - - # Prevent weird characters when shrinking tree columns - tree.setTextElideMode(QtCore.Qt.ElideNone) - - with maintained_ftrack_session() as _session, get_config("main") as cfg_d: - - for select in selection: - seq_height = select.height - seq_width = select.width - fps = float(str(select.frame_rate)[:-4]) - break - - # input fields - shot_name_label = uiwidgets.FlameLabel( - 'Shot name template', 'normal', window) - shot_name_template_input = uiwidgets.FlameLineEdit( - cfg_d["shot_name_template"], window) - - hierarchy_label = uiwidgets.FlameLabel( - 'Parents template', 'normal', window) - hierarchy_template_input = uiwidgets.FlameLineEdit( - cfg_d["hierarchy_template"], window) - - start_frame_label = uiwidgets.FlameLabel( - 'Workfile start frame', 'normal', window) - start_frame_input = uiwidgets.FlameLineEdit( - cfg_d["workfile_start_frame"], window) - - handles_label = uiwidgets.FlameLabel( - 'Shot handles', 'normal', window) - handles_input = uiwidgets.FlameLineEdit(cfg_d["shot_handles"], window) - - width_label = uiwidgets.FlameLabel( - 'Sequence width', 'normal', window) - width_input = uiwidgets.FlameLineEdit(str(seq_width), window) - - height_label = uiwidgets.FlameLabel( - 'Sequence height', 'normal', window) - height_input = uiwidgets.FlameLineEdit(str(seq_height), window) - - pixel_aspect_label = uiwidgets.FlameLabel( - 'Pixel aspect ratio', 'normal', window) - pixel_aspect_input = uiwidgets.FlameLineEdit(str(1.00), window) - - fps_label = uiwidgets.FlameLabel( - 'Frame rate', 'normal', window) - fps_input = uiwidgets.FlameLineEdit(str(fps), window) - - # get project name from flame current project - project_name = flame.project.current_project.name - - # get project from ftrack - - # ftrack project name has to be the same as flame project! - query = 'Project where full_name is "{}"'.format(project_name) - - # globally used variables - F_PROJ_ENTITY = _session.query(query).first() - - proj_selector = bool(not F_PROJ_ENTITY) - - if proj_selector: - all_projects = _session.query( - "Project where status is active").all() - F_PROJ_ENTITY = all_projects[0] - project_names = [p["full_name"] for p in all_projects] - TASK_TYPES_ALL = {p["full_name"]: get_all_task_types( - p).keys() for p in all_projects} - project_select_label = uiwidgets.FlameLabel( - 'Select Ftrack project', 'normal', window) - project_select_input = uiwidgets.FlamePushButtonMenu( - F_PROJ_ENTITY["full_name"], project_names, window) - project_select_input.selection_changed.connect(_on_project_changed) - - F_PROJ_TASK_TYPES = get_all_task_types(F_PROJ_ENTITY) - - task_type_label = uiwidgets.FlameLabel( - 'Create Task (type)', 'normal', window) - task_type_input = uiwidgets.FlamePushButtonMenu( - cfg_d["create_task_type"], F_PROJ_TASK_TYPES.keys(), window) - - # Button - select_all_btn = uiwidgets.FlameButton( - 'Select All', select_all, window) - remove_temp_data_btn = uiwidgets.FlameButton( - 'Remove temp data', remove_temp_data, window) - - ftrack_send_btn = uiwidgets.FlameButton( - 'Send to Ftrack', send_to_ftrack, window) - - # left props - v_shift = 0 - prop_layout_l = QtWidgets.QGridLayout() - prop_layout_l.setHorizontalSpacing(30) - if proj_selector: - prop_layout_l.addWidget(project_select_label, v_shift, 0) - prop_layout_l.addWidget(project_select_input, v_shift, 1) - v_shift += 1 - prop_layout_l.addWidget(shot_name_label, (v_shift + 0), 0) - prop_layout_l.addWidget(shot_name_template_input, (v_shift + 0), 1) - prop_layout_l.addWidget(hierarchy_label, (v_shift + 1), 0) - prop_layout_l.addWidget(hierarchy_template_input, (v_shift + 1), 1) - prop_layout_l.addWidget(start_frame_label, (v_shift + 2), 0) - prop_layout_l.addWidget(start_frame_input, (v_shift + 2), 1) - prop_layout_l.addWidget(handles_label, (v_shift + 3), 0) - prop_layout_l.addWidget(handles_input, (v_shift + 3), 1) - prop_layout_l.addWidget(task_type_label, (v_shift + 4), 0) - prop_layout_l.addWidget(task_type_input, (v_shift + 4), 1) - - # right props - prop_widget_r = QtWidgets.QWidget(window) - prop_layout_r = QtWidgets.QGridLayout(prop_widget_r) - prop_layout_r.setHorizontalSpacing(30) - prop_layout_r.setAlignment( - QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) - prop_layout_r.setContentsMargins(0, 0, 0, 0) - prop_layout_r.addWidget(width_label, 1, 0) - prop_layout_r.addWidget(width_input, 1, 1) - prop_layout_r.addWidget(height_label, 2, 0) - prop_layout_r.addWidget(height_input, 2, 1) - prop_layout_r.addWidget(pixel_aspect_label, 3, 0) - prop_layout_r.addWidget(pixel_aspect_input, 3, 1) - prop_layout_r.addWidget(fps_label, 4, 0) - prop_layout_r.addWidget(fps_input, 4, 1) - - # prop layout - prop_main_layout = QtWidgets.QHBoxLayout() - prop_main_layout.addLayout(prop_layout_l, 1) - prop_main_layout.addSpacing(20) - prop_main_layout.addWidget(prop_widget_r, 1) - - # buttons layout - hbox = QtWidgets.QHBoxLayout() - hbox.addWidget(remove_temp_data_btn) - hbox.addWidget(select_all_btn) - hbox.addWidget(ftrack_send_btn) - - # put all layouts together - main_frame = QtWidgets.QVBoxLayout(window) - main_frame.setMargin(20) - main_frame.addLayout(prop_main_layout) - main_frame.addWidget(tree) - main_frame.addLayout(hbox) - - window.show() - - timeline_info(selection) - - return window - - def scope_sequence(selection): import flame return any(isinstance(item, flame.PySequence) for item in selection) From dd1610a29cfa7763387cf219f4f8ff56a8be0d7e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 19 Nov 2021 16:15:43 +0100 Subject: [PATCH 050/307] reducing redundant code --- .../openpype_flame_to_ftrack/modules/app.py | 49 +++++++++---------- .../openpype_flame_to_ftrack.py | 16 +++--- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app.py index a13df25035..6a654792f5 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app.py @@ -17,7 +17,6 @@ from .utils import ( export_video, timecode_to_frames ) -from pprint import pformat class FlameToFtrackPanel(QtWidgets.QWidget()): @@ -254,8 +253,7 @@ class FlameToFtrackPanel(QtWidgets.QWidget()): # Prevent weird characters when shrinking tree columns self.tree.setTextElideMode(QtCore.Qt.ElideNone) - def _send_to_ftrack(self): - # resolve active project and add it to self.project_entity + def _resolve_project_entity(self): if self.project_selector_enabled: selected_project_name = self.project_select_input.text() self.project_entity = next( @@ -264,13 +262,22 @@ class FlameToFtrackPanel(QtWidgets.QWidget()): None ) - _cfg_data_back = {} + def _save_ui_state_to_cfg(self): + _cfg_data_back = { + "shot_name_template": self.shot_name_template_input.text(), + "workfile_start_frame": self.start_frame_input.text(), + "shot_handles": self.handles_input.text(), + "hierarchy_template": self.hierarchy_template_input.text(), + "create_task_type": self.task_type_input.text() + } - # get shot name template from gui input - shot_name_template = self.shot_name_template_input.text() + # add cfg data back to settings.ini + set_config(_cfg_data_back, "main") - # get hierarchy from gui input - hierarchy_text = self.hierarchy_template_input.text() + def _send_to_ftrack(self): + # resolve active project and add it to self.project_entity + self._resolve_project_entity() + self._save_ui_state_to_cfg() # get hanldes from gui input handles = self.handles_input.text() @@ -282,22 +289,8 @@ class FlameToFtrackPanel(QtWidgets.QWidget()): task_type = self.task_type_input.text() # get resolution from gui inputs - width = self.width_input.text() - height = self.height_input.text() - pixel_aspect = self.pixel_aspect_input.text() fps = self.fps_input.text() - _cfg_data_back = { - "shot_name_template": shot_name_template, - "workfile_start_frame": str(frame_start), - "shot_handles": handles, - "hierarchy_template": hierarchy_text, - "create_task_type": task_type - } - - # add cfg data back to settings.ini - set_config(_cfg_data_back, "main") - with maintained_ftrack_session() as session: print("Ftrack session is: {}".format(session)) @@ -356,10 +349,12 @@ class FlameToFtrackPanel(QtWidgets.QWidget()): } # format shot name template - _shot_name = shot_name_template.format(**shot_attributes) + _shot_name = self.shot_name_template_input.text().format( + **shot_attributes) # format hierarchy template - _hierarchy_text = hierarchy_text.format(**shot_attributes) + _hierarchy_text = self.hierarchy_template_input.text().format( + **shot_attributes) print(_hierarchy_text) # solve parents @@ -411,9 +406,9 @@ class FlameToFtrackPanel(QtWidgets.QWidget()): "frameEnd": frame_end, "handleStart": int(handles), "handleEnd": int(handles), - "resolutionWidth": int(width), - "resolutionHeight": int(height), - "pixelAspect": float(pixel_aspect), + "resolutionWidth": int(self.width_input.text()), + "resolutionHeight": int(self.height_input.text()), + "pixelAspect": float(self.pixel_aspect_input.text()), "fps": float(fps) } diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index bfdaf75385..96828ae971 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -1,14 +1,16 @@ from __future__ import print_function + import os import sys -import json +try: + from app import FlameToFtrackPanel +except ImportError: + SCRIPT_DIR = os.path.dirname(__file__) + PACKAGE_DIR = os.path.join(SCRIPT_DIR, "modules") + sys.path.append(PACKAGE_DIR) -SCRIPT_DIR = os.path.dirname(__file__) -PACKAGE_DIR = os.path.join(SCRIPT_DIR, "modules") - -sys.path.append(PACKAGE_DIR) - + from app import FlameToFtrackPanel def scope_sequence(selection): import flame @@ -23,7 +25,7 @@ def get_media_panel_custom_ui_actions(): { "name": "Create Shots", "isVisible": scope_sequence, - "execute": main_window + "execute": FlameToFtrackPanel } ] } From 13edf52f93e6ce0e6d4bc437fe28a1365410a42c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 19 Nov 2021 16:38:16 +0100 Subject: [PATCH 051/307] debugging modul names --- .../modules/{utils.py => app_utils.py} | 0 .../modules/{app.py => panel_app.py} | 70 +++++++++---------- .../openpype_flame_to_ftrack.py | 4 +- 3 files changed, 37 insertions(+), 37 deletions(-) rename openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/{utils.py => app_utils.py} (100%) rename openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/{app.py => panel_app.py} (90%) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/utils.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/utils.py rename to openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py similarity index 90% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app.py rename to openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py index 6a654792f5..f49578db2f 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py @@ -3,12 +3,12 @@ from PySide2 import QtWidgets, QtCore import uiwidgets import flame -from .ftrack_lib import ( +from ftrack_lib import ( maintained_ftrack_session, FtrackEntityOperator, FtrackComponentCreator ) -from .utils import ( +from app_utils import ( get_config, set_config, get_all_task_types, @@ -19,7 +19,7 @@ from .utils import ( ) -class FlameToFtrackPanel(QtWidgets.QWidget()): +class FlameToFtrackPanel(object): temp_data_dir = None project_entity = None task_types = {} @@ -50,17 +50,16 @@ class FlameToFtrackPanel(QtWidgets.QWidget()): }, } - def __init__(self, selection, *args, **kwargs): - super(FlameToFtrackPanel, self).__init__(*args, **kwargs) + def __init__(self, selection): self.selection = selection + self.window = QtWidgets.QWidget() # creating ui - self.setMinimumSize(1500, 600) - self.setWindowTitle('Sequence Shots to Ftrack') - self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) - self.setStyleSheet('background-color: #313131') - + self.window.setMinimumSize(1500, 600) + self.window.setWindowTitle('Sequence Shots to Ftrack') + self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + self.window.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.window.setStyleSheet('background-color: #313131') self._create_tree_widget() self._set_sequence_params() @@ -81,54 +80,54 @@ class FlameToFtrackPanel(QtWidgets.QWidget()): # input fields self.shot_name_label = uiwidgets.FlameLabel( - 'Shot name template', 'normal', self) + 'Shot name template', 'normal', self.window) self.shot_name_template_input = uiwidgets.FlameLineEdit( - cfg_d["shot_name_template"], self) + cfg_d["shot_name_template"], self.window) self.hierarchy_label = uiwidgets.FlameLabel( - 'Parents template', 'normal', self) + 'Parents template', 'normal', self.window) self.hierarchy_template_input = uiwidgets.FlameLineEdit( - cfg_d["hierarchy_template"], self) + cfg_d["hierarchy_template"], self.window) self.start_frame_label = uiwidgets.FlameLabel( - 'Workfile start frame', 'normal', self) + 'Workfile start frame', 'normal', self.window) self.start_frame_input = uiwidgets.FlameLineEdit( - cfg_d["workfile_start_frame"], self) + cfg_d["workfile_start_frame"], self.window) self.handles_label = uiwidgets.FlameLabel( - 'Shot handles', 'normal', self) + 'Shot handles', 'normal', self.window) self.handles_input = uiwidgets.FlameLineEdit( - cfg_d["shot_handles"], self) + cfg_d["shot_handles"], self.window) self.width_label = uiwidgets.FlameLabel( - 'Sequence width', 'normal', self) + 'Sequence width', 'normal', self.window) self.width_input = uiwidgets.FlameLineEdit( - str(self.seq_width), self) + str(self.seq_width), self.window) self.height_label = uiwidgets.FlameLabel( - 'Sequence height', 'normal', self) + 'Sequence height', 'normal', self.window) self.height_input = uiwidgets.FlameLineEdit( - str(self.seq_height), self) + str(self.seq_height), self.window) self.pixel_aspect_label = uiwidgets.FlameLabel( - 'Pixel aspect ratio', 'normal', self) + 'Pixel aspect ratio', 'normal', self.window) self.pixel_aspect_input = uiwidgets.FlameLineEdit( - str(1.00), self) + str(1.00), self.window) self.fps_label = uiwidgets.FlameLabel( - 'Frame rate', 'normal', self) + 'Frame rate', 'normal', self.window) self.fps_input = uiwidgets.FlameLineEdit( - str(self.fps), self) + str(self.fps), self.window) # Button self.select_all_btn = uiwidgets.FlameButton( - 'Select All', self.select_all, self) + 'Select All', self.select_all, self.window) self.remove_temp_data_btn = uiwidgets.FlameButton( - 'Remove temp data', self.remove_temp_data, self) + 'Remove temp data', self.remove_temp_data, self.window) self.ftrack_send_btn = uiwidgets.FlameButton( - 'Send to Ftrack', self._send_to_ftrack, self) + 'Send to Ftrack', self._send_to_ftrack, self.window) def _generate_layouts(self): # left props @@ -199,9 +198,9 @@ class FlameToFtrackPanel(QtWidgets.QWidget()): self.task_types = get_all_task_types(self.project_entity) self.task_type_label = uiwidgets.FlameLabel( - 'Create Task (type)', 'normal', self) + 'Create Task (type)', 'normal', self.window) self.task_type_input = uiwidgets.FlamePushButtonMenu( - cfg_d["create_task_type"], self.task_types.keys(), self) + cfg_d["create_task_type"], self.task_types.keys(), self.window) def _create_project_widget(self): @@ -226,9 +225,9 @@ class FlameToFtrackPanel(QtWidgets.QWidget()): self.all_task_types = {p["full_name"]: get_all_task_types( p).keys() for p in self.all_projects} self.project_select_label = uiwidgets.FlameLabel( - 'Select Ftrack project', 'normal', self) + 'Select Ftrack project', 'normal', self.window) self.project_select_input = uiwidgets.FlamePushButtonMenu( - self.project_entity["full_name"], project_names, self) + self.project_entity["full_name"], project_names, self.window) self.project_select_input.selection_changed.connect( self._on_project_changed) @@ -238,7 +237,8 @@ class FlameToFtrackPanel(QtWidgets.QWidget()): ordered_column_labels.pop(_value["order"]) ordered_column_labels.insert(_value["order"], _name) - self.tree = uiwidgets.FlameTreeWidget(ordered_column_labels, self) + self.tree = uiwidgets.FlameTreeWidget( + ordered_column_labels, self.window) # Allow multiple items in tree to be selected self.tree.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index 96828ae971..a6383ea7fe 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -4,13 +4,13 @@ import os import sys try: - from app import FlameToFtrackPanel + from panel_app import FlameToFtrackPanel except ImportError: SCRIPT_DIR = os.path.dirname(__file__) PACKAGE_DIR = os.path.join(SCRIPT_DIR, "modules") sys.path.append(PACKAGE_DIR) - from app import FlameToFtrackPanel + from panel_app import FlameToFtrackPanel def scope_sequence(selection): import flame From 05ef1311402d2c344c9f77ec17ac4375aa5f4ab4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 19 Nov 2021 17:38:41 +0100 Subject: [PATCH 052/307] debugging panel functions --- .../modules/panel_app.py | 104 +++++++++--------- .../openpype_flame_to_ftrack.py | 8 +- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py index f49578db2f..50eedbee77 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py @@ -3,20 +3,16 @@ from PySide2 import QtWidgets, QtCore import uiwidgets import flame +import ftrack_lib +reload(ftrack_lib) +import app_utils +reload(app_utils) + from ftrack_lib import ( maintained_ftrack_session, FtrackEntityOperator, FtrackComponentCreator ) -from app_utils import ( - get_config, - set_config, - get_all_task_types, - make_temp_dir, - export_thumbnail, - export_video, - timecode_to_frames -) class FlameToFtrackPanel(object): @@ -51,7 +47,7 @@ class FlameToFtrackPanel(object): } def __init__(self, selection): - + print(selection) self.selection = selection self.window = QtWidgets.QWidget() # creating ui @@ -61,21 +57,23 @@ class FlameToFtrackPanel(object): self.window.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.window.setStyleSheet('background-color: #313131') - self._create_tree_widget() - self._set_sequence_params() + with maintained_ftrack_session() as session: + self._create_project_widget(session) + self._create_tree_widget() + self._set_sequence_params() + self._generate_widgets() + print(5) + self._generate_layouts() + print(6) + self._timeline_info() + print(7) + self._fix_resolution() + print(8) - self._create_project_widget() - - self._generate_widgets() - - self._generate_layouts() - - self._timeline_info() - - self._fix_resolution() + self.window.show() def _generate_widgets(self): - with get_config("main") as cfg_d: + with app_utils.get_config("main") as cfg_d: self._create_task_type_widget(cfg_d) # input fields @@ -153,7 +151,7 @@ class FlameToFtrackPanel(object): self.task_type_input, (v_shift + 4), 1) # right props - prop_widget_r = QtWidgets.QWidget(self) + prop_widget_r = QtWidgets.QWidget(self.window) prop_layout_r = QtWidgets.QGridLayout(prop_widget_r) prop_layout_r.setHorizontalSpacing(30) prop_layout_r.setAlignment( @@ -181,7 +179,7 @@ class FlameToFtrackPanel(object): hbox.addWidget(self.ftrack_send_btn) # put all layouts together - main_frame = QtWidgets.QVBoxLayout(self) + main_frame = QtWidgets.QVBoxLayout(self.window) main_frame.setMargin(20) main_frame.addLayout(prop_main_layout) main_frame.addWidget(self.tree) @@ -195,41 +193,41 @@ class FlameToFtrackPanel(object): break def _create_task_type_widget(self, cfg_d): - self.task_types = get_all_task_types(self.project_entity) + print(self.project_entity) + self.task_types = app_utils.get_all_task_types(self.project_entity) self.task_type_label = uiwidgets.FlameLabel( 'Create Task (type)', 'normal', self.window) self.task_type_input = uiwidgets.FlamePushButtonMenu( cfg_d["create_task_type"], self.task_types.keys(), self.window) - def _create_project_widget(self): + def _create_project_widget(self, session): - with maintained_ftrack_session() as session: - # get project name from flame current project - self.project_name = flame.project.current_project.name + # get project name from flame current project + self.project_name = flame.project.current_project.name - # get project from ftrack - - # ftrack project name has to be the same as flame project! - query = 'Project where full_name is "{}"'.format(self.project_name) + # get project from ftrack - + # ftrack project name has to be the same as flame project! + query = 'Project where full_name is "{}"'.format(self.project_name) - # globally used variables - self.project_entity = session.query(query).first() + # globally used variables + self.project_entity = session.query(query).first() - self.project_selector_enabled = bool(not self.project_entity) + self.project_selector_enabled = bool(not self.project_entity) - if self.project_selector_enabled: - self.all_projects = session.query( - "Project where status is active").all() - self.project_entity = self.all_projects[0] - project_names = [p["full_name"] for p in self.all_projects] - self.all_task_types = {p["full_name"]: get_all_task_types( - p).keys() for p in self.all_projects} - self.project_select_label = uiwidgets.FlameLabel( - 'Select Ftrack project', 'normal', self.window) - self.project_select_input = uiwidgets.FlamePushButtonMenu( - self.project_entity["full_name"], project_names, self.window) - self.project_select_input.selection_changed.connect( - self._on_project_changed) + if self.project_selector_enabled: + self.all_projects = session.query( + "Project where status is active").all() + self.project_entity = self.all_projects[0] + project_names = [p["full_name"] for p in self.all_projects] + self.all_task_types = {p["full_name"]: app_utils.get_all_task_types( + p).keys() for p in self.all_projects} + self.project_select_label = uiwidgets.FlameLabel( + 'Select Ftrack project', 'normal', self.window) + self.project_select_input = uiwidgets.FlamePushButtonMenu( + self.project_entity["full_name"], project_names, self.window) + self.project_select_input.selection_changed.connect( + self._on_project_changed) def _create_tree_widget(self): ordered_column_labels = self.columns.keys() @@ -272,7 +270,7 @@ class FlameToFtrackPanel(object): } # add cfg data back to settings.ini - set_config(_cfg_data_back, "main") + app_utils.set_config(_cfg_data_back, "main") def _send_to_ftrack(self): # resolve active project and add it to self.project_entity @@ -456,7 +454,7 @@ class FlameToFtrackPanel(object): continue # get clip frame duration record_duration = str(segment.record_duration)[1:-1] - clip_duration = timecode_to_frames( + clip_duration = app_utils.timecode_to_frames( record_duration, frame_rate) # populate shot source metadata @@ -500,9 +498,9 @@ class FlameToFtrackPanel(object): if self.temp_data_dir: return True - with make_temp_dir() as tempdir_path: + with app_utils.make_temp_dir() as tempdir_path: for seq in self.selection: - export_thumbnail(seq, tempdir_path, change_preset_data) - export_video(seq, tempdir_path, change_preset_data) + app_utils.export_thumbnail(seq, tempdir_path, change_preset_data) + app_utils.export_video(seq, tempdir_path, change_preset_data) self.temp_data_dir = tempdir_path break diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index a6383ea7fe..3c1063c445 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -4,13 +4,15 @@ import os import sys try: - from panel_app import FlameToFtrackPanel + import panel_app + reload(panel_app) except ImportError: SCRIPT_DIR = os.path.dirname(__file__) PACKAGE_DIR = os.path.join(SCRIPT_DIR, "modules") sys.path.append(PACKAGE_DIR) - from panel_app import FlameToFtrackPanel + import panel_app + def scope_sequence(selection): import flame @@ -25,7 +27,7 @@ def get_media_panel_custom_ui_actions(): { "name": "Create Shots", "isVisible": scope_sequence, - "execute": FlameToFtrackPanel + "execute": panel_app.FlameToFtrackPanel } ] } From e4970603750067c5ad0ce12cc82f01e7668f6eb2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 19 Nov 2021 17:44:44 +0100 Subject: [PATCH 053/307] debugging window move and json import --- .../openpype_flame_to_ftrack/modules/ftrack_lib.py | 11 ++++++----- .../openpype_flame_to_ftrack/modules/panel_app.py | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py index 8ea1cfc775..215e3d69ec 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py @@ -2,6 +2,7 @@ import os import sys import six import re +import json from contextlib import contextmanager # Fill following constants or set them via environment variable @@ -127,13 +128,13 @@ class FtrackComponentCreator: duration = data["duration"] handles = data["handles"] fps = data["fps"] - component_data.update({ - "metadata": {'ftr_meta': json.dumps({ + component_data["metadata"] = { + 'ftr_meta': json.dumps({ 'frameIn': int(0), 'frameOut': int(duration + (handles * 2)), - 'frameRate': float(fps)})} - }) - + 'frameRate': float(fps) + }) + } if not assetversion_entity: # get assettype entity from session assettype_entity = self._get_assettype({"short": "reference"}) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py index 50eedbee77..4ef22c513d 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py @@ -434,9 +434,9 @@ class FlameToFtrackPanel(object): def _fix_resolution(self): # Center window in linux resolution = QtWidgets.QDesktopWidget().screenGeometry() - self.move( - (resolution.width() / 2) - (self.frameSize().width() / 2), - (resolution.height() / 2) - (self.frameSize().height() / 2)) + self.window.move( + (resolution.width() / 2) - (self.window.frameSize().width() / 2), + (resolution.height() / 2) - (self.window.frameSize().height() / 2)) def _on_project_changed(self): task_types = self.all_task_types[self.project_name] From 1127d4cc003e35e150bf7dfc412e9c749108c75c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sat, 20 Nov 2021 08:46:53 +0100 Subject: [PATCH 054/307] adding widget with closing event action --- .../openpype_flame_to_ftrack/modules/panel_app.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py index 4ef22c513d..b9ae72d7f1 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py @@ -14,6 +14,17 @@ from ftrack_lib import ( FtrackComponentCreator ) +class MainWindow(QtWidgets.QWidget): + can_close = True + def __init__(self, *args, **kwargs): + super(MainWindow, self).__init__(*args, **kwargs) + def closeEvent(self, event): + print("______>>>____ closing app") + print(event) + if self.can_close: + event.accept() + else: + event.ignore() class FlameToFtrackPanel(object): temp_data_dir = None @@ -49,7 +60,7 @@ class FlameToFtrackPanel(object): def __init__(self, selection): print(selection) self.selection = selection - self.window = QtWidgets.QWidget() + self.window = MainWindow() # creating ui self.window.setMinimumSize(1500, 600) self.window.setWindowTitle('Sequence Shots to Ftrack') From d7f5510f331bf7db8f17b074cd76d5c5e77750b7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sat, 20 Nov 2021 09:51:29 +0100 Subject: [PATCH 055/307] triggering clearing data --- .../modules/panel_app.py | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py index b9ae72d7f1..09fa312e88 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py @@ -16,22 +16,24 @@ from ftrack_lib import ( class MainWindow(QtWidgets.QWidget): can_close = True - def __init__(self, *args, **kwargs): + def __init__(self, klass, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) + self.panel_class = klass + def closeEvent(self, event): - print("______>>>____ closing app") - print(event) - if self.can_close: - event.accept() - else: - event.ignore() + # clear all temp data + print("Removing temp data") + self.panel_class.clear_temp_data() + + # now the panel can be closed + event.accept() class FlameToFtrackPanel(object): temp_data_dir = None + processed_components = [] project_entity = None task_types = {} all_task_types = {} - processed_components = [] # TreeWidget columns = { @@ -60,7 +62,7 @@ class FlameToFtrackPanel(object): def __init__(self, selection): print(selection) self.selection = selection - self.window = MainWindow() + self.window = MainWindow(self) # creating ui self.window.setMinimumSize(1500, 600) self.window.setWindowTitle('Sequence Shots to Ftrack') @@ -133,7 +135,7 @@ class FlameToFtrackPanel(object): 'Select All', self.select_all, self.window) self.remove_temp_data_btn = uiwidgets.FlameButton( - 'Remove temp data', self.remove_temp_data, self.window) + 'Remove temp data', self.clear_temp_data, self.window) self.ftrack_send_btn = uiwidgets.FlameButton( 'Send to Ftrack', self._send_to_ftrack, self.window) @@ -498,12 +500,14 @@ class FlameToFtrackPanel(object): def select_all(self, ): self.tree.selectAll() - def remove_temp_data(self, ): + def clear_temp_data(self): import shutil if self.temp_data_dir: shutil.rmtree(self.temp_data_dir) self.temp_data_dir = None + self.processed_components = [] + print("All Temp data were destroied ...") def generate_temp_data(self, change_preset_data): if self.temp_data_dir: From b7a5eef4bff125a229f3fc67c8bd5a967762578c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Sat, 20 Nov 2021 10:09:55 +0100 Subject: [PATCH 056/307] processed components testing --- .../openpype_flame_to_ftrack/modules/panel_app.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py index 09fa312e88..82e9d3ba02 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py @@ -61,6 +61,9 @@ class FlameToFtrackPanel(object): def __init__(self, selection): print(selection) + print(self.processed_components) + self.processed_components = [] + print(self.processed_components) self.selection = selection self.window = MainWindow(self) # creating ui @@ -501,12 +504,13 @@ class FlameToFtrackPanel(object): self.tree.selectAll() def clear_temp_data(self): + self.processed_components = [] + import shutil if self.temp_data_dir: shutil.rmtree(self.temp_data_dir) self.temp_data_dir = None - self.processed_components = [] print("All Temp data were destroied ...") def generate_temp_data(self, change_preset_data): From 62a626969431a2298abcef475a040586bb35a140 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 Nov 2021 11:59:56 +0100 Subject: [PATCH 057/307] moving temp data generator to ftrack component creator --- .../modules/ftrack_lib.py | 36 +++++++++++++ .../modules/panel_app.py | 50 +++++++------------ 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py index 215e3d69ec..6ea35f6c22 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py @@ -5,6 +5,9 @@ import re import json from contextlib import contextmanager +import app_utils +reload(app_utils) + # Fill following constants or set them via environment variable FTRACK_MODULE_PATH = None FTRACK_API_KEY = None @@ -96,11 +99,44 @@ def maintained_ftrack_session(): class FtrackComponentCreator: default_location = "ftrack.server" ftrack_locations = {} + thumbnails = [] + videos = [] + temp_dir = None def __init__(self, session): self.session = session self._get_ftrack_location() + + def generate_temp_data(self, selection, temp_folder, change_preset_data): + print(">>>>> self.temp_dir: " + self.temp_dir) + print(">>>>> self.thumbnails: " + str(self.thumbnails)) + print(">>>>> self.videos: " + str(self.videos)) + + if temp_folder == self.temp_dir: + return temp_folder + + with app_utils.make_temp_dir() as tempdir_path: + for seq in selection: + app_utils.export_thumbnail( + seq, tempdir_path, change_preset_data) + app_utils.export_video(seq, tempdir_path, change_preset_data) + temp_files = os.listdir(temp_folder) + self.thumbnails = [f for f in temp_files if "jpg" in f] + self.videos = [f for f in temp_files if "mov" in f] + self.temp_dir = tempdir_path + return tempdir_path + + def get_thumb_path(shot_name): + # get component files + thumb_f = next((f for f in self.thumbnails if shot_name in f), None) + return os.path.join(self.temp_dir, thumb_f) + + def get_video_path(shot_name): + # get component files + video_f = next((f for f in self.videos if shot_name in f), None) + return os.path.join(self.temp_dir, video_f) + def close(self): self.ftrack_locations = {} self.session = None diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py index 82e9d3ba02..c5a4ffea97 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py @@ -74,9 +74,13 @@ class FlameToFtrackPanel(object): self.window.setStyleSheet('background-color: #313131') with maintained_ftrack_session() as session: + print(1) self._create_project_widget(session) + print(2) self._create_tree_widget() + print(3) self._set_sequence_params() + print(4) self._generate_widgets() print(5) self._generate_layouts() @@ -312,17 +316,13 @@ class FlameToFtrackPanel(object): session, self.project_entity) component_creator = FtrackComponentCreator(session) - self.generate_temp_data({ - "nbHandles": handles - }) - - temp_files = os.listdir(self.temp_data_dir) - thumbnails = [f for f in temp_files if "jpg" in f] - videos = [f for f in temp_files if "mov" in f] - - print(temp_files) - print(thumbnails) - print(videos) + self.temp_data_dir = component_creator.generate_temp_data( + self.selection, + self.temp_data_dir, + { + "nbHandles": handles + } + ) # Get all selected items from treewidget for item in self.tree.selectedItems(): @@ -338,18 +338,13 @@ class FlameToFtrackPanel(object): sequence_name = item.text(0) shot_name = item.text(1) - # get component files - thumb_f = next((f for f in thumbnails if shot_name in f), None) - video_f = next((f for f in videos if shot_name in f), None) - thumb_fp = os.path.join(self.temp_data_dir, thumb_f) - video_fp = os.path.join(self.temp_data_dir, video_f) - print(thumb_fp) - print(video_fp) + thumb_fp = component_creator.get_thumb_path(shot_name) + video_fp = component_creator.get_video_path(shot_name) print("processed comps: {}".format(self.processed_components)) processed = False - if thumb_f not in self.processed_components: - self.processed_components.append(thumb_f) + if thumb_fp not in self.processed_components: + self.processed_components.append(thumb_fp) else: processed = True @@ -504,22 +499,11 @@ class FlameToFtrackPanel(object): self.tree.selectAll() def clear_temp_data(self): - self.processed_components = [] - import shutil + self.processed_components = [] + if self.temp_data_dir: shutil.rmtree(self.temp_data_dir) self.temp_data_dir = None print("All Temp data were destroied ...") - - def generate_temp_data(self, change_preset_data): - if self.temp_data_dir: - return True - - with app_utils.make_temp_dir() as tempdir_path: - for seq in self.selection: - app_utils.export_thumbnail(seq, tempdir_path, change_preset_data) - app_utils.export_video(seq, tempdir_path, change_preset_data) - self.temp_data_dir = tempdir_path - break From fbb9d3e23279b6725a3d326ca1a93928c53cdedd Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Mon, 22 Nov 2021 13:58:51 +0100 Subject: [PATCH 058/307] Add parent asset in anatomy --- openpype/lib/avalon_context.py | 5 +++++ .../plugins/publish/collect_anatomy_context_data.py | 5 +++++ openpype/tools/workfiles/app.py | 11 ++++++++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 372e116f43..581e4b9dbd 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -486,6 +486,10 @@ def get_workdir_data(project_doc, asset_doc, task_name, host_name): project_task_types = project_doc["config"]["tasks"] task_code = project_task_types.get(task_type, {}).get("short_name") + parent = project_doc["name"] + if len(asset_doc["data"]["parents"]) != 0: + parent = asset_doc["data"]["parents"][-1] + data = { "project": { "name": project_doc["name"], @@ -497,6 +501,7 @@ def get_workdir_data(project_doc, asset_doc, task_name, host_name): "short": task_code, }, "asset": asset_doc["name"], + "parent": parent, "app": host_name, "user": getpass.getuser(), "hierarchy": hierarchy, diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 6b95979b76..b0c9eea576 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -60,12 +60,17 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): project_task_types = project_entity["config"]["tasks"] task_code = project_task_types.get(task_type, {}).get("short_name") + parent = project_entity["name"] + if len(asset_entity["data"]["parents"]) != 0: + parent = asset_entity["data"]["parents"][-1] + context_data = { "project": { "name": project_entity["name"], "code": project_entity["data"].get("code") }, "asset": asset_entity["name"], + "parent": parent, "hierarchy": hierarchy.replace("\\", "/"), "task": { "name": task_name, diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index a4b1717a1c..4253f7450a 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -68,12 +68,16 @@ class NameWindow(QtWidgets.QDialog): "config.tasks": True, } ) + asset_doc = io.find_one( { "type": "asset", "name": asset_name }, - {"data.tasks": True} + { + "data.tasks": True, + "data.parents": True + } ) task_type = asset_doc["data"]["tasks"].get(task_name, {}).get("type") @@ -81,6 +85,10 @@ class NameWindow(QtWidgets.QDialog): project_task_types = project_doc["config"]["tasks"] task_short = project_task_types.get(task_type, {}).get("short_name") + parent = project_doc["name"] + if len(asset_doc["data"]["parents"]) != 0: + parent = asset_doc["data"]["parents"][-1] + self.data = { "project": { "name": project_doc["name"], @@ -92,6 +100,7 @@ class NameWindow(QtWidgets.QDialog): "type": task_type, "short": task_short, }, + "parent": parent, "version": 1, "user": getpass.getuser(), "comment": "", From 41a02fddc4b6241602f01620b3756d2887be9e54 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 29 Oct 2021 11:34:26 +0200 Subject: [PATCH 059/307] add parent asset to doc --- website/docs/admin_settings_project_anatomy.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/website/docs/admin_settings_project_anatomy.md b/website/docs/admin_settings_project_anatomy.md index 30784686e2..e897b0ffab 100644 --- a/website/docs/admin_settings_project_anatomy.md +++ b/website/docs/admin_settings_project_anatomy.md @@ -57,9 +57,14 @@ We have a few required anatomy templates for OpenPype to work properly, however | `project[code]` | Project's code | | `hierarchy` | All hierarchical parents as subfolders | | `asset` | Name of asset or shot | +<<<<<<< HEAD | `task[name]` | Name of task | | `task[type]` | Type of task | | `task[short]` | Shortname of task | +======= +| `parent` | Name of parent folder | +| `task` | Name of task | +>>>>>>> add7db0c0... add parent asset to doc | `version` | Version number | | `subset` | Subset name | | `family` | Main family name | From 0bb87ca691dea7b9b09c996cb6c5d663539cf31e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 Nov 2021 17:12:20 +0100 Subject: [PATCH 060/307] improve work with ftrack session --- .../modules/app_utils.py | 12 - .../modules/ftrack_lib.py | 91 ++--- .../modules/panel_app.py | 356 +++++++++--------- 3 files changed, 220 insertions(+), 239 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py index 2aa6577325..b255d8d3f5 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py @@ -99,18 +99,6 @@ create_task_type = Compositing """ -def get_all_task_types(project_entity): - tasks = {} - proj_template = project_entity['project_schema'] - temp_task_types = proj_template['_task_type_schema']['types'] - - for type in temp_task_types: - if type['name'] not in tasks: - tasks[type['name']] = type - - return tasks - - def configure_preset(file_path, data): split_fp = os.path.splitext(file_path) new_file_path = split_fp[0] + "_tmp" + split_fp[-1] diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py index 6ea35f6c22..b074a0acf1 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py @@ -27,73 +27,56 @@ def import_ftrack_api(): return ftrack_api -@contextmanager -def maintained_ftrack_session(): +def get_ftrack_session(): import os ftrack_api = import_ftrack_api() - def validate_credentials(url, user, api): - first_validation = True - if not user: - print('- Ftrack Username is not set') - first_validation = False - if not api: - print('- Ftrack API key is not set') - first_validation = False - if not first_validation: - return False - - try: - session = ftrack_api.Session( - server_url=url, - api_user=user, - api_key=api - ) - session.close() - except Exception as _e: - print( - "Can't log into Ftrack with used credentials: {}".format( - _e) - ) - ftrack_cred = { - 'Ftrack server': str(url), - 'Username': str(user), - 'API key': str(api), - } - - item_lens = [len(key) + 1 for key in ftrack_cred] - justify_len = max(*item_lens) - for key, value in ftrack_cred.items(): - print('{} {}'.format((key + ':').ljust( - justify_len, ' '), value)) - return False - print( - 'Credentials Username: "{}", API key: "{}" are valid.'.format( - user, api) - ) - return True - # fill your own credentials url = FTRACK_SERVER or os.getenv("FTRACK_SERVER") or "" user = FTRACK_API_USER or os.getenv("FTRACK_API_USER") or "" api = FTRACK_API_KEY or os.getenv("FTRACK_API_KEY") or "" + first_validation = True + if not user: + print('- Ftrack Username is not set') + first_validation = False + if not api: + print('- Ftrack API key is not set') + first_validation = False + if not first_validation: + return False + try: - assert validate_credentials(url, user, api), ( - "Ftrack credentials failed") - # open ftrack session - session = ftrack_api.Session( + return ftrack_api.Session( server_url=url, api_user=user, api_key=api ) - yield session - except Exception: - tp, value, tb = sys.exc_info() - six.reraise(tp, value, tb) - finally: - # close the session - session.close() + except Exception as _e: + print("Can't log into Ftrack with used credentials: {}".format(_e)) + ftrack_cred = { + 'Ftrack server': str(url), + 'Username': str(user), + 'API key': str(api), + } + + item_lens = [len(key) + 1 for key in ftrack_cred] + justify_len = max(*item_lens) + for key, value in ftrack_cred.items(): + print('{} {}'.format((key + ':').ljust(justify_len, ' '), value)) + return False + + +def get_project_task_types(project_entity): + tasks = {} + proj_template = project_entity['project_schema'] + temp_task_types = proj_template['_task_type_schema']['types'] + + for type in temp_task_types: + if type['name'] not in tasks: + tasks[type['name']] = type + + return tasks class FtrackComponentCreator: diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py index c5a4ffea97..0e3a4ae4cf 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py @@ -9,7 +9,7 @@ import app_utils reload(app_utils) from ftrack_lib import ( - maintained_ftrack_session, + get_ftrack_session, FtrackEntityOperator, FtrackComponentCreator ) @@ -24,11 +24,13 @@ class MainWindow(QtWidgets.QWidget): # clear all temp data print("Removing temp data") self.panel_class.clear_temp_data() + self.panel_class.close() # now the panel can be closed event.accept() class FlameToFtrackPanel(object): + session = None temp_data_dir = None processed_components = [] project_entity = None @@ -62,8 +64,12 @@ class FlameToFtrackPanel(object): def __init__(self, selection): print(selection) print(self.processed_components) + + self.session = get_ftrack_session() + self.processed_components = [] print(self.processed_components) + self.selection = selection self.window = MainWindow(self) # creating ui @@ -73,79 +79,80 @@ class FlameToFtrackPanel(object): self.window.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.window.setStyleSheet('background-color: #313131') - with maintained_ftrack_session() as session: - print(1) - self._create_project_widget(session) - print(2) - self._create_tree_widget() - print(3) - self._set_sequence_params() - print(4) - self._generate_widgets() - print(5) - self._generate_layouts() - print(6) - self._timeline_info() - print(7) - self._fix_resolution() - print(8) + print(1) + self._create_project_widget() + print(2) + self._create_tree_widget() + print(3) + self._set_sequence_params() + print(4) + self._generate_widgets() + print(5) + self._generate_layouts() + print(6) + self._timeline_info() + print(7) + self._fix_resolution() + print(8) self.window.show() def _generate_widgets(self): - with app_utils.get_config("main") as cfg_d: - self._create_task_type_widget(cfg_d) + with app_utils.get_config("main") as cfg_data: + cfg_d = cfg_data - # input fields - self.shot_name_label = uiwidgets.FlameLabel( - 'Shot name template', 'normal', self.window) - self.shot_name_template_input = uiwidgets.FlameLineEdit( - cfg_d["shot_name_template"], self.window) + self._create_task_type_widget(cfg_d) - self.hierarchy_label = uiwidgets.FlameLabel( - 'Parents template', 'normal', self.window) - self.hierarchy_template_input = uiwidgets.FlameLineEdit( - cfg_d["hierarchy_template"], self.window) + # input fields + self.shot_name_label = uiwidgets.FlameLabel( + 'Shot name template', 'normal', self.window) + self.shot_name_template_input = uiwidgets.FlameLineEdit( + cfg_d["shot_name_template"], self.window) - self.start_frame_label = uiwidgets.FlameLabel( - 'Workfile start frame', 'normal', self.window) - self.start_frame_input = uiwidgets.FlameLineEdit( - cfg_d["workfile_start_frame"], self.window) + self.hierarchy_label = uiwidgets.FlameLabel( + 'Parents template', 'normal', self.window) + self.hierarchy_template_input = uiwidgets.FlameLineEdit( + cfg_d["hierarchy_template"], self.window) - self.handles_label = uiwidgets.FlameLabel( - 'Shot handles', 'normal', self.window) - self.handles_input = uiwidgets.FlameLineEdit( - cfg_d["shot_handles"], self.window) + self.start_frame_label = uiwidgets.FlameLabel( + 'Workfile start frame', 'normal', self.window) + self.start_frame_input = uiwidgets.FlameLineEdit( + cfg_d["workfile_start_frame"], self.window) - self.width_label = uiwidgets.FlameLabel( - 'Sequence width', 'normal', self.window) - self.width_input = uiwidgets.FlameLineEdit( - str(self.seq_width), self.window) + self.handles_label = uiwidgets.FlameLabel( + 'Shot handles', 'normal', self.window) + self.handles_input = uiwidgets.FlameLineEdit( + cfg_d["shot_handles"], self.window) - self.height_label = uiwidgets.FlameLabel( - 'Sequence height', 'normal', self.window) - self.height_input = uiwidgets.FlameLineEdit( - str(self.seq_height), self.window) + self.width_label = uiwidgets.FlameLabel( + 'Sequence width', 'normal', self.window) + self.width_input = uiwidgets.FlameLineEdit( + str(self.seq_width), self.window) - self.pixel_aspect_label = uiwidgets.FlameLabel( - 'Pixel aspect ratio', 'normal', self.window) - self.pixel_aspect_input = uiwidgets.FlameLineEdit( - str(1.00), self.window) + self.height_label = uiwidgets.FlameLabel( + 'Sequence height', 'normal', self.window) + self.height_input = uiwidgets.FlameLineEdit( + str(self.seq_height), self.window) - self.fps_label = uiwidgets.FlameLabel( - 'Frame rate', 'normal', self.window) - self.fps_input = uiwidgets.FlameLineEdit( - str(self.fps), self.window) + self.pixel_aspect_label = uiwidgets.FlameLabel( + 'Pixel aspect ratio', 'normal', self.window) + self.pixel_aspect_input = uiwidgets.FlameLineEdit( + str(1.00), self.window) - # Button - self.select_all_btn = uiwidgets.FlameButton( - 'Select All', self.select_all, self.window) + self.fps_label = uiwidgets.FlameLabel( + 'Frame rate', 'normal', self.window) + self.fps_input = uiwidgets.FlameLineEdit( + str(self.fps), self.window) - self.remove_temp_data_btn = uiwidgets.FlameButton( - 'Remove temp data', self.clear_temp_data, self.window) + # Button + self.select_all_btn = uiwidgets.FlameButton( + 'Select All', self.select_all, self.window) - self.ftrack_send_btn = uiwidgets.FlameButton( - 'Send to Ftrack', self._send_to_ftrack, self.window) + self.remove_temp_data_btn = uiwidgets.FlameButton( + 'Remove temp data', self.clear_temp_data, self.window) + + self.ftrack_send_btn = uiwidgets.FlameButton( + 'Send to Ftrack', self._send_to_ftrack, self.window) def _generate_layouts(self): # left props @@ -214,14 +221,15 @@ class FlameToFtrackPanel(object): def _create_task_type_widget(self, cfg_d): print(self.project_entity) - self.task_types = app_utils.get_all_task_types(self.project_entity) + self.task_types = ftrack_lib.get_project_task_types( + self.project_entity) self.task_type_label = uiwidgets.FlameLabel( 'Create Task (type)', 'normal', self.window) self.task_type_input = uiwidgets.FlamePushButtonMenu( cfg_d["create_task_type"], self.task_types.keys(), self.window) - def _create_project_widget(self, session): + def _create_project_widget(self): # get project name from flame current project self.project_name = flame.project.current_project.name @@ -231,17 +239,19 @@ class FlameToFtrackPanel(object): query = 'Project where full_name is "{}"'.format(self.project_name) # globally used variables - self.project_entity = session.query(query).first() + self.project_entity = self.session.query(query).first() self.project_selector_enabled = bool(not self.project_entity) if self.project_selector_enabled: - self.all_projects = session.query( + self.all_projects = self.session.query( "Project where status is active").all() self.project_entity = self.all_projects[0] project_names = [p["full_name"] for p in self.all_projects] - self.all_task_types = {p["full_name"]: app_utils.get_all_task_types( - p).keys() for p in self.all_projects} + self.all_task_types = { + p["full_name"]: ftrack_lib.get_project_task_types(p).keys() + for p in self.all_projects + } self.project_select_label = uiwidgets.FlameLabel( 'Select Ftrack project', 'normal', self.window) self.project_select_input = uiwidgets.FlamePushButtonMenu( @@ -309,138 +319,135 @@ class FlameToFtrackPanel(object): # get resolution from gui inputs fps = self.fps_input.text() - with maintained_ftrack_session() as session: - print("Ftrack session is: {}".format(session)) + entity_operator = FtrackEntityOperator( + self.session, self.project_entity) + component_creator = FtrackComponentCreator(self.session) - entity_operator = FtrackEntityOperator( - session, self.project_entity) - component_creator = FtrackComponentCreator(session) + self.temp_data_dir = component_creator.generate_temp_data( + self.selection, + self.temp_data_dir, + { + "nbHandles": handles + } + ) - self.temp_data_dir = component_creator.generate_temp_data( - self.selection, - self.temp_data_dir, - { - "nbHandles": handles - } - ) + # Get all selected items from treewidget + for item in self.tree.selectedItems(): + # frame ranges + frame_duration = int(item.text(2)) + frame_end = frame_start + frame_duration - # Get all selected items from treewidget - for item in self.tree.selectedItems(): - # frame ranges - frame_duration = int(item.text(2)) - frame_end = frame_start + frame_duration + # description + shot_description = item.text(3) + task_description = item.text(4) - # description - shot_description = item.text(3) - task_description = item.text(4) + # other + sequence_name = item.text(0) + shot_name = item.text(1) - # other - sequence_name = item.text(0) - shot_name = item.text(1) + thumb_fp = component_creator.get_thumb_path(shot_name) + video_fp = component_creator.get_video_path(shot_name) - thumb_fp = component_creator.get_thumb_path(shot_name) - video_fp = component_creator.get_video_path(shot_name) + print("processed comps: {}".format(self.processed_components)) + processed = False + if thumb_fp not in self.processed_components: + self.processed_components.append(thumb_fp) + else: + processed = True - print("processed comps: {}".format(self.processed_components)) - processed = False - if thumb_fp not in self.processed_components: - self.processed_components.append(thumb_fp) - else: - processed = True + print("processed: {}".format(processed)) - print("processed: {}".format(processed)) + # populate full shot info + shot_attributes = { + "sequence": sequence_name, + "shot": shot_name, + "task": task_type + } - # populate full shot info - shot_attributes = { - "sequence": sequence_name, - "shot": shot_name, - "task": task_type - } + # format shot name template + _shot_name = self.shot_name_template_input.text().format( + **shot_attributes) - # format shot name template - _shot_name = self.shot_name_template_input.text().format( - **shot_attributes) + # format hierarchy template + _hierarchy_text = self.hierarchy_template_input.text().format( + **shot_attributes) + print(_hierarchy_text) - # format hierarchy template - _hierarchy_text = self.hierarchy_template_input.text().format( - **shot_attributes) - print(_hierarchy_text) + # solve parents + parents = entity_operator.create_parents(_hierarchy_text) + print(parents) - # solve parents - parents = entity_operator.create_parents(_hierarchy_text) - print(parents) - - # obtain shot parents entities - _parent = None - for _name, _type in parents: - p_entity = entity_operator.get_ftrack_entity( - session, - _type, - _name, - _parent - ) - print(p_entity) - _parent = p_entity - - # obtain shot ftrack entity - f_s_entity = entity_operator.get_ftrack_entity( - session, - "Shot", - _shot_name, + # obtain shot parents entities + _parent = None + for _name, _type in parents: + p_entity = entity_operator.get_ftrack_entity( + self.session, + _type, + _name, _parent ) - print("Shot entity is: {}".format(f_s_entity)) + print(p_entity) + _parent = p_entity - if not processed: - # first create thumbnail and get version entity - assetversion_entity = component_creator.create_comonent( - f_s_entity, { - "file_path": thumb_fp - } - ) + # obtain shot ftrack entity + f_s_entity = entity_operator.get_ftrack_entity( + self.session, + "Shot", + _shot_name, + _parent + ) + print("Shot entity is: {}".format(f_s_entity)) - # secondly add video to version entity - component_creator.create_comonent( - f_s_entity, { - "file_path": video_fp, - "duration": frame_duration, - "handles": int(handles), - "fps": float(fps) - }, assetversion_entity - ) + if not processed: + # first create thumbnail and get version entity + assetversion_entity = component_creator.create_comonent( + f_s_entity, { + "file_path": thumb_fp + } + ) - # create custom attributtes - custom_attrs = { - "frameStart": frame_start, - "frameEnd": frame_end, - "handleStart": int(handles), - "handleEnd": int(handles), - "resolutionWidth": int(self.width_input.text()), - "resolutionHeight": int(self.height_input.text()), - "pixelAspect": float(self.pixel_aspect_input.text()), - "fps": float(fps) - } + # secondly add video to version entity + component_creator.create_comonent( + f_s_entity, { + "file_path": video_fp, + "duration": frame_duration, + "handles": int(handles), + "fps": float(fps) + }, assetversion_entity + ) - # update custom attributes on shot entity - for key in custom_attrs: - f_s_entity['custom_attributes'][key] = custom_attrs[key] + # create custom attributtes + custom_attrs = { + "frameStart": frame_start, + "frameEnd": frame_end, + "handleStart": int(handles), + "handleEnd": int(handles), + "resolutionWidth": int(self.width_input.text()), + "resolutionHeight": int(self.height_input.text()), + "pixelAspect": float(self.pixel_aspect_input.text()), + "fps": float(fps) + } - task_entity = entity_operator.create_task( - task_type, self.task_types, f_s_entity) + # update custom attributes on shot entity + for key in custom_attrs: + f_s_entity['custom_attributes'][key] = custom_attrs[key] - # Create notes. - user = session.query( - "User where username is \"{}\"".format(session.api_user) - ).first() + task_entity = entity_operator.create_task( + task_type, self.task_types, f_s_entity) - f_s_entity.create_note(shot_description, author=user) + # Create notes. + user = self.session.query( + "User where username is \"{}\"".format(self.session.api_user) + ).first() - if task_description: - task_entity.create_note(task_description, user) + f_s_entity.create_note(shot_description, author=user) - entity_operator.commit() + if task_description: + task_entity.create_note(task_description, user) - component_creator.close() + entity_operator.commit() + + component_creator.close() def _fix_resolution(self): # Center window in linux @@ -507,3 +514,6 @@ class FlameToFtrackPanel(object): shutil.rmtree(self.temp_data_dir) self.temp_data_dir = None print("All Temp data were destroied ...") + + def close(self): + self.session.close() From 36ce07c85e062e4a25857f4bd86d32ad00c29fbd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 Nov 2021 17:14:09 +0100 Subject: [PATCH 061/307] saving cfg on window close --- .../openpype_flame_to_ftrack/modules/panel_app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py index 0e3a4ae4cf..0073771a83 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py @@ -516,4 +516,5 @@ class FlameToFtrackPanel(object): print("All Temp data were destroied ...") def close(self): + self._save_ui_state_to_cfg() self.session.close() From d179bae3214eda0786a4e4b5d94eee42905e205a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 Nov 2021 17:30:05 +0100 Subject: [PATCH 062/307] debugging --- .../openpype_flame_to_ftrack/modules/ftrack_lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py index b074a0acf1..90ff0e267c 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py @@ -92,7 +92,7 @@ class FtrackComponentCreator: def generate_temp_data(self, selection, temp_folder, change_preset_data): - print(">>>>> self.temp_dir: " + self.temp_dir) + print(">>>>> self.temp_dir: " + str(self.temp_dir)) print(">>>>> self.thumbnails: " + str(self.thumbnails)) print(">>>>> self.videos: " + str(self.videos)) @@ -110,12 +110,12 @@ class FtrackComponentCreator: self.temp_dir = tempdir_path return tempdir_path - def get_thumb_path(shot_name): + def get_thumb_path(self, shot_name): # get component files thumb_f = next((f for f in self.thumbnails if shot_name in f), None) return os.path.join(self.temp_dir, thumb_f) - def get_video_path(shot_name): + def get_video_path(self, shot_name): # get component files video_f = next((f for f in self.videos if shot_name in f), None) return os.path.join(self.temp_dir, video_f) From 59a70a29a82d0eae75102666044ac5bb081197f8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 Nov 2021 12:11:50 +0100 Subject: [PATCH 063/307] temp path should be empty at first run --- .../openpype_flame_to_ftrack/modules/ftrack_lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py index 90ff0e267c..18f2261b56 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py @@ -96,7 +96,7 @@ class FtrackComponentCreator: print(">>>>> self.thumbnails: " + str(self.thumbnails)) print(">>>>> self.videos: " + str(self.videos)) - if temp_folder == self.temp_dir: + if self.temp_dir: return temp_folder with app_utils.make_temp_dir() as tempdir_path: From bdbdaf53a6438d3bf6bb9a052b12898b3677d84e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 23 Nov 2021 13:56:58 +0100 Subject: [PATCH 064/307] fix plugin order and op autodetection --- .../publish/collect_default_rr_path.py | 2 +- .../publish/collect_rr_path_from_instance.py | 6 ++-- .../perjob/m50__openpype_publish_render.py | 29 +++++++++---------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/openpype/modules/default_modules/royal_render/plugins/publish/collect_default_rr_path.py b/openpype/modules/default_modules/royal_render/plugins/publish/collect_default_rr_path.py index cdca03bef0..3ce95e0c50 100644 --- a/openpype/modules/default_modules/royal_render/plugins/publish/collect_default_rr_path.py +++ b/openpype/modules/default_modules/royal_render/plugins/publish/collect_default_rr_path.py @@ -6,7 +6,7 @@ import pyblish.api class CollectDefaultRRPath(pyblish.api.ContextPlugin): """Collect default Royal Render path.""" - order = pyblish.api.CollectorOrder + 0.01 + order = pyblish.api.CollectorOrder label = "Default Royal Render Path" def process(self, context): diff --git a/openpype/modules/default_modules/royal_render/plugins/publish/collect_rr_path_from_instance.py b/openpype/modules/default_modules/royal_render/plugins/publish/collect_rr_path_from_instance.py index fb27a76d11..6a3dc276f3 100644 --- a/openpype/modules/default_modules/royal_render/plugins/publish/collect_rr_path_from_instance.py +++ b/openpype/modules/default_modules/royal_render/plugins/publish/collect_rr_path_from_instance.py @@ -5,7 +5,7 @@ import pyblish.api class CollectRRPathFromInstance(pyblish.api.InstancePlugin): """Collect RR Path from instance.""" - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder + 0.01 label = "Royal Render Path from the Instance" families = ["rendering"] @@ -38,8 +38,8 @@ class CollectRRPathFromInstance(pyblish.api.InstancePlugin): if k in default_servers } - except AttributeError: - # Handle situation were we had only one url for deadline. + except (AttributeError, KeyError): + # Handle situation were we had only one url for royal render. return render_instance.context.data["defaultRRPath"] return rr_servers[ diff --git a/openpype/modules/default_modules/royal_render/rr_root/plugins/control_job/perjob/m50__openpype_publish_render.py b/openpype/modules/default_modules/royal_render/rr_root/plugins/control_job/perjob/m50__openpype_publish_render.py index 290f26a44a..7fedb51410 100644 --- a/openpype/modules/default_modules/royal_render/rr_root/plugins/control_job/perjob/m50__openpype_publish_render.py +++ b/openpype/modules/default_modules/royal_render/rr_root/plugins/control_job/perjob/m50__openpype_publish_render.py @@ -38,28 +38,20 @@ class OpenPypeContextSelector: os.environ.get("PROGRAMFILES"), "OpenPype", "openpype_console.exe" ) - if os.path.exists(op_path): - print(" - found OpenPype installation {}".format(op_path)) - else: + if not os.path.exists(op_path): # try to find in user local context op_path = os.path.join( os.environ.get("LOCALAPPDATA"), "Programs", "OpenPype", "openpype_console.exe" ) - if os.path.exists(op_path): - print( - " - found OpenPype installation {}".format( - op_path)) - else: + if not os.path.exists(op_path): raise Exception("Error: OpenPype was not found.") - self.openpype_root = op_path + op_path = os.path.dirname(op_path) + print(" - found OpenPype installation {}".format(op_path)) - # TODO: this should try to find metadata file. Either using - # jobs custom attributes or using environment variable - # or just using plain existence of file. - # self.context = self._process_metadata_file() + self.openpype_root = op_path def _process_metadata_file(self): """Find and process metadata file. @@ -86,8 +78,8 @@ class OpenPypeContextSelector: automatically, no UI will be show and publishing will directly proceed. """ - if not self.context: - self.show() + if not self.context and not self.show(): + return self.context["user"] = self.job.userName self.run_publish() @@ -120,10 +112,15 @@ class OpenPypeContextSelector: not self.context.get("asset") or \ not self.context.get("task"): self._show_rr_warning("Context selection failed.") - return + return False # self.context["app_name"] = self.job.renderer.name + # there should be mapping between OpenPype and Royal Render + # app names and versions, but since app_name is not used + # currently down the line (but it is required by OP publish command + # right now). self.context["app_name"] = "maya/2020" + return True @staticmethod def _show_rr_warning(text): From 4815ce5417f26d8adde4c731c0e21ee7af761210 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 15:46:22 +0100 Subject: [PATCH 065/307] added 'is_oiio_supported' function to vendor bin utils --- openpype/lib/__init__.py | 4 +++- openpype/lib/vendor_bin_utils.py | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index ee4821b80d..d778e2eac8 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -49,7 +49,8 @@ from .vendor_bin_utils import ( get_vendor_bin_path, get_oiio_tools_path, get_ffmpeg_tool_path, - ffprobe_streams + ffprobe_streams, + is_oiio_supported ) from .python_module_tools import ( @@ -185,6 +186,7 @@ __all__ = [ "get_oiio_tools_path", "get_ffmpeg_tool_path", "ffprobe_streams", + "is_oiio_supported", "import_filepath", "modules_from_path", diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index 42f2b34bb2..2c7a1f7198 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -3,6 +3,7 @@ import logging import json import platform import subprocess +import distutils log = logging.getLogger("FFmpeg utils") @@ -105,3 +106,26 @@ def ffprobe_streams(path_to_file, logger=None): )) return json.loads(popen_stdout)["streams"] + + +def is_oiio_supported(): + """Checks if oiiotool is configured for this platform. + + Triggers simple subprocess, handles exception if fails. + + 'should_decompress' will throw exception if configured, + but not present or not working. + + Returns: + bool: OIIO tool executable is available. + """ + loaded_path = oiio_path = get_oiio_tools_path() + if oiio_path: + oiio_path = distutils.spawn.find_executable(oiio_path) + + if not oiio_path: + log.debug("OIIOTool is not configured or not present at {}".format( + loaded_path + )) + return False + return True From d4d799ebc25ec1aafaf765e233cdf8ec8aa822a6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 15:47:23 +0100 Subject: [PATCH 066/307] created new transcoding subfile in openpype lib --- openpype/lib/transcoding.py | 111 ++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 openpype/lib/transcoding.py diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py new file mode 100644 index 0000000000..cd6e600e49 --- /dev/null +++ b/openpype/lib/transcoding.py @@ -0,0 +1,111 @@ +import os +import re +import logging +import tempfile + +from .execute import run_subprocess +from .vendor_bin_utils import ( + get_oiio_tools_path, + is_oiio_supported, + get_ffmpeg_tool_path +) + + +def decompress(target_dir, file_url, + input_frame_start=None, input_frame_end=None, log=None): + """ + Decompresses DWAA 'file_url' .exr to 'target_dir'. + + Creates uncompressed files in 'target_dir', they need to be cleaned. + + File url could be for single file or for a sequence, in that case + %0Xd will be as a placeholder for frame number AND input_frame* will + be filled. + In that case single oiio command with '--frames' will be triggered for + all frames, this should be faster then looping and running sequentially + + Args: + target_dir (str): extended from stagingDir + file_url (str): full urls to source file (with or without %0Xd) + input_frame_start (int) (optional): first frame + input_frame_end (int) (optional): last frame + log (Logger) (optional): pype logger + """ + is_sequence = input_frame_start is not None and \ + input_frame_end is not None and \ + (int(input_frame_end) > int(input_frame_start)) + + oiio_cmd = [] + oiio_cmd.append(get_oiio_tools_path()) + + oiio_cmd.append("--compression none") + + base_file_name = os.path.basename(file_url) + oiio_cmd.append(file_url) + + if is_sequence: + oiio_cmd.append("--frames {}-{}".format(input_frame_start, + input_frame_end)) + + oiio_cmd.append("-o") + oiio_cmd.append(os.path.join(target_dir, base_file_name)) + + subprocess_exr = " ".join(oiio_cmd) + + if not log: + log = logging.getLogger(__name__) + + log.debug("Decompressing {}".format(subprocess_exr)) + run_subprocess( + subprocess_exr, shell=True, logger=log + ) + + +def get_decompress_dir(): + """ + Creates temporary folder for decompressing. + Its local, in case of farm it is 'local' to the farm machine. + + Should be much faster, needs to be cleaned up later. + """ + return os.path.normpath( + tempfile.mkdtemp(prefix="pyblish_tmp_") + ) + + +def should_decompress(file_url): + """ + Tests that 'file_url' is compressed with DWAA. + + Uses 'is_oiio_supported' to check that OIIO tool is available for this + platform. + + Shouldn't throw exception as oiiotool is guarded by check function. + Currently implemented this way as there is no support for Mac and Linux + In the future, it should be more strict and throws exception on + misconfiguration. + + Args: + file_url (str): path to rendered file (in sequence it would be + first file, if that compressed it is expected that whole seq + will be too) + Returns: + (bool): 'file_url' is DWAA compressed and should be decompressed + and we can decompress (oiiotool supported) + """ + if is_oiio_supported(): + try: + output = run_subprocess([ + get_oiio_tools_path(), + "--info", "-v", file_url]) + return "compression: \"dwaa\"" in output or \ + "compression: \"dwab\"" in output + except RuntimeError: + _name, ext = os.path.splitext(file_url) + # TODO: should't the list of allowed extensions be + # taken from an OIIO variable of supported formats + if ext not in [".mxf"]: + # Reraise exception + raise + return False + return False From 6239c06715cdf0552cb1785abe36fd7cd54a3875 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 15:48:29 +0100 Subject: [PATCH 067/307] changed imports and removed transcoding code from plugin tools --- openpype/lib/__init__.py | 17 ++--- openpype/lib/plugin_tools.py | 127 ----------------------------------- 2 files changed, 9 insertions(+), 135 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index d778e2eac8..18e2d8aa6a 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -66,6 +66,11 @@ from .profiles_filtering import ( filter_profiles ) +from .transcoding import ( + decompress, + get_decompress_dir, + should_decompress +) from .avalon_context import ( CURRENT_DOC_SCHEMAS, PROJECT_NAME_ALLOWED_SYMBOLS, @@ -138,10 +143,6 @@ from .plugin_tools import ( source_hash, get_unique_layer_name, get_background_layers, - oiio_supported, - decompress, - get_decompress_dir, - should_decompress ) from .path_tools import ( @@ -194,6 +195,10 @@ __all__ = [ "classes_from_module", "import_module_from_dirpath", + "decompress", + "get_decompress_dir", + "should_decompress", + "CURRENT_DOC_SCHEMAS", "PROJECT_NAME_ALLOWED_SYMBOLS", "PROJECT_NAME_REGEX", @@ -258,10 +263,6 @@ __all__ = [ "source_hash", "get_unique_layer_name", "get_background_layers", - "oiio_supported", - "decompress", - "get_decompress_dir", - "should_decompress", "version_up", "get_version_from_path", diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 891163e3ae..2a859da7cb 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -5,12 +5,8 @@ import inspect import logging import re import json -import tempfile -import distutils -from .execute import run_subprocess from .profiles_filtering import filter_profiles -from .vendor_bin_utils import get_oiio_tools_path from openpype.settings import get_project_settings @@ -425,129 +421,6 @@ def get_background_layers(file_url): return layers -def oiio_supported(): - """ - Checks if oiiotool is configured for this platform. - - Triggers simple subprocess, handles exception if fails. - - 'should_decompress' will throw exception if configured, - but not present or not working. - Returns: - (bool) - """ - oiio_path = get_oiio_tools_path() - if oiio_path: - oiio_path = distutils.spawn.find_executable(oiio_path) - - if not oiio_path: - log.debug("OIIOTool is not configured or not present at {}". - format(oiio_path)) - return False - - return True - - -def decompress(target_dir, file_url, - input_frame_start=None, input_frame_end=None, log=None): - """ - Decompresses DWAA 'file_url' .exr to 'target_dir'. - - Creates uncompressed files in 'target_dir', they need to be cleaned. - - File url could be for single file or for a sequence, in that case - %0Xd will be as a placeholder for frame number AND input_frame* will - be filled. - In that case single oiio command with '--frames' will be triggered for - all frames, this should be faster then looping and running sequentially - - Args: - target_dir (str): extended from stagingDir - file_url (str): full urls to source file (with or without %0Xd) - input_frame_start (int) (optional): first frame - input_frame_end (int) (optional): last frame - log (Logger) (optional): pype logger - """ - is_sequence = input_frame_start is not None and \ - input_frame_end is not None and \ - (int(input_frame_end) > int(input_frame_start)) - - oiio_cmd = [] - oiio_cmd.append(get_oiio_tools_path()) - - oiio_cmd.append("--compression none") - - base_file_name = os.path.basename(file_url) - oiio_cmd.append(file_url) - - if is_sequence: - oiio_cmd.append("--frames {}-{}".format(input_frame_start, - input_frame_end)) - - oiio_cmd.append("-o") - oiio_cmd.append(os.path.join(target_dir, base_file_name)) - - subprocess_exr = " ".join(oiio_cmd) - - if not log: - log = logging.getLogger(__name__) - - log.debug("Decompressing {}".format(subprocess_exr)) - run_subprocess( - subprocess_exr, shell=True, logger=log - ) - - -def get_decompress_dir(): - """ - Creates temporary folder for decompressing. - Its local, in case of farm it is 'local' to the farm machine. - - Should be much faster, needs to be cleaned up later. - """ - return os.path.normpath( - tempfile.mkdtemp(prefix="pyblish_tmp_") - ) - - -def should_decompress(file_url): - """ - Tests that 'file_url' is compressed with DWAA. - - Uses 'oiio_supported' to check that OIIO tool is available for this - platform. - - Shouldn't throw exception as oiiotool is guarded by check function. - Currently implemented this way as there is no support for Mac and Linux - In the future, it should be more strict and throws exception on - misconfiguration. - - Args: - file_url (str): path to rendered file (in sequence it would be - first file, if that compressed it is expected that whole seq - will be too) - Returns: - (bool): 'file_url' is DWAA compressed and should be decompressed - and we can decompress (oiiotool supported) - """ - if oiio_supported(): - try: - output = run_subprocess([ - get_oiio_tools_path(), - "--info", "-v", file_url]) - return "compression: \"dwaa\"" in output or \ - "compression: \"dwab\"" in output - except RuntimeError: - _name, ext = os.path.splitext(file_url) - # TODO: should't the list of allowed extensions be - # taken from an OIIO variable of supported formats - if ext not in [".mxf"]: - # Reraise exception - raise - return False - return False - - def parse_json(path): """Parses json file at 'path' location From 249bcd65c609924bc14046d32d469ba54fd721a7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 16:38:54 +0100 Subject: [PATCH 068/307] reorganization of 'should_compress' --- openpype/lib/transcoding.py | 42 +++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index cd6e600e49..21300cac70 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -6,8 +6,7 @@ import tempfile from .execute import run_subprocess from .vendor_bin_utils import ( get_oiio_tools_path, - is_oiio_supported, - get_ffmpeg_tool_path + is_oiio_supported ) @@ -74,32 +73,35 @@ def get_decompress_dir(): def should_decompress(file_url): - """ - Tests that 'file_url' is compressed with DWAA. + """Tests that 'file_url' is compressed with DWAA. - Uses 'is_oiio_supported' to check that OIIO tool is available for this - platform. + Uses 'is_oiio_supported' to check that OIIO tool is available for this + platform. - Shouldn't throw exception as oiiotool is guarded by check function. - Currently implemented this way as there is no support for Mac and Linux - In the future, it should be more strict and throws exception on - misconfiguration. + Shouldn't throw exception as oiiotool is guarded by check function. + Currently implemented this way as there is no support for Mac and Linux + In the future, it should be more strict and throws exception on + misconfiguration. - Args: - file_url (str): path to rendered file (in sequence it would be - first file, if that compressed it is expected that whole seq - will be too) - Returns: - (bool): 'file_url' is DWAA compressed and should be decompressed - and we can decompress (oiiotool supported) + Args: + file_url (str): path to rendered file (in sequence it would be + first file, if that compressed it is expected that whole seq + will be too) + + Returns: + bool: 'file_url' is DWAA compressed and should be decompressed + and we can decompress (oiiotool supported) """ if is_oiio_supported(): try: output = run_subprocess([ get_oiio_tools_path(), "--info", "-v", file_url]) - return "compression: \"dwaa\"" in output or \ - "compression: \"dwab\"" in output + return ( + "compression: \"dwaa\"" in output + or "compression: \"dwab\"" in output + ) + except RuntimeError: _name, ext = os.path.splitext(file_url) # TODO: should't the list of allowed extensions be @@ -107,5 +109,5 @@ def should_decompress(file_url): if ext not in [".mxf"]: # Reraise exception raise - return False + return False From 8915211af6643965e6624e8684fe46827e1f48fc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 16:40:55 +0100 Subject: [PATCH 069/307] added helper function to call oiio tool and get input info --- openpype/lib/transcoding.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 21300cac70..f3d54a218e 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -111,3 +111,11 @@ def should_decompress(file_url): raise return False + + +def get_oiio_info_for_input(filepath, logger=None): + """Call oiiotool to get information about input and return stdout.""" + args = [ + get_oiio_tools_path(), "--info", "-v", filepath + ] + return run_subprocess(args, logger=logger) From 57ce70d1895bc735272f85f8382732edd06ac7f4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 16:41:10 +0100 Subject: [PATCH 070/307] adde parsing function for oiio data --- openpype/lib/transcoding.py | 51 +++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index f3d54a218e..d18ded9f5f 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -119,3 +119,54 @@ def get_oiio_info_for_input(filepath, logger=None): get_oiio_tools_path(), "--info", "-v", filepath ] return run_subprocess(args, logger=logger) + + +def parse_oiio_info(oiio_info): + """Create an object based on output from oiiotool. + + Removes quotation marks from compression value. Parse channels into + dictionary - key is channel name value is determined type of channel + (e.g. 'uint', 'float'). + + Args: + oiio_info (str): Output of calling "oiiotool --info -v " + + Returns: + dict: Loaded data from output. + """ + lines = [ + line.strip() + for line in oiio_info.split("\n") + ] + # Each line should contain information about one key + # key - value are separated with ": " + oiio_sep = ": " + data_map = {} + for line in lines: + parts = line.split(oiio_sep) + if len(parts) < 2: + continue + key = parts.pop(0) + value = oiio_sep.join(parts) + data_map[key] = value + + if "compression" in data_map: + value = data_map["compression"] + data_map["compression"] = value.replace("\"", "") + + channels_info = {} + channels_value = data_map.get("channel list") or "" + if channels_value: + channels = channels_value.split(", ") + type_regex = re.compile(r"(?P[^\(]+) \((?P[^\)]+)\)") + for channel in channels: + match = type_regex.search(channel) + if not match: + channel_name = channel + channel_type = "uint" + else: + channel_name = match.group("name") + channel_type = match.group("type") + channels_info[channel_name] = channel_type + data_map["channels_info"] = channels_info + return data_map From 8ee3f07a4032bae3dc802913c4b3814c73846b80 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 16:42:42 +0100 Subject: [PATCH 071/307] implemented function which helps determine if can export any channels from input --- openpype/lib/transcoding.py | 65 +++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index d18ded9f5f..efb97283df 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -170,3 +170,68 @@ def parse_oiio_info(oiio_info): channels_info[channel_name] = channel_type data_map["channels_info"] = channels_info return data_map + + +def get_convert_rgb_channels(channels_info): + """Get first available RGB(A) group from channels info. + + ## Examples + ``` + # Ideal situation + channels_info: { + "R": ..., + "G": ..., + "B": ..., + "A": ... + } + ``` + Result will be `("R", "G", "B", "A")` + + ``` + # Not ideal situation + channels_info: { + "beauty.red": ..., + "beuaty.green": ..., + "beauty.blue": ..., + "depth.Z": ... + } + ``` + Result will be `("beauty.red", "beauty.green", "beauty.blue", None)` + + Returns: + NoneType: There is not channel combination that matches RGB + combination. + tuple: Tuple of 4 channel names defying channel names for R, G, B, A + where A can be None. + """ + rgb_by_main_name = collections.defaultdict(dict) + main_name_order = [""] + for channel_name in channels_info.keys(): + name_parts = channel_name.split(".") + rgb_part = name_parts.pop(-1).lower() + main_name = ".".join(name_parts) + if rgb_part in ("r", "red"): + rgb_by_main_name[main_name]["R"] = channel_name + elif rgb_part in ("g", "green"): + rgb_by_main_name[main_name]["G"] = channel_name + elif rgb_part in ("b", "blue"): + rgb_by_main_name[main_name]["B"] = channel_name + elif rgb_part in ("a", "alpha"): + rgb_by_main_name[main_name]["A"] = channel_name + else: + continue + if main_name not in main_name_order: + main_name_order.append(main_name) + + output = None + for main_name in main_name_order: + colors = rgb_by_main_name.get(main_name) or {} + red = colors.get("R") + green = colors.get("G") + blue = colors.get("B") + alpha = colors.get("A") + if red is not None and green is not None and blue is not None: + output = (red, green, blue, alpha) + break + + return output From 3946729c8ed515e7f11068395e9386620c58762e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 16:42:53 +0100 Subject: [PATCH 072/307] added 'should_convert_for_ffmpeg' as extended version of 'should_decompress' --- openpype/lib/transcoding.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index efb97283df..d55373b5d2 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -235,3 +235,39 @@ def get_convert_rgb_channels(channels_info): break return output + + +def should_convert_for_ffmpeg(src_filepath): + """Find out if input should be converted for ffmpeg. + + Currently cares only about exr inputs and is based on OpenImageIO. + + Returns: + bool/NoneType: True if should be converted, False if should not and + None if can't determine. + """ + # Care only about exr at this moment + ext = os.path.splitext(src_filepath)[-1].lower() + if ext != ".exr": + return False + + # Can't determine if should convert or not without oiio_tool + if not is_oiio_supported(): + return None + + # Load info about info from oiio tool + oiio_info = get_oiio_info_for_input(src_filepath) + input_info = parse_oiio_info(oiio_info) + + # Check compression + compression = input_info["compression"] + if compression in ("dwaa", "dwab"): + return True + + # Check channels + channels_info = input_info["channels_info"] + review_channels = get_convert_rgb_channels(channels_info) + if review_channels is None: + return None + + return False From ad7bf3d4bdef7df90f85066281b69d9b738dda94 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 16:43:07 +0100 Subject: [PATCH 073/307] added conversion function --- openpype/lib/transcoding.py | 83 +++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index d55373b5d2..c70cc6cf86 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -271,3 +271,86 @@ def should_convert_for_ffmpeg(src_filepath): return None return False + + +def convert_for_ffmpeg( + first_input_path, + output_dir, + input_frame_start, + input_frame_end, + logger=None +): + """Contert source file to format supported in ffmpeg. + + Currently can convert only exrs. + + Args: + first_input_path (str): Path to first file of a sequence or a single + file path for non-sequential input. + output_dir (str): Path to directory where output will be rendered. + Must not be same as input's directory. + input_frame_start (int): Frame start of input. + input_frame_end (int): Frame end of input. + logger (logging.Logger): Logger used for logging. + + Raises: + ValueError: If input filepath has extension not supported by function. + Currently is supported only ".exr" extension. + """ + if logger is None: + logger = logging.getLogger(__name__) + + ext = os.path.splitext(first_input_path)[1].lower() + if ext != ".exr": + raise ValueError(( + "Function 'convert_for_ffmpeg' currently support only" + " \".exr\" extension. Got \"{}\"." + ).format(ext)) + + is_sequence = False + if input_frame_start is not None and input_frame_end is not None: + is_sequence = int(input_frame_end) != int(input_frame_start) + + oiio_info = get_oiio_info_for_input(first_input_path) + input_info = parse_oiio_info(oiio_info) + + # Change compression only if source compression is "dwaa" or "dwab" + # - they're not supported in ffmpeg + compression = input_info["compression"] + if compression in ("dwaa", "dwab"): + compression = "none" + + # Prepare subprocess arguments + oiio_cmd = [ + get_oiio_tools_path(), + "--compression", compression, + first_input_path + ] + + channels_info = input_info["channels_info"] + review_channels = get_convert_rgb_channels(channels_info) + if review_channels is None: + raise ValueError( + "Couldn't find channels that can be used for conversion." + ) + + red, green, blue, alpha = review_channels + channels_arg = "R={},G={},B={}".format(red, green, blue) + if alpha is not None: + channels_arg += ",A={}".format(alpha) + oiio_cmd.append("--ch") + oiio_cmd.append(channels_arg) + + # Add frame definitions to arguments + if is_sequence: + oiio_cmd.append("--frames") + oiio_cmd.append("{}-{}".format(input_frame_start, input_frame_end)) + + # Add last argument - path to output + base_file_name = os.path.basename(first_input_path) + output_path = os.path.join(output_dir, base_file_name) + oiio_cmd.append("-o") + oiio_cmd.append(output_path) + + logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) + run_subprocess(oiio_cmd, logger=logger) From a4cc0b43eb6cdc4881fa4130bcbb4a4158a3f8b0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 16:43:16 +0100 Subject: [PATCH 074/307] added missing import --- openpype/lib/transcoding.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index c70cc6cf86..80dbc765cb 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1,6 +1,7 @@ import os import re import logging +import collections import tempfile from .execute import run_subprocess From 66ae8a0ccd07c0541a0d992cffc8fe9d2ee3db50 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 16:44:19 +0100 Subject: [PATCH 075/307] added function to create temp transcoding directory --- openpype/lib/transcoding.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 80dbc765cb..8384292d4b 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -114,6 +114,18 @@ def should_decompress(file_url): return False +def get_transcode_temp_directory(): + """Creates temporary folder for transcoding. + + Its local, in case of farm it is 'local' to the farm machine. + + Should be much faster, needs to be cleaned up later. + """ + return os.path.normpath( + tempfile.mkdtemp(prefix="op_transcoding_") + ) + + def get_oiio_info_for_input(filepath, logger=None): """Call oiiotool to get information about input and return stdout.""" args = [ From da4fe6bb49c6edd3f8757a9f44596c83b5f7d425 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 16:46:06 +0100 Subject: [PATCH 076/307] added new functions to openpype.lib --- openpype/lib/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 18e2d8aa6a..96a0e3c97f 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -69,7 +69,10 @@ from .profiles_filtering import ( from .transcoding import ( decompress, get_decompress_dir, - should_decompress + should_decompress, + get_transcode_temp_directory, + should_convert_for_ffmpeg, + convert_for_ffmpeg ) from .avalon_context import ( CURRENT_DOC_SCHEMAS, @@ -198,6 +201,9 @@ __all__ = [ "decompress", "get_decompress_dir", "should_decompress", + "get_transcode_temp_directory", + "should_convert_for_ffmpeg", + "convert_for_ffmpeg", "CURRENT_DOC_SCHEMAS", "PROJECT_NAME_ALLOWED_SYMBOLS", From 337c65ff5db191de2a2e415c68b460f824d244f8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 16:51:23 +0100 Subject: [PATCH 077/307] separated main process into more submethods --- openpype/plugins/publish/extract_review.py | 38 +++++++++++++++------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index ba6ef17072..aecdf4fad3 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -92,7 +92,7 @@ class ExtractReview(pyblish.api.InstancePlugin): if "delete" in tags and "thumbnail" not in tags: instance.data["representations"].remove(repre) - def main_process(self, instance): + def _get_outputs_for_instance(self, instance): host_name = instance.context.data["hostName"] task_name = os.environ["AVALON_TASK"] family = self.main_family_from_instance(instance) @@ -114,24 +114,25 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("Matching profile: \"{}\"".format(json.dumps(profile))) instance_families = self.families_from_instance(instance) - _profile_outputs = self.filter_outputs_by_families( + filtered_outputs = self.filter_outputs_by_families( profile, instance_families ) - if not _profile_outputs: + # Store `filename_suffix` to save arguments + profile_outputs = [] + for filename_suffix, definition in filtered_outputs.items(): + definition["filename_suffix"] = filename_suffix + profile_outputs.append(definition) + + if not filtered_outputs: self.log.info(( "Skipped instance. All output definitions from selected" " profile does not match to instance families. \"{}\"" ).format(str(instance_families))) - return + return profile_outputs - # Store `filename_suffix` to save arguments - profile_outputs = [] - for filename_suffix, definition in _profile_outputs.items(): - definition["filename_suffix"] = filename_suffix - profile_outputs.append(definition) - - # Loop through representations - for repre in tuple(instance.data["representations"]): + def _get_outputs_per_representaions(self, instance, profile_outputs): + outputs_per_representations = [] + for repre in instance.data["representations"]: repre_name = str(repre.get("name")) tags = repre.get("tags") or [] if "review" not in tags: @@ -173,6 +174,19 @@ class ExtractReview(pyblish.api.InstancePlugin): " tags. \"{}\"" ).format(str(tags))) continue + outputs_per_representations.append((repre, outputs)) + return outputs_per_representations + + def main_process(self, instance): + profile_outputs = self._get_outputs_for_instance(instance) + if not profile_outputs: + return + + # Loop through representations + outputs_per_repres = self._get_outputs_per_representaions( + instance, profile_outputs + ) + for repre, outputs in outputs_per_repres: for _output_def in outputs: output_def = copy.deepcopy(_output_def) From c415357b277fb10904b053ca229ba49056cec370 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 17:00:32 +0100 Subject: [PATCH 078/307] make a difference between source staging and destination staging --- openpype/plugins/publish/extract_review.py | 23 +++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index aecdf4fad3..257841c916 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -758,13 +758,14 @@ class ExtractReview(pyblish.api.InstancePlugin): "sequence_file" (if output is sequence) keys to new representation. """ - staging_dir = new_repre["stagingDir"] repre = temp_data["origin_repre"] + src_staging_dir = repre["stagingDir"] + dst_staging_dir = new_repre["stagingDir"] if temp_data["input_is_sequence"]: collections = clique.assemble(repre["files"])[0] full_input_path = os.path.join( - staging_dir, + src_staging_dir, collections[0].format("{head}{padding}{tail}") ) @@ -774,12 +775,12 @@ class ExtractReview(pyblish.api.InstancePlugin): # Make sure to have full path to one input file full_input_path_single_file = os.path.join( - staging_dir, repre["files"][0] + src_staging_dir, repre["files"][0] ) else: full_input_path = os.path.join( - staging_dir, repre["files"] + src_staging_dir, repre["files"] ) filename = os.path.splitext(repre["files"])[0] @@ -825,27 +826,27 @@ class ExtractReview(pyblish.api.InstancePlugin): new_repre["sequence_file"] = repr_file full_output_path = os.path.join( - staging_dir, filename_base, repr_file + dst_staging_dir, filename_base, repr_file ) else: repr_file = "{}_{}.{}".format( filename, filename_suffix, output_ext ) - full_output_path = os.path.join(staging_dir, repr_file) + full_output_path = os.path.join(dst_staging_dir, repr_file) new_repre_files = repr_file # Store files to representation new_repre["files"] = new_repre_files # Make sure stagingDire exists - staging_dir = os.path.normpath(os.path.dirname(full_output_path)) - if not os.path.exists(staging_dir): - self.log.debug("Creating dir: {}".format(staging_dir)) - os.makedirs(staging_dir) + dst_staging_dir = os.path.normpath(os.path.dirname(full_output_path)) + if not os.path.exists(dst_staging_dir): + self.log.debug("Creating dir: {}".format(dst_staging_dir)) + os.makedirs(dst_staging_dir) # Store stagingDir to representaion - new_repre["stagingDir"] = staging_dir + new_repre["stagingDir"] = dst_staging_dir # Store paths to temp data temp_data["full_input_path"] = full_input_path From 675d595558ca9fe877e67cf07beaf88638b9401d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 17:01:11 +0100 Subject: [PATCH 079/307] make sure new representation has same staging as representation had at the start of output definition processing --- openpype/plugins/publish/extract_review.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 257841c916..90ef91e4d4 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -187,6 +187,9 @@ class ExtractReview(pyblish.api.InstancePlugin): instance, profile_outputs ) for repre, outputs in outputs_per_repres: + # Check if input should be preconverted before processing + # Store original staging dir (it's value may change) + src_repre_staging_dir = repre["stagingDir"] for _output_def in outputs: output_def = copy.deepcopy(_output_def) @@ -199,6 +202,10 @@ class ExtractReview(pyblish.api.InstancePlugin): # Create copy of representation new_repre = copy.deepcopy(repre) + # Make sure new representation has origin staging dir + # - this is because source representation may change + # it's staging dir because of ffmpeg conversion + new_repre["stagingDir"] = src_repre_staging_dir # Remove "delete" tag from new repre if there is if "delete" in new_repre["tags"]: From 06ffe0e73805fe7613f026ce8a444dc8d6c63b8c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 17:05:07 +0100 Subject: [PATCH 080/307] skip decompression --- openpype/plugins/publish/extract_review.py | 29 ---------------------- 1 file changed, 29 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 90ef91e4d4..556fa3c886 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -16,9 +16,6 @@ from openpype.lib import ( path_to_subprocess_arg, - should_decompress, - get_decompress_dir, - decompress ) import speedcopy @@ -426,35 +423,9 @@ class ExtractReview(pyblish.api.InstancePlugin): value for value in _ffmpeg_audio_filters if value.strip() ] - if isinstance(new_repre['files'], list): - input_files_urls = [os.path.join(new_repre["stagingDir"], f) for f - in new_repre['files']] - test_path = input_files_urls[0] - else: - test_path = os.path.join( - new_repre["stagingDir"], new_repre['files']) - do_decompress = should_decompress(test_path) - - if do_decompress: - # change stagingDir, decompress first - # calculate all paths with modified directory, used on too many - # places - # will be purged by cleanup.py automatically - orig_staging_dir = new_repre["stagingDir"] - new_repre["stagingDir"] = get_decompress_dir() - # Prepare input and output filepaths self.input_output_paths(new_repre, output_def, temp_data) - if do_decompress: - input_file = temp_data["full_input_path"].\ - replace(new_repre["stagingDir"], orig_staging_dir) - - decompress(new_repre["stagingDir"], input_file, - temp_data["frame_start"], - temp_data["frame_end"], - self.log) - # Set output frames len to 1 when ouput is single image if ( temp_data["output_ext_is_image"] From f47c32daa344a0ba9611276966e82f44e2dbe7bc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 17:10:08 +0100 Subject: [PATCH 081/307] added log of just processed instance --- openpype/plugins/publish/extract_review.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 556fa3c886..8d949ed467 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -174,7 +174,18 @@ class ExtractReview(pyblish.api.InstancePlugin): outputs_per_representations.append((repre, outputs)) return outputs_per_representations + @staticmethod + def get_instance_label(instance): + return ( + getattr(instance, "label", None) + or instance.data.get("label") + or instance.data.get("name") + or str(instance) + ) + def main_process(self, instance): + instance_label = self.get_instance_label(instance) + self.log.debug("Processing instance \"{}\"".format(instance_label)) profile_outputs = self._get_outputs_for_instance(instance) if not profile_outputs: return From a4fee5af79592e17f3f8d32b915d0ee0d1e7f275 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 17:10:46 +0100 Subject: [PATCH 082/307] added conversion to ffmpeg supported formats --- openpype/plugins/publish/extract_review.py | 61 ++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 8d949ed467..9c88cb33c6 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -2,6 +2,7 @@ import os import re import copy import json +import shutil from abc import ABCMeta, abstractmethod import six @@ -16,6 +17,10 @@ from openpype.lib import ( path_to_subprocess_arg, + should_convert_for_ffmpeg, + convert_for_ffmpeg, + get_transcode_temp_directory, + get_temp_directory ) import speedcopy @@ -198,6 +203,54 @@ class ExtractReview(pyblish.api.InstancePlugin): # Check if input should be preconverted before processing # Store original staging dir (it's value may change) src_repre_staging_dir = repre["stagingDir"] + # Receive filepath to first file in representation + first_input_path = None + if not self.input_is_sequence(repre): + first_input_path = os.path.join( + src_repre_staging_dir, repre["files"] + ) + else: + for filename in repre["files"]: + first_input_path = os.path.join( + src_repre_staging_dir, filename + ) + break + + # Skip if file is not set + if first_input_path is None: + self.log.warning(( + "Representation \"{}\" seems to have empty files." + " Skipped." + ).format(repre["name"])) + continue + + # Determine if representation requires pre conversion for ffmpeg + do_convert = should_convert_for_ffmpeg(first_input_path) + # If result is None the requirement of conversion can't be + # determined + if do_convert is None: + self.log.info(( + "Can't determine if representation requires conversion." + " Skipped." + )) + continue + + # Do conversion if needed + # - change staging dir of source representation + # - must be set back after output definitions processing + if do_convert: + new_staging_dir = get_temp_directory() + repre["stagingDir"] = new_staging_dir + + frame_start = instance.data["frameStart"] + frame_end = instance.data["frameEnd"] + convert_for_ffmpeg( + first_input_path, + new_staging_dir, + frame_start, + frame_end, + self.log + ) for _output_def in outputs: output_def = copy.deepcopy(_output_def) @@ -305,6 +358,14 @@ class ExtractReview(pyblish.api.InstancePlugin): ) instance.data["representations"].append(new_repre) + # Cleanup temp staging dir after procesisng of output definitions + if do_convert: + temp_dir = repre["stagingDir"] + shutil.rmtree(temp_dir) + # Set staging dir of source representation back to previous + # value + repre["stagingDir"] = src_repre_staging_dir + def input_is_sequence(self, repre): """Deduce from representation data if input is sequence.""" # TODO GLOBAL ISSUE - Find better way how to find out if input From eeead975b7b2898c3083660b45809e50f99c55f5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 17:10:56 +0100 Subject: [PATCH 083/307] removed multipart condition --- openpype/plugins/publish/extract_review.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 9c88cb33c6..069f2aa032 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -73,18 +73,6 @@ class ExtractReview(pyblish.api.InstancePlugin): if not instance.data.get("review", True): return - # ffmpeg doesn't support multipart exrs - if instance.data.get("multipartExr") is True: - instance_label = ( - getattr(instance, "label", None) - or instance.data.get("label") - or instance.data.get("name") - ) - self.log.info(( - "Instance \"{}\" contain \"multipartExr\". Skipped." - ).format(instance_label)) - return - # Run processing self.main_process(instance) From 30cd2457582ac040d07fda82d0a2c728edb3cc1f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 17:39:40 +0100 Subject: [PATCH 084/307] fixed typo --- openpype/plugins/publish/extract_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 069f2aa032..5a660bb36f 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -120,7 +120,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ).format(str(instance_families))) return profile_outputs - def _get_outputs_per_representaions(self, instance, profile_outputs): + def _get_outputs_per_representations(self, instance, profile_outputs): outputs_per_representations = [] for repre in instance.data["representations"]: repre_name = str(repre.get("name")) @@ -184,7 +184,7 @@ class ExtractReview(pyblish.api.InstancePlugin): return # Loop through representations - outputs_per_repres = self._get_outputs_per_representaions( + outputs_per_repres = self._get_outputs_per_representations( instance, profile_outputs ) for repre, outputs in outputs_per_repres: From 439e1e0ba7fb6787cc915b7bd92921052fbf70a9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 17:39:48 +0100 Subject: [PATCH 085/307] shortened message --- openpype/plugins/publish/extract_review.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 5a660bb36f..febe97636d 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -207,8 +207,7 @@ class ExtractReview(pyblish.api.InstancePlugin): # Skip if file is not set if first_input_path is None: self.log.warning(( - "Representation \"{}\" seems to have empty files." - " Skipped." + "Representation \"{}\" have empty files. Skipped." ).format(repre["name"])) continue From ff1821958626c1cbde9b560450da3e99a00b3baa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 17:42:34 +0100 Subject: [PATCH 086/307] modified extract burnin same way as extract review --- openpype/plugins/publish/extract_burnin.py | 190 +++++++++++++-------- 1 file changed, 119 insertions(+), 71 deletions(-) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index 35d9e4b2f2..c37ccb8c27 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -14,6 +14,11 @@ import openpype import openpype.api from openpype.lib import ( get_pype_execute_args, + + get_temp_directory, + convert_for_ffmpeg, + should_convert_for_ffmpeg, + should_decompress, get_decompress_dir, decompress, @@ -95,6 +100,55 @@ class ExtractBurnin(openpype.api.Extractor): self.log.debug("Removing representation: {}".format(repre)) instance.data["representations"].remove(repre) + def _get_burnins_per_representations(self, instance, src_burnin_defs): + self.log.debug("Filtering of representations and their burnins starts") + + filtered_repres = [] + repres = instance.data.get("representations") or [] + for idx, repre in enumerate(repres): + self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) + if not self.repres_is_valid(repre): + continue + + repre_burnin_links = repre.get("burnins", []) + self.log.debug( + "repre_burnin_links: {}".format(repre_burnin_links) + ) + + burnin_defs = copy.deepcopy(src_burnin_defs) + self.log.debug( + "burnin_defs.keys(): {}".format(burnin_defs.keys()) + ) + + # Filter output definition by `burnin` represetation key + repre_linked_burnins = { + name: output + for name, output in burnin_defs.items() + if name in repre_burnin_links + } + self.log.debug( + "repre_linked_burnins: {}".format(repre_linked_burnins) + ) + + # if any match then replace burnin defs and follow tag filtering + if repre_linked_burnins: + burnin_defs = repre_linked_burnins + + # Filter output definition by representation tags (optional) + repre_burnin_defs = self.filter_burnins_by_tags( + burnin_defs, repre["tags"] + ) + if not repre_burnin_defs: + self.log.info(( + "Skipped representation. All burnin definitions from" + " selected profile does not match to representation's" + " tags. \"{}\"" + ).format(str(repre["tags"]))) + continue + filtered_repres.append((repre, repre_burnin_defs)) + + return filtered_repres + def main_process(self, instance): # TODO get these data from context host_name = instance.context.data["hostName"] @@ -110,8 +164,7 @@ class ExtractBurnin(openpype.api.Extractor): ).format(host_name, family, task_name)) return - self.log.debug("profile: {}".format( - profile)) + self.log.debug("profile: {}".format(profile)) # Pre-filter burnin definitions by instance families burnin_defs = self.filter_burnins_defs(profile, instance) @@ -133,46 +186,10 @@ class ExtractBurnin(openpype.api.Extractor): # Executable args that will execute the script # [pype executable, *pype script, "run"] executable_args = get_pype_execute_args("run", scriptpath) - - for idx, repre in enumerate(tuple(instance.data["representations"])): - self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) - - repre_burnin_links = repre.get("burnins", []) - - if not self.repres_is_valid(repre): - continue - - self.log.debug("repre_burnin_links: {}".format( - repre_burnin_links)) - - self.log.debug("burnin_defs.keys(): {}".format( - burnin_defs.keys())) - - # Filter output definition by `burnin` represetation key - repre_linked_burnins = { - name: output for name, output in burnin_defs.items() - if name in repre_burnin_links - } - self.log.debug("repre_linked_burnins: {}".format( - repre_linked_burnins)) - - # if any match then replace burnin defs and follow tag filtering - _burnin_defs = copy.deepcopy(burnin_defs) - if repre_linked_burnins: - _burnin_defs = repre_linked_burnins - - # Filter output definition by representation tags (optional) - repre_burnin_defs = self.filter_burnins_by_tags( - _burnin_defs, repre["tags"] - ) - if not repre_burnin_defs: - self.log.info(( - "Skipped representation. All burnin definitions from" - " selected profile does not match to representation's" - " tags. \"{}\"" - ).format(str(repre["tags"]))) - continue - + burnins_per_repres = self._get_burnins_per_representations( + instance, burnin_defs + ) + for repre, repre_burnin_defs in burnins_per_repres: # Create copy of `_burnin_data` and `_temp_data` for repre. burnin_data = copy.deepcopy(_burnin_data) temp_data = copy.deepcopy(_temp_data) @@ -180,6 +197,40 @@ class ExtractBurnin(openpype.api.Extractor): # Prepare representation based data. self.prepare_repre_data(instance, repre, burnin_data, temp_data) + src_repre_staging_dir = repre["stagingDir"] + # Should convert representation source files before processing? + if self.input_is_sequence(repre): + filename = repre["files"][0] + else: + filename = repre["files"] + + first_input_path = os.path.join(src_repre_staging_dir, filename) + # Determine if representation requires pre conversion for ffmpeg + do_convert = should_convert_for_ffmpeg(first_input_path) + # If result is None the requirement of conversion can't be + # determined + if do_convert is None: + self.log.info(( + "Can't determine if representation requires conversion." + " Skipped." + )) + continue + + # Do conversion if needed + # - change staging dir of source representation + # - must be set back after output definitions processing + if do_convert: + new_staging_dir = get_temp_directory() + repre["stagingDir"] = new_staging_dir + + convert_for_ffmpeg( + first_input_path, + new_staging_dir, + _temp_data["frameStart"], + _temp_data["frameEnd"], + self.log + ) + # Add anatomy keys to burnin_data. filled_anatomy = anatomy.format_all(burnin_data) burnin_data["anatomy"] = filled_anatomy.get_solved() @@ -199,6 +250,7 @@ class ExtractBurnin(openpype.api.Extractor): files_to_delete = [] for filename_suffix, burnin_def in repre_burnin_defs.items(): new_repre = copy.deepcopy(repre) + new_repre["stagingDir"] = src_repre_staging_dir # Keep "ftrackreview" tag only on first output if first_output: @@ -229,27 +281,9 @@ class ExtractBurnin(openpype.api.Extractor): new_repre["outputName"] = new_name # Prepare paths and files for process. - self.input_output_paths(new_repre, temp_data, filename_suffix) - - decompressed_dir = '' - full_input_path = temp_data["full_input_path"] - do_decompress = should_decompress(full_input_path) - if do_decompress: - decompressed_dir = get_decompress_dir() - - decompress( - decompressed_dir, - full_input_path, - temp_data["frame_start"], - temp_data["frame_end"], - self.log - ) - - # input path changed, 'decompressed' added - input_file = os.path.basename(full_input_path) - temp_data["full_input_path"] = os.path.join( - decompressed_dir, - input_file) + self.input_output_paths( + repre, new_repre, temp_data, filename_suffix + ) # Data for burnin script script_data = { @@ -305,6 +339,14 @@ class ExtractBurnin(openpype.api.Extractor): # Add new representation to instance instance.data["representations"].append(new_repre) + # Cleanup temp staging dir after procesisng of output definitions + if do_convert: + temp_dir = repre["stagingDir"] + shutil.rmtree(temp_dir) + # Set staging dir of source representation back to previous + # value + repre["stagingDir"] = src_repre_staging_dir + # Remove source representation # NOTE we maybe can keep source representation if necessary instance.data["representations"].remove(repre) @@ -317,9 +359,6 @@ class ExtractBurnin(openpype.api.Extractor): os.remove(filepath) self.log.debug("Removed: \"{}\"".format(filepath)) - if do_decompress and os.path.exists(decompressed_dir): - shutil.rmtree(decompressed_dir) - def _get_burnin_options(self): # Prepare burnin options burnin_options = copy.deepcopy(self.default_options) @@ -474,6 +513,12 @@ class ExtractBurnin(openpype.api.Extractor): "Representation \"{}\" don't have \"burnin\" tag. Skipped." ).format(repre["name"])) return False + + if not repre.get("files"): + self.log.warning(( + "Representation \"{}\" have empty files. Skipped." + ).format(repre["name"])) + return False return True def filter_burnins_by_tags(self, burnin_defs, tags): @@ -504,7 +549,9 @@ class ExtractBurnin(openpype.api.Extractor): return filtered_burnins - def input_output_paths(self, new_repre, temp_data, filename_suffix): + def input_output_paths( + self, src_repre, new_repre, temp_data, filename_suffix + ): """Prepare input and output paths for representation. Store data to `temp_data` for keys "full_input_path" which is full path @@ -565,12 +612,13 @@ class ExtractBurnin(openpype.api.Extractor): repre_files = output_filename - stagingdir = new_repre["stagingDir"] + src_stagingdir = src_repre["stagingDir"] + dst_stagingdir = new_repre["stagingDir"] full_input_path = os.path.join( - os.path.normpath(stagingdir), input_filename + os.path.normpath(src_stagingdir), input_filename ).replace("\\", "/") full_output_path = os.path.join( - os.path.normpath(stagingdir), output_filename + os.path.normpath(dst_stagingdir), output_filename ).replace("\\", "/") temp_data["full_input_path"] = full_input_path @@ -587,7 +635,7 @@ class ExtractBurnin(openpype.api.Extractor): if is_sequence: for filename in input_filenames: filepath = os.path.join( - os.path.normpath(stagingdir), filename + os.path.normpath(src_stagingdir), filename ).replace("\\", "/") full_input_paths.append(filepath) From 57b771216781ab4cd140f768f4d830ee544e9630 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 18:18:32 +0100 Subject: [PATCH 087/307] convert extract jpeg as extract review and burnin --- openpype/lib/vendor_bin_utils.py | 5 - openpype/plugins/publish/extract_burnin.py | 7 +- openpype/plugins/publish/extract_jpeg_exr.py | 122 ++++++++++--------- openpype/plugins/publish/extract_review.py | 4 +- 4 files changed, 71 insertions(+), 67 deletions(-) diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index 2c7a1f7198..a5d4153b2a 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -111,11 +111,6 @@ def ffprobe_streams(path_to_file, logger=None): def is_oiio_supported(): """Checks if oiiotool is configured for this platform. - Triggers simple subprocess, handles exception if fails. - - 'should_decompress' will throw exception if configured, - but not present or not working. - Returns: bool: OIIO tool executable is available. """ diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index c37ccb8c27..5526d492c1 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -15,13 +15,10 @@ import openpype.api from openpype.lib import ( get_pype_execute_args, - get_temp_directory, + get_transcode_temp_directory, convert_for_ffmpeg, should_convert_for_ffmpeg, - should_decompress, - get_decompress_dir, - decompress, CREATE_NO_WINDOW ) @@ -220,7 +217,7 @@ class ExtractBurnin(openpype.api.Extractor): # - change staging dir of source representation # - must be set back after output definitions processing if do_convert: - new_staging_dir = get_temp_directory() + new_staging_dir = get_transcode_temp_directory() repre["stagingDir"] = new_staging_dir convert_for_ffmpeg( diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index 3c08c1862d..9909697285 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -7,10 +7,11 @@ from openpype.lib import ( run_subprocess, path_to_subprocess_arg, - should_decompress, - get_decompress_dir, - decompress + get_transcode_temp_directory, + convert_for_ffmpeg, + should_convert_for_ffmpeg ) + import shutil @@ -34,54 +35,49 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): if 'crypto' in instance.data['subset']: return - do_decompress = False - # ffmpeg doesn't support multipart exrs, use oiiotool if available - if instance.data.get("multipartExr") is True: - return - # Skip review when requested. if not instance.data.get("review", True): return - # get representation and loop them - representations = instance.data["representations"] - - # filter out mov and img sequences - representations_new = representations[:] - - for repre in representations: - tags = repre.get("tags", []) - self.log.debug(repre) - valid = 'review' in tags or "thumb-nuke" in tags - if not valid: - continue - - if not isinstance(repre['files'], (list, tuple)): - input_file = repre['files'] + filtered_repres = self._get_filtered_repres(instance) + for repre in filtered_repres: + repre_files = repre["files"] + if not isinstance(repre_files, (list, tuple)): + input_file = repre_files else: - file_index = int(float(len(repre['files'])) * 0.5) - input_file = repre['files'][file_index] + file_index = int(float(len(repre_files)) * 0.5) + input_file = repre_files[file_index] - stagingdir = os.path.normpath(repre.get("stagingDir")) + stagingdir = os.path.normpath(repre["stagingDir"]) - # input_file = ( - # collections[0].format('{head}{padding}{tail}') % start - # ) full_input_path = os.path.join(stagingdir, input_file) self.log.info("input {}".format(full_input_path)) - decompressed_dir = '' - do_decompress = should_decompress(full_input_path) - if do_decompress: - decompressed_dir = get_decompress_dir() + do_convert = should_convert_for_ffmpeg(full_input_path) + # If result is None the requirement of conversion can't be + # determined + if do_convert is None: + self.log.info(( + "Can't determine if representation requires conversion." + " Skipped." + )) + continue - decompress( - decompressed_dir, - full_input_path) - # input path changed, 'decompressed' added - full_input_path = os.path.join( - decompressed_dir, - input_file) + # Do conversion if needed + # - change staging dir of source representation + # - must be set back after output definitions processing + convert_dir = None + if do_convert: + convert_dir = get_transcode_temp_directory() + filename = os.path.basename(full_input_path) + convert_for_ffmpeg( + full_input_path, + convert_dir, + None, + None, + self.log + ) + full_input_path = os.path.join(convert_dir, filename) filename = os.path.splitext(input_file)[0] if not filename.endswith('.'): @@ -124,29 +120,45 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): ) except RuntimeError as exp: if "Compression" in str(exp): - self.log.debug("Unsupported compression on input files. " + - "Skipping!!!") + self.log.debug( + "Unsupported compression on input files. Skipping!!!" + ) return self.log.warning("Conversion crashed", exc_info=True) raise - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'thumbnail', - 'ext': 'jpg', - 'files': jpeg_file, + new_repre = { + "name": "thumbnail", + "ext": "jpg", + "files": jpeg_file, "stagingDir": stagingdir, "thumbnail": True, - "tags": ['thumbnail'] + "tags": ["thumbnail"] } # adding representation - self.log.debug("Adding: {}".format(representation)) - representations_new.append(representation) + self.log.debug("Adding: {}".format(new_repre)) + instance.data["representations"].append(new_repre) - if do_decompress and os.path.exists(decompressed_dir): - shutil.rmtree(decompressed_dir) + # Cleanup temp folder + if convert_dir is not None and os.path.exists(convert_dir): + shutil.rmtree(convert_dir) - instance.data["representations"] = representations_new + def _get_filtered_repres(self, instance): + filtered_repres = [] + src_repres = instance.data.get("representations") or [] + for repre in src_repres: + self.log.debug(repre) + tags = repre.get("tags") or [] + valid = "review" in tags or "thumb-nuke" in tags + if not valid: + continue + + if not repre.get("files"): + self.log.info(( + "Representation \"{}\" don't have files. Skipping" + ).format(repre["name"])) + continue + + filtered_repres.append(repre) + return filtered_repres diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index febe97636d..e7d5451631 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -20,7 +20,7 @@ from openpype.lib import ( should_convert_for_ffmpeg, convert_for_ffmpeg, get_transcode_temp_directory, - get_temp_directory + get_transcode_temp_directory ) import speedcopy @@ -226,7 +226,7 @@ class ExtractReview(pyblish.api.InstancePlugin): # - change staging dir of source representation # - must be set back after output definitions processing if do_convert: - new_staging_dir = get_temp_directory() + new_staging_dir = get_transcode_temp_directory() repre["stagingDir"] = new_staging_dir frame_start = instance.data["frameStart"] From 8b4ceb9c775139f22305c1cec46ec0dd31aed7d1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 18:21:08 +0100 Subject: [PATCH 088/307] removed unused functions --- openpype/lib/__init__.py | 6 --- openpype/lib/transcoding.py | 103 ------------------------------------ 2 files changed, 109 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 96a0e3c97f..efd2cddf7e 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -67,9 +67,6 @@ from .profiles_filtering import ( ) from .transcoding import ( - decompress, - get_decompress_dir, - should_decompress, get_transcode_temp_directory, should_convert_for_ffmpeg, convert_for_ffmpeg @@ -198,9 +195,6 @@ __all__ = [ "classes_from_module", "import_module_from_dirpath", - "decompress", - "get_decompress_dir", - "should_decompress", "get_transcode_temp_directory", "should_convert_for_ffmpeg", "convert_for_ffmpeg", diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 8384292d4b..3d587e2f29 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -11,109 +11,6 @@ from .vendor_bin_utils import ( ) -def decompress(target_dir, file_url, - input_frame_start=None, input_frame_end=None, log=None): - """ - Decompresses DWAA 'file_url' .exr to 'target_dir'. - - Creates uncompressed files in 'target_dir', they need to be cleaned. - - File url could be for single file or for a sequence, in that case - %0Xd will be as a placeholder for frame number AND input_frame* will - be filled. - In that case single oiio command with '--frames' will be triggered for - all frames, this should be faster then looping and running sequentially - - Args: - target_dir (str): extended from stagingDir - file_url (str): full urls to source file (with or without %0Xd) - input_frame_start (int) (optional): first frame - input_frame_end (int) (optional): last frame - log (Logger) (optional): pype logger - """ - is_sequence = input_frame_start is not None and \ - input_frame_end is not None and \ - (int(input_frame_end) > int(input_frame_start)) - - oiio_cmd = [] - oiio_cmd.append(get_oiio_tools_path()) - - oiio_cmd.append("--compression none") - - base_file_name = os.path.basename(file_url) - oiio_cmd.append(file_url) - - if is_sequence: - oiio_cmd.append("--frames {}-{}".format(input_frame_start, - input_frame_end)) - - oiio_cmd.append("-o") - oiio_cmd.append(os.path.join(target_dir, base_file_name)) - - subprocess_exr = " ".join(oiio_cmd) - - if not log: - log = logging.getLogger(__name__) - - log.debug("Decompressing {}".format(subprocess_exr)) - run_subprocess( - subprocess_exr, shell=True, logger=log - ) - - -def get_decompress_dir(): - """ - Creates temporary folder for decompressing. - Its local, in case of farm it is 'local' to the farm machine. - - Should be much faster, needs to be cleaned up later. - """ - return os.path.normpath( - tempfile.mkdtemp(prefix="pyblish_tmp_") - ) - - -def should_decompress(file_url): - """Tests that 'file_url' is compressed with DWAA. - - Uses 'is_oiio_supported' to check that OIIO tool is available for this - platform. - - Shouldn't throw exception as oiiotool is guarded by check function. - Currently implemented this way as there is no support for Mac and Linux - In the future, it should be more strict and throws exception on - misconfiguration. - - Args: - file_url (str): path to rendered file (in sequence it would be - first file, if that compressed it is expected that whole seq - will be too) - - Returns: - bool: 'file_url' is DWAA compressed and should be decompressed - and we can decompress (oiiotool supported) - """ - if is_oiio_supported(): - try: - output = run_subprocess([ - get_oiio_tools_path(), - "--info", "-v", file_url]) - return ( - "compression: \"dwaa\"" in output - or "compression: \"dwab\"" in output - ) - - except RuntimeError: - _name, ext = os.path.splitext(file_url) - # TODO: should't the list of allowed extensions be - # taken from an OIIO variable of supported formats - if ext not in [".mxf"]: - # Reraise exception - raise - - return False - - def get_transcode_temp_directory(): """Creates temporary folder for transcoding. From 3cba06331b3a0a9581bd62a3efdeca691d764c50 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 18:33:05 +0100 Subject: [PATCH 089/307] fix missing method --- openpype/plugins/publish/extract_burnin.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index 5526d492c1..c8b0246dc4 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -196,10 +196,11 @@ class ExtractBurnin(openpype.api.Extractor): src_repre_staging_dir = repre["stagingDir"] # Should convert representation source files before processing? - if self.input_is_sequence(repre): - filename = repre["files"][0] + repre_files = repre["files"] + if isinstance(repre_files, (tuple, list)): + filename = repre_files[0] else: - filename = repre["files"] + filename = repre_files first_input_path = os.path.join(src_repre_staging_dir, filename) # Determine if representation requires pre conversion for ffmpeg From 7a4f3e6987fe9e932770ed874d47e00e1a6ded88 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 25 Nov 2021 18:36:23 +0100 Subject: [PATCH 090/307] removed check of multipartExr key on instance in extract burnin --- openpype/plugins/publish/extract_burnin.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index c8b0246dc4..df7dc47e17 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -72,18 +72,6 @@ class ExtractBurnin(openpype.api.Extractor): options = None def process(self, instance): - # ffmpeg doesn't support multipart exrs - if instance.data.get("multipartExr") is True: - instance_label = ( - getattr(instance, "label", None) - or instance.data.get("label") - or instance.data.get("name") - ) - self.log.info(( - "Instance \"{}\" contain \"multipartExr\". Skipped." - ).format(instance_label)) - return - # QUESTION what is this for and should we raise an exception? if "representations" not in instance.data: raise RuntimeError("Burnin needs already created mov to work on.") From adba50492eac9241064a28a0a436d3fc485c2e32 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 26 Nov 2021 11:52:29 +0100 Subject: [PATCH 091/307] improving efectivity --- .../modules/ftrack_lib.py | 12 +----- .../modules/panel_app.py | 41 +++++++++---------- 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py index 18f2261b56..16ad41ebfa 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py @@ -90,21 +90,13 @@ class FtrackComponentCreator: self.session = session self._get_ftrack_location() - - def generate_temp_data(self, selection, temp_folder, change_preset_data): - print(">>>>> self.temp_dir: " + str(self.temp_dir)) - print(">>>>> self.thumbnails: " + str(self.thumbnails)) - print(">>>>> self.videos: " + str(self.videos)) - - if self.temp_dir: - return temp_folder - + def generate_temp_data(self, selection, change_preset_data): with app_utils.make_temp_dir() as tempdir_path: for seq in selection: app_utils.export_thumbnail( seq, tempdir_path, change_preset_data) app_utils.export_video(seq, tempdir_path, change_preset_data) - temp_files = os.listdir(temp_folder) + temp_files = os.listdir(tempdir_path) self.thumbnails = [f for f in temp_files if "jpg" in f] self.videos = [f for f in temp_files if "mov" in f] self.temp_dir = tempdir_path diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py index 0073771a83..cedd5d6d02 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py @@ -1,3 +1,9 @@ +from ftrack_lib import ( + get_ftrack_session, + FtrackEntityOperator, + FtrackComponentCreator +) +import app_utils import os from PySide2 import QtWidgets, QtCore import uiwidgets @@ -5,17 +11,12 @@ import flame import ftrack_lib reload(ftrack_lib) -import app_utils reload(app_utils) -from ftrack_lib import ( - get_ftrack_session, - FtrackEntityOperator, - FtrackComponentCreator -) class MainWindow(QtWidgets.QWidget): can_close = True + def __init__(self, klass, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self.panel_class = klass @@ -29,6 +30,7 @@ class MainWindow(QtWidgets.QWidget): # now the panel can be closed event.accept() + class FlameToFtrackPanel(object): session = None temp_data_dir = None @@ -77,23 +79,16 @@ class FlameToFtrackPanel(object): self.window.setWindowTitle('Sequence Shots to Ftrack') self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) self.window.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.window.setFocusPolicy(QtCore.Qt.StrongFocus) self.window.setStyleSheet('background-color: #313131') - print(1) self._create_project_widget() - print(2) self._create_tree_widget() - print(3) self._set_sequence_params() - print(4) self._generate_widgets() - print(5) self._generate_layouts() - print(6) self._timeline_info() - print(7) self._fix_resolution() - print(8) self.window.show() @@ -323,13 +318,15 @@ class FlameToFtrackPanel(object): self.session, self.project_entity) component_creator = FtrackComponentCreator(self.session) - self.temp_data_dir = component_creator.generate_temp_data( - self.selection, - self.temp_data_dir, - { - "nbHandles": handles - } - ) + if not self.temp_data_dir: + self.window.hide() + self.temp_data_dir = component_creator.generate_temp_data( + self.selection, + { + "nbHandles": handles + } + ) + self.window.show() # Get all selected items from treewidget for item in self.tree.selectedItems(): @@ -349,6 +346,8 @@ class FlameToFtrackPanel(object): video_fp = component_creator.get_video_path(shot_name) print("processed comps: {}".format(self.processed_components)) + print("processed thumb_fp: {}".format(thumb_fp)) + processed = False if thumb_fp not in self.processed_components: self.processed_components.append(thumb_fp) From 5ff76141fc72eb2b18a2fab58b5e9e9a62b92737 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 26 Nov 2021 18:20:59 +0100 Subject: [PATCH 092/307] make sure splash and tray icon are painted before triggering logic --- openpype/tools/tray/pype_tray.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 0f817d7130..1751911581 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -370,6 +370,8 @@ class PypeTrayStarter(QtCore.QObject): splash = self._get_splash() splash.show() self._tray_widget.show() + # Make sure tray and splash are painted out + QtWidgets.QApplication.processEvents() elif self._timer_counter == 1: self._timer_counter += 1 From 0e37641ec002e765cc3683d2942488ecbb98c5ce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 29 Nov 2021 11:46:11 +0100 Subject: [PATCH 093/307] added second processing of events --- openpype/tools/tray/pype_tray.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 1751911581..8c6a6d3266 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -374,6 +374,8 @@ class PypeTrayStarter(QtCore.QObject): QtWidgets.QApplication.processEvents() elif self._timer_counter == 1: + # Second processing of events to make sure splash is painted + QtWidgets.QApplication.processEvents() self._timer_counter += 1 self._tray_widget.initialize_modules() From 41c1f6a2d150ccdfa882b4e6a7e8003c694e1d3d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 29 Nov 2021 13:09:00 +0100 Subject: [PATCH 094/307] reuse already available variables --- openpype/lib/avalon_context.py | 13 +++++++------ .../publish/collect_anatomy_context_data.py | 16 +++++++--------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 581e4b9dbd..8fb2543412 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -479,16 +479,17 @@ def get_workdir_data(project_doc, asset_doc, task_name, host_name): Returns: dict: Data prepared for filling workdir template. """ - hierarchy = "/".join(asset_doc["data"]["parents"]) - task_type = asset_doc['data']['tasks'].get(task_name, {}).get('type') project_task_types = project_doc["config"]["tasks"] task_code = project_task_types.get(task_type, {}).get("short_name") - parent = project_doc["name"] - if len(asset_doc["data"]["parents"]) != 0: - parent = asset_doc["data"]["parents"][-1] + asset_parents = asset_doc["data"]["parents"] + hierarchy = "/".join(asset_parents) + + parent_name = project_doc["name"] + if asset_parents: + parent_name = asset_parents[-1] data = { "project": { @@ -501,7 +502,7 @@ def get_workdir_data(project_doc, asset_doc, task_name, host_name): "short": task_code, }, "asset": asset_doc["name"], - "parent": parent, + "parent": parent_name, "app": host_name, "user": getpass.getuser(), "hierarchy": hierarchy, diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index b0c9eea576..ae8a879fba 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -49,20 +49,18 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): project_entity = context.data["projectEntity"] asset_entity = context.data["assetEntity"] - hierarchy_items = asset_entity["data"]["parents"] - hierarchy = "" - if hierarchy_items: - hierarchy = os.path.join(*hierarchy_items) - asset_tasks = asset_entity["data"]["tasks"] task_type = asset_tasks.get(task_name, {}).get("type") project_task_types = project_entity["config"]["tasks"] task_code = project_task_types.get(task_type, {}).get("short_name") - parent = project_entity["name"] - if len(asset_entity["data"]["parents"]) != 0: - parent = asset_entity["data"]["parents"][-1] + asset_parents = asset_entity["data"]["parents"] + hierarchy = "/".join(asset_parents) + + parent_name = project_entity["name"] + if asset_parents: + parent_name = asset_parents[-1] context_data = { "project": { @@ -70,7 +68,7 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): "code": project_entity["data"].get("code") }, "asset": asset_entity["name"], - "parent": parent, + "parent": parent_name, "hierarchy": hierarchy.replace("\\", "/"), "task": { "name": task_name, From 5c9a83b55dd79e9ddac6d678d4ad10cffed41bd8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 29 Nov 2021 13:09:12 +0100 Subject: [PATCH 095/307] added missing update of parent --- openpype/plugins/publish/collect_anatomy_instance_data.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py index da6a2195ee..74b556e28a 100644 --- a/openpype/plugins/publish/collect_anatomy_instance_data.py +++ b/openpype/plugins/publish/collect_anatomy_instance_data.py @@ -242,7 +242,11 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): asset_doc = instance.data.get("assetEntity") if asset_doc and asset_doc["_id"] != context_asset_doc["_id"]: parents = asset_doc["data"].get("parents") or list() + parent_name = project_doc["name"] + if parents: + parent_name = parents[-1] anatomy_updates["hierarchy"] = "/".join(parents) + anatomy_updates["parent"] = parent_name # Task task_name = instance.data.get("task") From 853ba0efb37a464b7ee2db0781a43ffee9a132d1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 29 Nov 2021 13:10:06 +0100 Subject: [PATCH 096/307] removed conflict marks from docstring --- website/docs/admin_settings_project_anatomy.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/website/docs/admin_settings_project_anatomy.md b/website/docs/admin_settings_project_anatomy.md index e897b0ffab..5cb396264e 100644 --- a/website/docs/admin_settings_project_anatomy.md +++ b/website/docs/admin_settings_project_anatomy.md @@ -57,14 +57,11 @@ We have a few required anatomy templates for OpenPype to work properly, however | `project[code]` | Project's code | | `hierarchy` | All hierarchical parents as subfolders | | `asset` | Name of asset or shot | -<<<<<<< HEAD | `task[name]` | Name of task | | `task[type]` | Type of task | | `task[short]` | Shortname of task | -======= | `parent` | Name of parent folder | | `task` | Name of task | ->>>>>>> add7db0c0... add parent asset to doc | `version` | Version number | | `subset` | Subset name | | `family` | Main family name | From c6916ef96cd7470a4e24668c0d6b5951363c7149 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 29 Nov 2021 13:11:17 +0100 Subject: [PATCH 097/307] modified description of parent --- website/docs/admin_settings_project_anatomy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/admin_settings_project_anatomy.md b/website/docs/admin_settings_project_anatomy.md index 5cb396264e..a8be77d25b 100644 --- a/website/docs/admin_settings_project_anatomy.md +++ b/website/docs/admin_settings_project_anatomy.md @@ -60,7 +60,7 @@ We have a few required anatomy templates for OpenPype to work properly, however | `task[name]` | Name of task | | `task[type]` | Type of task | | `task[short]` | Shortname of task | -| `parent` | Name of parent folder | +| `parent` | Name of hierarchical parent | | `task` | Name of task | | `version` | Version number | | `subset` | Subset name | From 741f326935f73a45f6b15da8185ee3d2c3c7ba70 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 29 Nov 2021 13:13:01 +0100 Subject: [PATCH 098/307] removed not needed replacement --- openpype/plugins/publish/collect_anatomy_context_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index ae8a879fba..07de1b4420 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -69,7 +69,7 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): }, "asset": asset_entity["name"], "parent": parent_name, - "hierarchy": hierarchy.replace("\\", "/"), + "hierarchy": hierarchy, "task": { "name": task_name, "type": task_type, From 77cdd406108f7fa0b4506153d196f8f237a96946 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 29 Nov 2021 13:16:24 +0100 Subject: [PATCH 099/307] changed variable name in workfiles tool --- openpype/tools/workfiles/app.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index 4253f7450a..987b886864 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -85,9 +85,10 @@ class NameWindow(QtWidgets.QDialog): project_task_types = project_doc["config"]["tasks"] task_short = project_task_types.get(task_type, {}).get("short_name") - parent = project_doc["name"] - if len(asset_doc["data"]["parents"]) != 0: - parent = asset_doc["data"]["parents"][-1] + asset_parents = asset_doc["data"]["parents"] + parent_name = project_doc["name"] + if asset_parents: + parent_name = asset_parents[-1] self.data = { "project": { @@ -100,7 +101,7 @@ class NameWindow(QtWidgets.QDialog): "type": task_type, "short": task_short, }, - "parent": parent, + "parent": parent_name, "version": 1, "user": getpass.getuser(), "comment": "", From 20d5ac6ccb76862e459b5ca056116376366781b8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 29 Nov 2021 18:29:50 +0100 Subject: [PATCH 100/307] fix pop with defying object out of pop scope --- openpype/hosts/flame/api/lib.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 48331dcbc2..f6eb0c59e7 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -8,6 +8,9 @@ from openpype.api import Logger log = Logger().get_logger(__name__) +# Dumb object to know if argument was passed where None is valid value +_dumb_obj = object() + @contextlib.contextmanager def io_preferences_file(klass, filepath, write=False): @@ -47,8 +50,8 @@ class FlameAppFramework(object): def setdefault(self, k, default=None): return self.master[self.name].setdefault(k, default) - def pop(self, k, v=object()): - if v is object(): + def pop(self, k, v=_dumb_obj): + if v is _dumb_obj: return self.master[self.name].pop(k) return self.master[self.name].pop(k, v) From 1408a2f241f99cebaba1aa9fbce27e5889276328 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 29 Nov 2021 20:07:27 +0100 Subject: [PATCH 101/307] apply qt context over tools --- openpype/tools/utils/host_tools.py | 115 ++++++++++++++++------------- 1 file changed, 62 insertions(+), 53 deletions(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index ef1cd3cf5c..60c9e79829 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -5,6 +5,7 @@ use singleton approach with global functions (using helper anyway). """ import avalon.api +from .lib import qt_app_context class HostToolsHelper: @@ -61,22 +62,23 @@ class HostToolsHelper: if save is None: save = True - workfiles_tool = self.get_workfiles_tool(parent) - workfiles_tool.set_save_enabled(save) + with qt_app_context(): + workfiles_tool = self.get_workfiles_tool(parent) + workfiles_tool.set_save_enabled(save) - if not workfiles_tool.isVisible(): - workfiles_tool.show() + if not workfiles_tool.isVisible(): + workfiles_tool.show() - if use_context: - context = { - "asset": avalon.api.Session["AVALON_ASSET"], - "task": avalon.api.Session["AVALON_TASK"] - } - workfiles_tool.set_context(context) + if use_context: + context = { + "asset": avalon.api.Session["AVALON_ASSET"], + "task": avalon.api.Session["AVALON_TASK"] + } + workfiles_tool.set_context(context) - # Pull window to the front. - workfiles_tool.raise_() - workfiles_tool.activateWindow() + # Pull window to the front. + workfiles_tool.raise_() + workfiles_tool.activateWindow() def get_loader_tool(self, parent): """Create, cache and return loader tool window.""" @@ -90,20 +92,21 @@ class HostToolsHelper: def show_loader(self, parent=None, use_context=None): """Loader tool for loading representations.""" - loader_tool = self.get_loader_tool(parent) + with qt_app_context(): + loader_tool = self.get_loader_tool(parent) - loader_tool.show() - loader_tool.raise_() - loader_tool.activateWindow() + loader_tool.show() + loader_tool.raise_() + loader_tool.activateWindow() - if use_context is None: - use_context = False + if use_context is None: + use_context = False - if use_context: - context = {"asset": avalon.api.Session["AVALON_ASSET"]} - loader_tool.set_context(context, refresh=True) - else: - loader_tool.refresh() + if use_context: + context = {"asset": avalon.api.Session["AVALON_ASSET"]} + loader_tool.set_context(context, refresh=True) + else: + loader_tool.refresh() def get_creator_tool(self, parent): """Create, cache and return creator tool window.""" @@ -117,13 +120,14 @@ class HostToolsHelper: def show_creator(self, parent=None): """Show tool to create new instantes for publishing.""" - creator_tool = self.get_creator_tool(parent) - creator_tool.refresh() - creator_tool.show() + with qt_app_context(): + creator_tool = self.get_creator_tool(parent) + creator_tool.refresh() + creator_tool.show() - # Pull window to the front. - creator_tool.raise_() - creator_tool.activateWindow() + # Pull window to the front. + creator_tool.raise_() + creator_tool.activateWindow() def get_subset_manager_tool(self, parent): """Create, cache and return subset manager tool window.""" @@ -139,12 +143,13 @@ class HostToolsHelper: def show_subset_manager(self, parent=None): """Show tool display/remove existing created instances.""" - subset_manager_tool = self.get_subset_manager_tool(parent) - subset_manager_tool.show() + with qt_app_context(): + subset_manager_tool = self.get_subset_manager_tool(parent) + subset_manager_tool.show() - # Pull window to the front. - subset_manager_tool.raise_() - subset_manager_tool.activateWindow() + # Pull window to the front. + subset_manager_tool.raise_() + subset_manager_tool.activateWindow() def get_scene_inventory_tool(self, parent): """Create, cache and return scene inventory tool window.""" @@ -160,13 +165,14 @@ class HostToolsHelper: def show_scene_inventory(self, parent=None): """Show tool maintain loaded containers.""" - scene_inventory_tool = self.get_scene_inventory_tool(parent) - scene_inventory_tool.show() - scene_inventory_tool.refresh() + with qt_app_context(): + scene_inventory_tool = self.get_scene_inventory_tool(parent) + scene_inventory_tool.show() + scene_inventory_tool.refresh() - # Pull window to the front. - scene_inventory_tool.raise_() - scene_inventory_tool.activateWindow() + # Pull window to the front. + scene_inventory_tool.raise_() + scene_inventory_tool.activateWindow() def get_library_loader_tool(self, parent): """Create, cache and return library loader tool window.""" @@ -182,11 +188,12 @@ class HostToolsHelper: def show_library_loader(self, parent=None): """Loader tool for loading representations from library project.""" - library_loader_tool = self.get_library_loader_tool(parent) - library_loader_tool.show() - library_loader_tool.raise_() - library_loader_tool.activateWindow() - library_loader_tool.refresh() + with qt_app_context(): + library_loader_tool = self.get_library_loader_tool(parent) + library_loader_tool.show() + library_loader_tool.raise_() + library_loader_tool.activateWindow() + library_loader_tool.refresh() def show_publish(self, parent=None): """Publish UI.""" @@ -207,9 +214,10 @@ class HostToolsHelper: """Look manager is Maya specific tool for look management.""" from avalon import style - look_assigner_tool = self.get_look_assigner_tool(parent) - look_assigner_tool.show() - look_assigner_tool.setStyleSheet(style.load_stylesheet()) + with qt_app_context(): + look_assigner_tool = self.get_look_assigner_tool(parent) + look_assigner_tool.show() + look_assigner_tool.setStyleSheet(style.load_stylesheet()) def get_experimental_tools_dialog(self, parent=None): """Dialog of experimental tools. @@ -232,11 +240,12 @@ class HostToolsHelper: def show_experimental_tools_dialog(self, parent=None): """Show dialog with experimental tools.""" - dialog = self.get_experimental_tools_dialog(parent) + with qt_app_context(): + dialog = self.get_experimental_tools_dialog(parent) - dialog.show() - dialog.raise_() - dialog.activateWindow() + dialog.show() + dialog.raise_() + dialog.activateWindow() def get_tool_by_name(self, tool_name, parent=None, *args, **kwargs): """Show tool by it's name. From 53c83b3b92b66bffea83078d9db72f99b5f2b103 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 30 Nov 2021 10:20:49 +0100 Subject: [PATCH 102/307] Fix - remove wrongly used host for hook Host left here after debugging, causing issue in openpype_gui --- openpype/hooks/pre_foundry_apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hooks/pre_foundry_apps.py b/openpype/hooks/pre_foundry_apps.py index 7df1a6a833..85f68c6b60 100644 --- a/openpype/hooks/pre_foundry_apps.py +++ b/openpype/hooks/pre_foundry_apps.py @@ -13,7 +13,7 @@ class LaunchFoundryAppsWindows(PreLaunchHook): # Should be as last hook because must change launch arguments to string order = 1000 - app_groups = ["nuke", "nukex", "hiero", "nukestudio", "photoshop"] + app_groups = ["nuke", "nukex", "hiero", "nukestudio"] platforms = ["windows"] def execute(self): From ed5a8dc3f8843871826ce9886f96940e87ab3fcf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 30 Nov 2021 12:56:29 +0100 Subject: [PATCH 103/307] adding collector for generated data --- .../openpype_flame_to_ftrack/modules/ftrack_lib.py | 14 +++++++++----- .../openpype_flame_to_ftrack/modules/panel_app.py | 3 +++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py index 16ad41ebfa..e3dad79a67 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py @@ -96,11 +96,15 @@ class FtrackComponentCreator: app_utils.export_thumbnail( seq, tempdir_path, change_preset_data) app_utils.export_video(seq, tempdir_path, change_preset_data) - temp_files = os.listdir(tempdir_path) - self.thumbnails = [f for f in temp_files if "jpg" in f] - self.videos = [f for f in temp_files if "mov" in f] - self.temp_dir = tempdir_path - return tempdir_path + + return tempdir_path + + def collect_generated_data(self, tempdir_path): + temp_files = os.listdir(tempdir_path) + self.thumbnails = [f for f in temp_files if "jpg" in f] + self.videos = [f for f in temp_files if "mov" in f] + self.temp_dir = tempdir_path + def get_thumb_path(self, shot_name): # get component files diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py index cedd5d6d02..692a0fe850 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py @@ -328,6 +328,9 @@ class FlameToFtrackPanel(object): ) self.window.show() + # collect generated files to list data for farther use + component_creator.collect_generated_data(self.temp_data_dir) + # Get all selected items from treewidget for item in self.tree.selectedItems(): # frame ranges From 9ed9cf9ffe3e649d83e2be8806ffe78f69372635 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 30 Nov 2021 13:20:48 +0100 Subject: [PATCH 104/307] fix problem with location --- .../modules/ftrack_lib.py | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py index e3dad79a67..54c809660b 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py @@ -14,6 +14,7 @@ FTRACK_API_KEY = None FTRACK_API_USER = None FTRACK_SERVER = None +_ftrack_api = None def import_ftrack_api(): try: @@ -31,6 +32,7 @@ def get_ftrack_session(): import os ftrack_api = import_ftrack_api() + _ftrack_api = ftrack_api # fill your own credentials url = FTRACK_SERVER or os.getenv("FTRACK_SERVER") or "" user = FTRACK_API_USER or os.getenv("FTRACK_API_USER") or "" @@ -179,6 +181,28 @@ class FtrackComponentCreator: origin_location = self._get_ftrack_location("ftrack.origin") location = data.pop("location") + + + entity["file_type"] = data["file_type"] + + origin_location.add_component( + entity, data["file_path"] + ) + + self._remove_component_from_location(entity, location) + + try: + # Add components to location. + location.add_component( + entity, origin_location, recursive=True) + except _ftrack_api.exception.ComponentInLocationError: + self._remove_component_from_location(entity, origin_location) + # Add components to location. + location.add_component( + entity, origin_location, recursive=True) + + + def _remove_component_from_location(self, location, entity): # Removing existing members from location components = list(entity.get("members", [])) components += [entity] @@ -200,16 +224,6 @@ class FtrackComponentCreator: if "members" in entity.keys(): entity["members"] = [] - entity["file_type"] = data["file_type"] - - origin_location.add_component( - entity, data["file_path"] - ) - - # Add components to location. - location.add_component( - entity, origin_location, recursive=True) - def _get_assettype(self, data): return self.session.query( self._query("AssetType", data)).first() From d8ce82b9167de1f9808c13197f6f6db924ffaa67 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 30 Nov 2021 14:01:11 +0100 Subject: [PATCH 105/307] add changes needed for Maya --- openpype/hosts/maya/plugins/publish/collect_render.py | 7 +++++-- .../deadline/plugins/publish/submit_publish_job.py | 9 +++++++++ openpype/plugins/publish/extract_jpeg_exr.py | 6 +++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 580d459a90..345f5264b7 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -192,7 +192,10 @@ class CollectMayaRender(pyblish.api.ContextPlugin): render_products = layer_render_products.layer_data.products assert render_products, "no render products generated" exp_files = [] + multipart = False for product in render_products: + if product.multipart: + multipart = True product_name = product.productName if product.camera and layer_render_products.has_camera_token(): product_name = "{}{}".format( @@ -205,7 +208,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): }) self.log.info("multipart: {}".format( - layer_render_products.multipart)) + multipart)) assert exp_files, "no file names were generated, this is bug" self.log.info(exp_files) @@ -300,7 +303,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "subset": expected_layer_name, "attachTo": attach_to, "setMembers": layer_name, - "multipartExr": layer_render_products.multipart, + "multipartExr": multipart, "review": render_instance.data.get("review") or False, "publish": True, diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py index 1e158bda9b..516bd755d0 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py @@ -445,9 +445,14 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): preview = True break + if instance_data.get("multipartExr"): + preview = True + new_instance = copy(instance_data) new_instance["subset"] = subset_name new_instance["subsetGroup"] = group_name + if preview: + new_instance["review"] = True # create represenation if isinstance(col, (list, tuple)): @@ -527,6 +532,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if bake_renders: preview = False + # toggle preview on if multipart is on + if instance.get("multipartExr", False): + preview = True + staging = os.path.dirname(list(collection)[0]) success, rootless_staging_dir = ( self.anatomy.find_root_template_from_path(staging) diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index 9909697285..3cb4f8f9cb 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -32,11 +32,15 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): def process(self, instance): self.log.info("subset {}".format(instance.data['subset'])) + + # skip crypto passes. if 'crypto' in instance.data['subset']: + self.log.info("Skipping crypto passes.") return - # Skip review when requested. + # Skip if review not set. if not instance.data.get("review", True): + self.log.info("Skipping - no review set on instance.") return filtered_repres = self._get_filtered_repres(instance) From 5795636af55067be12e3992408d40c23c339c1a4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 30 Nov 2021 14:39:43 +0100 Subject: [PATCH 106/307] OP-2042 - updates to db dumps and loads --- tests/lib/db_handler.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/lib/db_handler.py b/tests/lib/db_handler.py index 9be70895da..4dde5ba46e 100644 --- a/tests/lib/db_handler.py +++ b/tests/lib/db_handler.py @@ -136,7 +136,8 @@ class DBHandler: print("Dropping {} database".format(db_name)) self.client.drop_database(db_name) - def backup_to_dump(self, db_name, dump_dir, overwrite=False): + def backup_to_dump(self, db_name, dump_dir, overwrite=False, + collection=None): """ Helper method for running mongodump for specific 'db_name' """ @@ -148,7 +149,8 @@ class DBHandler: raise RuntimeError("Backup already exists, " "run with overwrite=True") - query = self._dump_query(self.uri, dump_dir, db_name=db_name) + query = self._dump_query(self.uri, dump_dir, + db_name=db_name, collection=collection) print("Mongodump query:: {}".format(query)) subprocess.run(query) @@ -187,7 +189,8 @@ class DBHandler: drop_part = "--drop" if db_name_out: - db_part += " --nsTo={}.*".format(db_name_out) + collection_str = collection or '*' + db_part += " --nsTo={}.{}".format(db_name_out, collection_str) query = "\"{}\" --uri=\"{}\" --dir=\"{}\" {} {} {}".format( "mongorestore", uri, dump_dir, db_part, coll_part, drop_part @@ -217,12 +220,12 @@ class DBHandler: return query -# handler = DBHandler(uri="mongodb://localhost:27017") +#handler = DBHandler(uri="mongodb://localhost:27017") # -# backup_dir = "c:\\projects\\dumps" +#backup_dir = "c:\\projects\\test_nuke_publish\\input\\dumps" # # -# handler.backup_to_dump("openpype", backup_dir, True) -# # handler.setup_from_dump("test_db", backup_dir, True) +#handler.backup_to_dump("avalon", backup_dir, True, collection="test_project") +#handler.setup_from_dump("test_db", backup_dir, True, db_name_out="avalon", collection="test_project") # # handler.setup_from_sql_file("test_db", "c:\\projects\\sql\\item.sql", # # collection="test_project", # # drop=False, mode="upsert") From 3fa0b39df196524ad4dd8b013e90ebe02a59e1b6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 30 Nov 2021 14:40:01 +0100 Subject: [PATCH 107/307] OP-2042 - wip testing in Nuke --- .../hosts/nuke/test_publish_in_nuke.py | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tests/integration/hosts/nuke/test_publish_in_nuke.py diff --git a/tests/integration/hosts/nuke/test_publish_in_nuke.py b/tests/integration/hosts/nuke/test_publish_in_nuke.py new file mode 100644 index 0000000000..3f3f191ac7 --- /dev/null +++ b/tests/integration/hosts/nuke/test_publish_in_nuke.py @@ -0,0 +1,94 @@ +import pytest +import os +import shutil + +from tests.lib.testing_classes import PublishTest + + +class TestPublishInNuke(PublishTest): + """Basic test case for publishing in Nuke + + Uses generic TestCase to prepare fixtures for test data, testing DBs, + env vars. + + Opens Maya, run publish on prepared workile. + + Then checks content of DB (if subset, version, representations were + created. + Checks tmp folder if all expected files were published. + + """ + PERSIST = True + + TEST_FILES = [ + ("1Bciy2pCwMKl1UIpxuPnlX_LHMo_Xkq0K", "test_Nuke_publish.zip", "") + ] + + APP = "Nuke" + APP_VARIANT = "12" + + APP_NAME = "{}/{}".format(APP, APP_VARIANT) + + TIMEOUT = 120 # publish timeout + + @pytest.fixture(scope="module") + def last_workfile_path(self, download_test_data): + """Get last_workfile_path from source data. + + Maya expects workfile in proper folder, so copy is done first. + """ + src_path = os.path.join(download_test_data, + "input", + "workfile", + "test_project_test_asset_TestTask_v001.psd") + dest_folder = os.path.join(download_test_data, + self.PROJECT, + self.ASSET, + "work", + self.TASK) + os.makedirs(dest_folder) + dest_path = os.path.join(dest_folder, + "test_project_test_asset_TestTask_v001.psd") + shutil.copy(src_path, dest_path) + + yield dest_path + + @pytest.fixture(scope="module") + def startup_scripts(self, monkeypatch_session, download_test_data): + """Points Maya to userSetup file from input data""" + pass + + def test_db_asserts(self, dbcon, publish_finished): + """Host and input data dependent expected results in DB.""" + print("test_db_asserts") + assert 5 == dbcon.count_documents({"type": "version"}), \ + "Not expected no of versions" + + assert 0 == dbcon.count_documents({"type": "version", + "name": {"$ne": 1}}), \ + "Only versions with 1 expected" + + assert 1 == dbcon.count_documents({"type": "subset", + "name": "modelMain"}), \ + "modelMain subset must be present" + + assert 1 == dbcon.count_documents({"type": "subset", + "name": "workfileTest_task"}), \ + "workfileTest_task subset must be present" + + assert 11 == dbcon.count_documents({"type": "representation"}), \ + "Not expected no of representations" + + assert 2 == dbcon.count_documents({"type": "representation", + "context.subset": "modelMain", + "context.ext": "abc"}), \ + "Not expected no of representations with ext 'abc'" + + assert 2 == dbcon.count_documents({"type": "representation", + "context.subset": "modelMain", + "context.ext": "ma"}), \ + "Not expected no of representations with ext 'abc'" + + +if __name__ == "__main__": + test_case = TestPublishInNuke() From 746504004401a3f435bff82130af3ae13ec9ab87 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 30 Nov 2021 17:33:19 +0100 Subject: [PATCH 108/307] update of avalon-core --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index 7e5efd6885..9499f6517a 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 7e5efd6885330d84bb8495975bcab84df49bfa3d +Subproject commit 9499f6517a1ff2d3bf94c5d34c0aece146734760 From 23ecee1cf0daa598d63eaf1efabd7c6c5e0b3460 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 30 Nov 2021 17:54:46 +0100 Subject: [PATCH 109/307] fixing remove component removing module ftrack api attribute --- .../modules/ftrack_lib.py | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py index 54c809660b..8321521d98 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py @@ -14,8 +14,6 @@ FTRACK_API_KEY = None FTRACK_API_USER = None FTRACK_SERVER = None -_ftrack_api = None - def import_ftrack_api(): try: import ftrack_api @@ -32,7 +30,6 @@ def get_ftrack_session(): import os ftrack_api = import_ftrack_api() - _ftrack_api = ftrack_api # fill your own credentials url = FTRACK_SERVER or os.getenv("FTRACK_SERVER") or "" user = FTRACK_API_USER or os.getenv("FTRACK_API_USER") or "" @@ -181,34 +178,37 @@ class FtrackComponentCreator: origin_location = self._get_ftrack_location("ftrack.origin") location = data.pop("location") - + self._remove_component_from_location(entity, location) entity["file_type"] = data["file_type"] - origin_location.add_component( - entity, data["file_path"] - ) - - self._remove_component_from_location(entity, location) - try: + origin_location.add_component( + entity, data["file_path"] + ) # Add components to location. location.add_component( entity, origin_location, recursive=True) - except _ftrack_api.exception.ComponentInLocationError: + except Exception as __e: + print("Error: {}".format(__e)) self._remove_component_from_location(entity, origin_location) + origin_location.add_component( + entity, data["file_path"] + ) # Add components to location. location.add_component( entity, origin_location, recursive=True) - def _remove_component_from_location(self, location, entity): + def _remove_component_from_location(self, entity, location): + print(location) # Removing existing members from location components = list(entity.get("members", [])) components += [entity] for component in components: - for loc in component["component_locations"]: + for loc in component.get("component_locations", []): if location["id"] == loc["location_id"]: + print("<< Removing component: {}".format(component)) location.remove_component( component, recursive=False ) @@ -216,6 +216,7 @@ class FtrackComponentCreator: # Deleting existing members on component entity for member in entity.get("members", []): self.session.delete(member) + print("<< Deleting member: {}".format(member)) del(member) self._commit() @@ -302,8 +303,8 @@ class FtrackComponentCreator: self.session.commit() except Exception: tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() + # self.session.rollback() + # self.session._configure_locations() six.reraise(tp, value, tb) def _get_ftrack_location(self, name=None): From 63a911d60d14611571246959444256351e69267d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 30 Nov 2021 20:47:25 +0100 Subject: [PATCH 110/307] Fix - remove setting of stdout and stderr Was causing issues in starting in openpype_console or openpype_gui --- openpype/hooks/pre_non_python_host_launch.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/hooks/pre_non_python_host_launch.py b/openpype/hooks/pre_non_python_host_launch.py index 0447f4a06f..848ed675a8 100644 --- a/openpype/hooks/pre_non_python_host_launch.py +++ b/openpype/hooks/pre_non_python_host_launch.py @@ -49,7 +49,3 @@ class NonPythonHostHook(PreLaunchHook): if remainders: self.launch_context.launch_args.extend(remainders) - # This must be set otherwise it wouldn't be possible to catch output - # when build OpenPype is used. - self.launch_context.kwargs["stdout"] = subprocess.DEVNULL - self.launch_context.kwargs["stderr"] = subprocess.DEVNULL From a6ada2fbe031c8ce373a0bf680bf3da3307744cc Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 1 Dec 2021 03:40:47 +0000 Subject: [PATCH 111/307] [Automated] Bump version --- CHANGELOG.md | 43 ++++++++++++++++++++++++------------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9405ff759..37a53d341c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.7.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.7.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.4...HEAD) @@ -10,26 +10,37 @@ **🆕 New features** -- Store typed version dependencies for workfiles [\#2192](https://github.com/pypeclub/OpenPype/pull/2192) +- Settings UI use OpenPype styles [\#2296](https://github.com/pypeclub/OpenPype/pull/2296) **🚀 Enhancements** +- Assets Widget: Clear model on project change [\#2345](https://github.com/pypeclub/OpenPype/pull/2345) +- General: OpenPype default modules hierarchy [\#2338](https://github.com/pypeclub/OpenPype/pull/2338) +- General: FFprobe error exception contain original error message [\#2328](https://github.com/pypeclub/OpenPype/pull/2328) +- Resolve: Add experimental button to menu [\#2325](https://github.com/pypeclub/OpenPype/pull/2325) - Hiero: Add experimental tools action [\#2323](https://github.com/pypeclub/OpenPype/pull/2323) - Input links: Cleanup and unification of differences [\#2322](https://github.com/pypeclub/OpenPype/pull/2322) +- General: Don't validate vendor bin with executing them [\#2317](https://github.com/pypeclub/OpenPype/pull/2317) - General: Run process log stderr as info log level [\#2309](https://github.com/pypeclub/OpenPype/pull/2309) +- General: Reduce vendor imports [\#2305](https://github.com/pypeclub/OpenPype/pull/2305) - Tools: Cleanup of unused classes [\#2304](https://github.com/pypeclub/OpenPype/pull/2304) - Project Manager: Added ability to delete project [\#2298](https://github.com/pypeclub/OpenPype/pull/2298) - Ftrack: Synchronize input links [\#2287](https://github.com/pypeclub/OpenPype/pull/2287) - StandalonePublisher: Remove unused plugin ExtractHarmonyZip [\#2277](https://github.com/pypeclub/OpenPype/pull/2277) - Ftrack: Support multiple reviews [\#2271](https://github.com/pypeclub/OpenPype/pull/2271) - Ftrack: Remove unused clean component plugin [\#2269](https://github.com/pypeclub/OpenPype/pull/2269) +- Royal Render: Support for rr channels in separate dirs [\#2268](https://github.com/pypeclub/OpenPype/pull/2268) - Houdini: Add experimental tools action [\#2267](https://github.com/pypeclub/OpenPype/pull/2267) -- Tools: Assets widget [\#2265](https://github.com/pypeclub/OpenPype/pull/2265) - Nuke: extract baked review videos presets [\#2248](https://github.com/pypeclub/OpenPype/pull/2248) - TVPaint: Workers rendering [\#2209](https://github.com/pypeclub/OpenPype/pull/2209) **🐛 Bug fixes** +- Maya Look Assigner: Fix Python 3 compatibility [\#2343](https://github.com/pypeclub/OpenPype/pull/2343) +- Tools: Use Qt context on tools show [\#2340](https://github.com/pypeclub/OpenPype/pull/2340) +- Timers Manager: Disable auto stop timer on linux platform [\#2334](https://github.com/pypeclub/OpenPype/pull/2334) +- nuke: bake preset single input exception [\#2331](https://github.com/pypeclub/OpenPype/pull/2331) +- Hiero: fixing multiple templates at a hierarchy parent [\#2330](https://github.com/pypeclub/OpenPype/pull/2330) - Fix - provider icons are pulled from a folder [\#2326](https://github.com/pypeclub/OpenPype/pull/2326) - InputLinks: Typo in "inputLinks" key [\#2314](https://github.com/pypeclub/OpenPype/pull/2314) - Deadline timeout and logging [\#2312](https://github.com/pypeclub/OpenPype/pull/2312) @@ -44,6 +55,10 @@ - Bug: fix variable name \_asset\_id in workfiles application [\#2274](https://github.com/pypeclub/OpenPype/pull/2274) - Version handling fixes [\#2272](https://github.com/pypeclub/OpenPype/pull/2272) +**Merged pull requests:** + +- Maya: configurable model top level validation [\#2321](https://github.com/pypeclub/OpenPype/pull/2321) + ## [3.6.4](https://github.com/pypeclub/OpenPype/tree/3.6.4) (2021-11-23) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.7.0-nightly.1...3.6.4) @@ -66,11 +81,12 @@ **🚀 Enhancements** -- Royal Render: Support for rr channels in separate dirs [\#2268](https://github.com/pypeclub/OpenPype/pull/2268) +- Tools: Assets widget [\#2265](https://github.com/pypeclub/OpenPype/pull/2265) - SceneInventory: Choose loader in asset switcher [\#2262](https://github.com/pypeclub/OpenPype/pull/2262) - Style: New fonts in OpenPype style [\#2256](https://github.com/pypeclub/OpenPype/pull/2256) - Tools: SceneInventory in OpenPype [\#2255](https://github.com/pypeclub/OpenPype/pull/2255) - Tools: Tasks widget [\#2251](https://github.com/pypeclub/OpenPype/pull/2251) +- Tools: Creator in OpenPype [\#2244](https://github.com/pypeclub/OpenPype/pull/2244) - Added endpoint for configured extensions [\#2221](https://github.com/pypeclub/OpenPype/pull/2221) **🐛 Bug fixes** @@ -81,15 +97,16 @@ - Maya: Render publishing fails on linux [\#2260](https://github.com/pypeclub/OpenPype/pull/2260) - LookAssigner: Fix tool reopen [\#2259](https://github.com/pypeclub/OpenPype/pull/2259) - Standalone: editorial not publishing thumbnails on all subsets [\#2258](https://github.com/pypeclub/OpenPype/pull/2258) -- Loader doesn't allow changing of version before loading [\#2254](https://github.com/pypeclub/OpenPype/pull/2254) - Burnins: Support mxf metadata [\#2247](https://github.com/pypeclub/OpenPype/pull/2247) -- Maya: Support for configurable AOV separator characters [\#2197](https://github.com/pypeclub/OpenPype/pull/2197) -- Maya: texture colorspace modes in looks [\#2195](https://github.com/pypeclub/OpenPype/pull/2195) ## [3.6.1](https://github.com/pypeclub/OpenPype/tree/3.6.1) (2021-11-16) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.1-nightly.1...3.6.1) +**🐛 Bug fixes** + +- Loader doesn't allow changing of version before loading [\#2254](https://github.com/pypeclub/OpenPype/pull/2254) + ## [3.6.0](https://github.com/pypeclub/OpenPype/tree/3.6.0) (2021-11-15) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.0-nightly.6...3.6.0) @@ -97,11 +114,9 @@ ### 📖 Documentation - Add alternative sites for Site Sync [\#2206](https://github.com/pypeclub/OpenPype/pull/2206) -- Add command line way of running site sync server [\#2188](https://github.com/pypeclub/OpenPype/pull/2188) **🚀 Enhancements** -- Tools: Creator in OpenPype [\#2244](https://github.com/pypeclub/OpenPype/pull/2244) - Tools: Subset manager in OpenPype [\#2243](https://github.com/pypeclub/OpenPype/pull/2243) - General: Skip module directories without init file [\#2239](https://github.com/pypeclub/OpenPype/pull/2239) - General: Static interfaces [\#2238](https://github.com/pypeclub/OpenPype/pull/2238) @@ -114,13 +129,6 @@ - Maya : Validate shape zero [\#2212](https://github.com/pypeclub/OpenPype/pull/2212) - Maya : validate unique names [\#2211](https://github.com/pypeclub/OpenPype/pull/2211) - Tools: OpenPype stylesheet in workfiles tool [\#2208](https://github.com/pypeclub/OpenPype/pull/2208) -- Ftrack: Replace Queue with deque in event handlers logic [\#2204](https://github.com/pypeclub/OpenPype/pull/2204) -- Tools: New select context dialog [\#2200](https://github.com/pypeclub/OpenPype/pull/2200) -- Maya : Validate mesh ngons [\#2199](https://github.com/pypeclub/OpenPype/pull/2199) -- Dirmap in Nuke [\#2198](https://github.com/pypeclub/OpenPype/pull/2198) -- Delivery: Check 'frame' key in template for sequence delivery [\#2196](https://github.com/pypeclub/OpenPype/pull/2196) -- Settings: Site sync project settings improvement [\#2193](https://github.com/pypeclub/OpenPype/pull/2193) -- Usage of tools code [\#2185](https://github.com/pypeclub/OpenPype/pull/2185) **🐛 Bug fixes** @@ -133,9 +141,6 @@ - Ftrack: Base event fix of 'get\_project\_from\_entity' method [\#2214](https://github.com/pypeclub/OpenPype/pull/2214) - Maya : multiple subsets review broken [\#2210](https://github.com/pypeclub/OpenPype/pull/2210) - Fix - different command used for Linux and Mac OS [\#2207](https://github.com/pypeclub/OpenPype/pull/2207) -- Tools: Workfiles tool don't use avalon widgets [\#2205](https://github.com/pypeclub/OpenPype/pull/2205) -- Ftrack: Fill missing ftrack id on mongo project [\#2203](https://github.com/pypeclub/OpenPype/pull/2203) -- Project Manager: Fix copying of tasks [\#2191](https://github.com/pypeclub/OpenPype/pull/2191) ## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17) diff --git a/openpype/version.py b/openpype/version.py index 2e9592f57d..5160fbdfb8 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.7.0-nightly.3" +__version__ = "3.7.0-nightly.4" diff --git a/pyproject.toml b/pyproject.toml index ac1d133561..86b40762e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.7.0-nightly.3" # OpenPype +version = "3.7.0-nightly.4" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 4306aa01849d9d464d6fbae86f2eeed2873feae3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Dec 2021 11:01:42 +0100 Subject: [PATCH 112/307] make studi paths read only --- openpype/tools/settings/local_settings/apps_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/local_settings/apps_widget.py b/openpype/tools/settings/local_settings/apps_widget.py index 850e009937..6f58a68fff 100644 --- a/openpype/tools/settings/local_settings/apps_widget.py +++ b/openpype/tools/settings/local_settings/apps_widget.py @@ -65,7 +65,7 @@ class AppVariantWidget(QtWidgets.QWidget): for item in studio_executables: path_widget = QtWidgets.QLineEdit(content_widget) path_widget.setText(item.value) - path_widget.setEnabled(False) + path_widget.setReadOnly(True) content_layout.addWidget(path_widget) def update_local_settings(self, value): From 3bd94db78a23e52d95c080b229609834e51e47f6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Dec 2021 11:02:01 +0100 Subject: [PATCH 113/307] style the input to look like disabled --- openpype/style/style.css | 14 ++++++++++++++ .../tools/settings/local_settings/apps_widget.py | 1 + 2 files changed, 15 insertions(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index a60c3592d7..19245cdc40 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1194,3 +1194,17 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { #ImageButton:disabled { background: {color:bg-buttons-disabled}; } + +/* Input field that looks like disabled +- QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit +- usage: QLineEdit that is not editable but has selectable color + */ +#LikeDisabledInput { + background: {color:bg-inputs-disabled}; +} +#LikeDisabledInput:hover { + border-color: {color:border}; +} +#LikeDisabledInput:focus { + border-color: {color:border}; +} diff --git a/openpype/tools/settings/local_settings/apps_widget.py b/openpype/tools/settings/local_settings/apps_widget.py index 6f58a68fff..f06e1ec109 100644 --- a/openpype/tools/settings/local_settings/apps_widget.py +++ b/openpype/tools/settings/local_settings/apps_widget.py @@ -64,6 +64,7 @@ class AppVariantWidget(QtWidgets.QWidget): for item in studio_executables: path_widget = QtWidgets.QLineEdit(content_widget) + path_widget.setObjectName("LikeDisabledInput") path_widget.setText(item.value) path_widget.setReadOnly(True) content_layout.addWidget(path_widget) From bf704068c1c7bd1f207d2e4950c39b80ddce3a2a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Dec 2021 11:17:13 +0100 Subject: [PATCH 114/307] do the pop without dummy object --- openpype/hosts/flame/api/lib.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index f6eb0c59e7..89e020b329 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -8,9 +8,6 @@ from openpype.api import Logger log = Logger().get_logger(__name__) -# Dumb object to know if argument was passed where None is valid value -_dumb_obj = object() - @contextlib.contextmanager def io_preferences_file(klass, filepath, write=False): @@ -50,10 +47,8 @@ class FlameAppFramework(object): def setdefault(self, k, default=None): return self.master[self.name].setdefault(k, default) - def pop(self, k, v=_dumb_obj): - if v is _dumb_obj: - return self.master[self.name].pop(k) - return self.master[self.name].pop(k, v) + def pop(self, *args, **kwargs): + return self.master[self.name].pop(*args, **kwargs) def update(self, mapping=(), **kwargs): self.master[self.name].update(mapping, **kwargs) From 13785ca7add51d647cd32a69b30585c9043e7bb5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Dec 2021 11:23:36 +0100 Subject: [PATCH 115/307] finally solving issue with ftrack session closed needed to remove all modules and re-import them on start --- .../modules/ftrack_lib.py | 5 +-- .../modules/panel_app.py | 43 ++++++++++--------- .../openpype_flame_to_ftrack.py | 20 +++++---- 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py index 8321521d98..1ceba18e57 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py @@ -6,7 +6,6 @@ import json from contextlib import contextmanager import app_utils -reload(app_utils) # Fill following constants or set them via environment variable FTRACK_MODULE_PATH = None @@ -14,6 +13,7 @@ FTRACK_API_KEY = None FTRACK_API_USER = None FTRACK_SERVER = None + def import_ftrack_api(): try: import ftrack_api @@ -104,7 +104,6 @@ class FtrackComponentCreator: self.videos = [f for f in temp_files if "mov" in f] self.temp_dir = tempdir_path - def get_thumb_path(self, shot_name): # get component files thumb_f = next((f for f in self.thumbnails if shot_name in f), None) @@ -162,7 +161,7 @@ class FtrackComponentCreator: # get or create assetversion entity from session assetversion_entity = self._get_assetversion({ - "version": 1, + "version": 0, "asset": asset_entity }) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py index 692a0fe850..d73a5c7013 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py @@ -1,21 +1,26 @@ -from ftrack_lib import ( - get_ftrack_session, - FtrackEntityOperator, - FtrackComponentCreator -) -import app_utils import os from PySide2 import QtWidgets, QtCore + import uiwidgets -import flame - +import app_utils import ftrack_lib -reload(ftrack_lib) -reload(app_utils) +def clear_inner_modules(): + import sys + + if "ftrack_lib" in sys.modules.keys(): + del sys.modules["ftrack_lib"] + print("Ftrack Lib module removed from sys.modules") + + if "app_utils" in sys.modules.keys(): + del sys.modules["app_utils"] + print("app_utils module removed from sys.modules") + + if "uiwidgets" in sys.modules.keys(): + del sys.modules["uiwidgets"] + print("uiwidgets module removed from sys.modules") class MainWindow(QtWidgets.QWidget): - can_close = True def __init__(self, klass, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) @@ -26,7 +31,7 @@ class MainWindow(QtWidgets.QWidget): print("Removing temp data") self.panel_class.clear_temp_data() self.panel_class.close() - + clear_inner_modules() # now the panel can be closed event.accept() @@ -65,13 +70,8 @@ class FlameToFtrackPanel(object): def __init__(self, selection): print(selection) - print(self.processed_components) - - self.session = get_ftrack_session() - - self.processed_components = [] - print(self.processed_components) + self.session = ftrack_lib.get_ftrack_session() self.selection = selection self.window = MainWindow(self) # creating ui @@ -225,7 +225,7 @@ class FlameToFtrackPanel(object): cfg_d["create_task_type"], self.task_types.keys(), self.window) def _create_project_widget(self): - + import flame # get project name from flame current project self.project_name = flame.project.current_project.name @@ -314,9 +314,9 @@ class FlameToFtrackPanel(object): # get resolution from gui inputs fps = self.fps_input.text() - entity_operator = FtrackEntityOperator( + entity_operator = ftrack_lib.FtrackEntityOperator( self.session, self.project_entity) - component_creator = FtrackComponentCreator(self.session) + component_creator = ftrack_lib.FtrackComponentCreator(self.session) if not self.temp_data_dir: self.window.hide() @@ -517,6 +517,7 @@ class FlameToFtrackPanel(object): self.temp_data_dir = None print("All Temp data were destroied ...") + def close(self): self._save_ui_state_to_cfg() self.session.close() diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py index 3c1063c445..688b8b6ae3 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py @@ -3,15 +3,19 @@ from __future__ import print_function import os import sys -try: - import panel_app - reload(panel_app) -except ImportError: - SCRIPT_DIR = os.path.dirname(__file__) - PACKAGE_DIR = os.path.join(SCRIPT_DIR, "modules") - sys.path.append(PACKAGE_DIR) +SCRIPT_DIR = os.path.dirname(__file__) +PACKAGE_DIR = os.path.join(SCRIPT_DIR, "modules") +sys.path.append(PACKAGE_DIR) + + +def flame_panel_executor(selection): + if "panel_app" in sys.modules.keys(): + print("panel_app module is already loaded") + del sys.modules["panel_app"] + print("panel_app module removed from sys.modules") import panel_app + panel_app.FlameToFtrackPanel(selection) def scope_sequence(selection): @@ -27,7 +31,7 @@ def get_media_panel_custom_ui_actions(): { "name": "Create Shots", "isVisible": scope_sequence, - "execute": panel_app.FlameToFtrackPanel + "execute": flame_panel_executor } ] } From 6048ebfcea3a6ca46441d12bf1349acb5ed53f97 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Dec 2021 11:26:52 +0100 Subject: [PATCH 116/307] code clean --- .../openpype_flame_to_ftrack/modules/panel_app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py index d73a5c7013..9e39147776 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py @@ -1,10 +1,10 @@ -import os from PySide2 import QtWidgets, QtCore import uiwidgets import app_utils import ftrack_lib + def clear_inner_modules(): import sys @@ -20,6 +20,7 @@ def clear_inner_modules(): del sys.modules["uiwidgets"] print("uiwidgets module removed from sys.modules") + class MainWindow(QtWidgets.QWidget): def __init__(self, klass, *args, **kwargs): @@ -74,6 +75,7 @@ class FlameToFtrackPanel(object): self.session = ftrack_lib.get_ftrack_session() self.selection = selection self.window = MainWindow(self) + # creating ui self.window.setMinimumSize(1500, 600) self.window.setWindowTitle('Sequence Shots to Ftrack') @@ -517,7 +519,6 @@ class FlameToFtrackPanel(object): self.temp_data_dir = None print("All Temp data were destroied ...") - def close(self): self._save_ui_state_to_cfg() self.session.close() From e6ab772dd74e8141ea0c1e7defce95921c2cb946 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Dec 2021 13:16:45 +0100 Subject: [PATCH 117/307] fix import of constant --- openpype/tools/standalonepublish/widgets/widget_family.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/standalonepublish/widgets/widget_family.py b/openpype/tools/standalonepublish/widgets/widget_family.py index 682a6fc974..1e20028392 100644 --- a/openpype/tools/standalonepublish/widgets/widget_family.py +++ b/openpype/tools/standalonepublish/widgets/widget_family.py @@ -10,7 +10,7 @@ from openpype.api import ( Creator ) from openpype.lib import TaskNotSetError -from avalon.tools.creator.app import SubsetAllowedSymbols +from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS class FamilyWidget(QtWidgets.QWidget): @@ -223,7 +223,7 @@ class FamilyWidget(QtWidgets.QWidget): # QUESTION should Creator care about this and here should be # only validated with schema regex? subset_name = re.sub( - "[^{}]+".format(SubsetAllowedSymbols), + "[^{}]+".format(SUBSET_NAME_ALLOWED_SYMBOLS), "", subset_name ) From 753e7f9591588c76cafe87cfb890aff518ba4c9b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Dec 2021 13:31:42 +0100 Subject: [PATCH 118/307] remove testing only from app settings --- openpype/settings/defaults/system_settings/applications.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index cc80a94d3f..782424f15b 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -502,7 +502,7 @@ "environment": {} }, "__dynamic_keys_labels__": { - "13-0": "13.0 (Testing only)", + "13-0": "13.0", "12-2": "12.2", "12-0": "12.0", "11-3": "11.3", @@ -639,7 +639,7 @@ "environment": {} }, "__dynamic_keys_labels__": { - "13-0": "13.0 (Testing only)", + "13-0": "13.0", "12-2": "12.2", "12-0": "12.0", "11-3": "11.3", From c700395709aab52986d90abde1a441cb2cf5bcad Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Dec 2021 13:34:12 +0100 Subject: [PATCH 119/307] fixing host directory - converting single quotation to double - converting py2 prints to py3 --- openpype/hosts/hiero/__init__.py | 2 +- openpype/hosts/hiero/api/lib.py | 8 +- .../hosts/hiero/{ => api}/otio/__init__.py | 0 .../hiero/{ => api}/otio/hiero_export.py | 9 +- .../hiero/{ => api}/otio/hiero_import.py | 0 openpype/hosts/hiero/{ => api}/otio/utils.py | 0 .../startup/HieroPlayer/PlayerPresets.hrox | 0 .../startup/Icons/1_add_handles_end.png | Bin .../{ => api}/startup/Icons/2_add_handles.png | Bin .../hiero/{ => api}/startup/Icons/3D.png | Bin .../startup/Icons/3_add_handles_start.png | Bin .../hiero/{ => api}/startup/Icons/4_2D.png | Bin .../hiero/{ => api}/startup/Icons/edit.png | Bin .../hiero/{ => api}/startup/Icons/fusion.png | Bin .../{ => api}/startup/Icons/hierarchy.png | Bin .../hiero/{ => api}/startup/Icons/houdini.png | Bin .../hiero/{ => api}/startup/Icons/layers.psd | Bin .../hiero/{ => api}/startup/Icons/lense.png | Bin .../hiero/{ => api}/startup/Icons/lense1.png | Bin .../hiero/{ => api}/startup/Icons/maya.png | Bin .../hiero/{ => api}/startup/Icons/nuke.png | Bin .../{ => api}/startup/Icons/pype_icon.png | Bin .../{ => api}/startup/Icons/resolution.png | Bin .../{ => api}/startup/Icons/resolution.psd | Bin .../{ => api}/startup/Icons/retiming.png | Bin .../{ => api}/startup/Icons/retiming.psd | Bin .../hiero/{ => api}/startup/Icons/review.png | Bin .../hiero/{ => api}/startup/Icons/review.psd | Bin .../hiero/{ => api}/startup/Icons/volume.png | Bin .../{ => api}/startup/Icons/z_layer_bg.png | Bin .../{ => api}/startup/Icons/z_layer_fg.png | Bin .../{ => api}/startup/Icons/z_layer_main.png | Bin .../Python/Startup/SpreadsheetExport.py | 20 +- .../startup/Python/Startup/Startup.py | 0 .../Startup/otioexporter/OTIOExportTask.py | 3 +- .../Startup/otioexporter/OTIOExportUI.py | 12 +- .../Python/Startup/otioexporter/__init__.py | 7 + .../startup/Python/Startup/project_helpers.py | 53 +-- .../Python/Startup/selection_tracker.py | 9 + .../startup/Python/Startup/setFrameRate.py | 54 +-- .../Python/Startup/version_everywhere.py | 24 +- .../Python/StartupUI/PimpMySpreadsheet.py | 309 +++++++++--------- .../startup/Python/StartupUI/Purge.py | 20 +- .../StartupUI/nukeStyleKeyboardShortcuts.py | 19 +- .../StartupUI/otioimporter/OTIOImport.py | 34 +- .../Python/StartupUI/otioimporter/__init__.py | 26 +- .../Python/StartupUI/setPosterFrame.py | 6 +- .../pipeline.xml | 0 .../pipeline.xml | 0 .../pipeline.xml | 0 .../plugins/publish/precollect_instances.py | 2 +- .../plugins/publish/precollect_workfile.py | 2 +- .../publish_old_workflow/precollect_retime.py | 2 +- .../Python/Startup/otioexporter/__init__.py | 7 - .../Python/Startup/selection_tracker.py | 9 - 55 files changed, 333 insertions(+), 304 deletions(-) rename openpype/hosts/hiero/{ => api}/otio/__init__.py (100%) rename openpype/hosts/hiero/{ => api}/otio/hiero_export.py (98%) rename openpype/hosts/hiero/{ => api}/otio/hiero_import.py (100%) rename openpype/hosts/hiero/{ => api}/otio/utils.py (100%) rename openpype/hosts/hiero/{ => api}/startup/HieroPlayer/PlayerPresets.hrox (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/1_add_handles_end.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/2_add_handles.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/3D.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/3_add_handles_start.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/4_2D.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/edit.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/fusion.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/hierarchy.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/houdini.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/layers.psd (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/lense.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/lense1.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/maya.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/nuke.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/pype_icon.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/resolution.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/resolution.psd (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/retiming.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/retiming.psd (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/review.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/review.psd (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/volume.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/z_layer_bg.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/z_layer_fg.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Icons/z_layer_main.png (100%) rename openpype/hosts/hiero/{ => api}/startup/Python/Startup/SpreadsheetExport.py (86%) rename openpype/hosts/hiero/{ => api}/startup/Python/Startup/Startup.py (100%) rename openpype/hosts/hiero/{ => api}/startup/Python/Startup/otioexporter/OTIOExportTask.py (96%) rename openpype/hosts/hiero/{ => api}/startup/Python/Startup/otioexporter/OTIOExportUI.py (91%) create mode 100644 openpype/hosts/hiero/api/startup/Python/Startup/otioexporter/__init__.py rename openpype/hosts/hiero/{ => api}/startup/Python/Startup/project_helpers.py (81%) create mode 100644 openpype/hosts/hiero/api/startup/Python/Startup/selection_tracker.py rename openpype/hosts/hiero/{ => api}/startup/Python/Startup/setFrameRate.py (90%) rename openpype/hosts/hiero/{ => api}/startup/Python/Startup/version_everywhere.py (95%) rename openpype/hosts/hiero/{ => api}/startup/Python/StartupUI/PimpMySpreadsheet.py (74%) rename openpype/hosts/hiero/{ => api}/startup/Python/StartupUI/Purge.py (89%) rename openpype/hosts/hiero/{ => api}/startup/Python/StartupUI/nukeStyleKeyboardShortcuts.py (72%) rename openpype/hosts/hiero/{ => api}/startup/Python/StartupUI/otioimporter/OTIOImport.py (94%) rename openpype/hosts/hiero/{ => api}/startup/Python/StartupUI/otioimporter/__init__.py (84%) rename openpype/hosts/hiero/{ => api}/startup/Python/StartupUI/setPosterFrame.py (94%) rename openpype/hosts/hiero/{ => api}/startup/TaskPresets/10.5/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml (100%) rename openpype/hosts/hiero/{ => api}/startup/TaskPresets/11.1/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml (100%) rename openpype/hosts/hiero/{ => api}/startup/TaskPresets/11.2/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml (100%) delete mode 100644 openpype/hosts/hiero/startup/Python/Startup/otioexporter/__init__.py delete mode 100644 openpype/hosts/hiero/startup/Python/Startup/selection_tracker.py diff --git a/openpype/hosts/hiero/__init__.py b/openpype/hosts/hiero/__init__.py index 1781f808e2..15bd10fdb0 100644 --- a/openpype/hosts/hiero/__init__.py +++ b/openpype/hosts/hiero/__init__.py @@ -6,7 +6,7 @@ def add_implementation_envs(env, _app): # Add requirements to HIERO_PLUGIN_PATH pype_root = os.environ["OPENPYPE_REPOS_ROOT"] new_hiero_paths = [ - os.path.join(pype_root, "openpype", "hosts", "hiero", "startup") + os.path.join(pype_root, "openpype", "hosts", "hiero", "api", "startup") ] old_hiero_path = env.get("HIERO_PLUGIN_PATH") or "" for path in old_hiero_path.split(os.pathsep): diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 21b65e5c96..ad9b0a6819 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -12,7 +12,6 @@ import avalon.api as avalon import avalon.io from openpype.api import (Logger, Anatomy, get_anatomy_settings) from . import tags -from compiler.ast import flatten try: from PySide.QtCore import QFile, QTextStream @@ -39,6 +38,13 @@ AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") def get_current_project(remove_untitled=False): + def flatten(l): + for i in l: + if isinstance(i, (list, tuple)): + yield from flatten(i) + else: + yield i + projects = flatten(hiero.core.projects()) if not remove_untitled: return next(iter(projects)) diff --git a/openpype/hosts/hiero/otio/__init__.py b/openpype/hosts/hiero/api/otio/__init__.py similarity index 100% rename from openpype/hosts/hiero/otio/__init__.py rename to openpype/hosts/hiero/api/otio/__init__.py diff --git a/openpype/hosts/hiero/otio/hiero_export.py b/openpype/hosts/hiero/api/otio/hiero_export.py similarity index 98% rename from openpype/hosts/hiero/otio/hiero_export.py rename to openpype/hosts/hiero/api/otio/hiero_export.py index af4322e3d9..5067c05515 100644 --- a/openpype/hosts/hiero/otio/hiero_export.py +++ b/openpype/hosts/hiero/api/otio/hiero_export.py @@ -5,7 +5,6 @@ import os import re import sys import ast -from compiler.ast import flatten import opentimelineio as otio from . import utils import hiero.core @@ -29,6 +28,14 @@ self.timeline = None self.include_tags = True +def flatten(l): + for i in l: + if isinstance(i, (list, tuple)): + yield from flatten(i) + else: + yield i + + def get_current_hiero_project(remove_untitled=False): projects = flatten(hiero.core.projects()) if not remove_untitled: diff --git a/openpype/hosts/hiero/otio/hiero_import.py b/openpype/hosts/hiero/api/otio/hiero_import.py similarity index 100% rename from openpype/hosts/hiero/otio/hiero_import.py rename to openpype/hosts/hiero/api/otio/hiero_import.py diff --git a/openpype/hosts/hiero/otio/utils.py b/openpype/hosts/hiero/api/otio/utils.py similarity index 100% rename from openpype/hosts/hiero/otio/utils.py rename to openpype/hosts/hiero/api/otio/utils.py diff --git a/openpype/hosts/hiero/startup/HieroPlayer/PlayerPresets.hrox b/openpype/hosts/hiero/api/startup/HieroPlayer/PlayerPresets.hrox similarity index 100% rename from openpype/hosts/hiero/startup/HieroPlayer/PlayerPresets.hrox rename to openpype/hosts/hiero/api/startup/HieroPlayer/PlayerPresets.hrox diff --git a/openpype/hosts/hiero/startup/Icons/1_add_handles_end.png b/openpype/hosts/hiero/api/startup/Icons/1_add_handles_end.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/1_add_handles_end.png rename to openpype/hosts/hiero/api/startup/Icons/1_add_handles_end.png diff --git a/openpype/hosts/hiero/startup/Icons/2_add_handles.png b/openpype/hosts/hiero/api/startup/Icons/2_add_handles.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/2_add_handles.png rename to openpype/hosts/hiero/api/startup/Icons/2_add_handles.png diff --git a/openpype/hosts/hiero/startup/Icons/3D.png b/openpype/hosts/hiero/api/startup/Icons/3D.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/3D.png rename to openpype/hosts/hiero/api/startup/Icons/3D.png diff --git a/openpype/hosts/hiero/startup/Icons/3_add_handles_start.png b/openpype/hosts/hiero/api/startup/Icons/3_add_handles_start.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/3_add_handles_start.png rename to openpype/hosts/hiero/api/startup/Icons/3_add_handles_start.png diff --git a/openpype/hosts/hiero/startup/Icons/4_2D.png b/openpype/hosts/hiero/api/startup/Icons/4_2D.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/4_2D.png rename to openpype/hosts/hiero/api/startup/Icons/4_2D.png diff --git a/openpype/hosts/hiero/startup/Icons/edit.png b/openpype/hosts/hiero/api/startup/Icons/edit.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/edit.png rename to openpype/hosts/hiero/api/startup/Icons/edit.png diff --git a/openpype/hosts/hiero/startup/Icons/fusion.png b/openpype/hosts/hiero/api/startup/Icons/fusion.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/fusion.png rename to openpype/hosts/hiero/api/startup/Icons/fusion.png diff --git a/openpype/hosts/hiero/startup/Icons/hierarchy.png b/openpype/hosts/hiero/api/startup/Icons/hierarchy.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/hierarchy.png rename to openpype/hosts/hiero/api/startup/Icons/hierarchy.png diff --git a/openpype/hosts/hiero/startup/Icons/houdini.png b/openpype/hosts/hiero/api/startup/Icons/houdini.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/houdini.png rename to openpype/hosts/hiero/api/startup/Icons/houdini.png diff --git a/openpype/hosts/hiero/startup/Icons/layers.psd b/openpype/hosts/hiero/api/startup/Icons/layers.psd similarity index 100% rename from openpype/hosts/hiero/startup/Icons/layers.psd rename to openpype/hosts/hiero/api/startup/Icons/layers.psd diff --git a/openpype/hosts/hiero/startup/Icons/lense.png b/openpype/hosts/hiero/api/startup/Icons/lense.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/lense.png rename to openpype/hosts/hiero/api/startup/Icons/lense.png diff --git a/openpype/hosts/hiero/startup/Icons/lense1.png b/openpype/hosts/hiero/api/startup/Icons/lense1.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/lense1.png rename to openpype/hosts/hiero/api/startup/Icons/lense1.png diff --git a/openpype/hosts/hiero/startup/Icons/maya.png b/openpype/hosts/hiero/api/startup/Icons/maya.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/maya.png rename to openpype/hosts/hiero/api/startup/Icons/maya.png diff --git a/openpype/hosts/hiero/startup/Icons/nuke.png b/openpype/hosts/hiero/api/startup/Icons/nuke.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/nuke.png rename to openpype/hosts/hiero/api/startup/Icons/nuke.png diff --git a/openpype/hosts/hiero/startup/Icons/pype_icon.png b/openpype/hosts/hiero/api/startup/Icons/pype_icon.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/pype_icon.png rename to openpype/hosts/hiero/api/startup/Icons/pype_icon.png diff --git a/openpype/hosts/hiero/startup/Icons/resolution.png b/openpype/hosts/hiero/api/startup/Icons/resolution.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/resolution.png rename to openpype/hosts/hiero/api/startup/Icons/resolution.png diff --git a/openpype/hosts/hiero/startup/Icons/resolution.psd b/openpype/hosts/hiero/api/startup/Icons/resolution.psd similarity index 100% rename from openpype/hosts/hiero/startup/Icons/resolution.psd rename to openpype/hosts/hiero/api/startup/Icons/resolution.psd diff --git a/openpype/hosts/hiero/startup/Icons/retiming.png b/openpype/hosts/hiero/api/startup/Icons/retiming.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/retiming.png rename to openpype/hosts/hiero/api/startup/Icons/retiming.png diff --git a/openpype/hosts/hiero/startup/Icons/retiming.psd b/openpype/hosts/hiero/api/startup/Icons/retiming.psd similarity index 100% rename from openpype/hosts/hiero/startup/Icons/retiming.psd rename to openpype/hosts/hiero/api/startup/Icons/retiming.psd diff --git a/openpype/hosts/hiero/startup/Icons/review.png b/openpype/hosts/hiero/api/startup/Icons/review.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/review.png rename to openpype/hosts/hiero/api/startup/Icons/review.png diff --git a/openpype/hosts/hiero/startup/Icons/review.psd b/openpype/hosts/hiero/api/startup/Icons/review.psd similarity index 100% rename from openpype/hosts/hiero/startup/Icons/review.psd rename to openpype/hosts/hiero/api/startup/Icons/review.psd diff --git a/openpype/hosts/hiero/startup/Icons/volume.png b/openpype/hosts/hiero/api/startup/Icons/volume.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/volume.png rename to openpype/hosts/hiero/api/startup/Icons/volume.png diff --git a/openpype/hosts/hiero/startup/Icons/z_layer_bg.png b/openpype/hosts/hiero/api/startup/Icons/z_layer_bg.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/z_layer_bg.png rename to openpype/hosts/hiero/api/startup/Icons/z_layer_bg.png diff --git a/openpype/hosts/hiero/startup/Icons/z_layer_fg.png b/openpype/hosts/hiero/api/startup/Icons/z_layer_fg.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/z_layer_fg.png rename to openpype/hosts/hiero/api/startup/Icons/z_layer_fg.png diff --git a/openpype/hosts/hiero/startup/Icons/z_layer_main.png b/openpype/hosts/hiero/api/startup/Icons/z_layer_main.png similarity index 100% rename from openpype/hosts/hiero/startup/Icons/z_layer_main.png rename to openpype/hosts/hiero/api/startup/Icons/z_layer_main.png diff --git a/openpype/hosts/hiero/startup/Python/Startup/SpreadsheetExport.py b/openpype/hosts/hiero/api/startup/Python/Startup/SpreadsheetExport.py similarity index 86% rename from openpype/hosts/hiero/startup/Python/Startup/SpreadsheetExport.py rename to openpype/hosts/hiero/api/startup/Python/Startup/SpreadsheetExport.py index 3adea8051c..9c919e7cb4 100644 --- a/openpype/hosts/hiero/startup/Python/Startup/SpreadsheetExport.py +++ b/openpype/hosts/hiero/api/startup/Python/Startup/SpreadsheetExport.py @@ -18,7 +18,7 @@ except: ### Magic Widget Finding Methods - This stuff crawls all the PySide widgets, looking for an answer def findWidget(w): global foundryWidgets - if 'Foundry' in w.metaObject().className(): + if "Foundry" in w.metaObject().className(): foundryWidgets += [w] for c in w.children(): @@ -49,7 +49,7 @@ def activeSpreadsheetTreeView(): Does some PySide widget Magic to detect the Active Spreadsheet TreeView. """ spreadsheetViews = getFoundryWidgetsWithClassName( - filter='SpreadsheetTreeView') + filter="SpreadsheetTreeView") for spreadSheet in spreadsheetViews: if spreadSheet.hasFocus(): activeSpreadSheet = spreadSheet @@ -77,23 +77,23 @@ class SpreadsheetExportCSVAction(QAction): spreadsheetTreeView = activeSpreadsheetTreeView() if not spreadsheetTreeView: - return 'Unable to detect the active TreeView.' + return "Unable to detect the active TreeView." seq = hiero.ui.activeView().sequence() if not seq: - print 'Unable to detect the active Sequence from the activeView.' + print("Unable to detect the active Sequence from the activeView.") return # The data model of the QTreeView model = spreadsheetTreeView.model() - csvSavePath = os.path.join(QDir.homePath(), 'Desktop', - seq.name() + '.csv') + csvSavePath = os.path.join(QDir.homePath(), "Desktop", + seq.name() + ".csv") savePath, filter = QFileDialog.getSaveFileName( None, caption="Export Spreadsheet to .CSV as...", dir=csvSavePath, filter="*.csv") - print 'Saving To: ' + str(savePath) + print("Saving To: {}".format(savePath)) # Saving was cancelled... if len(savePath) == 0: @@ -101,12 +101,12 @@ class SpreadsheetExportCSVAction(QAction): # Get the Visible Header Columns from the QTreeView - #csvHeader = ['Event', 'Status', 'Shot Name', 'Reel', 'Track', 'Speed', 'Src In', 'Src Out','Src Duration', 'Dst In', 'Dst Out', 'Dst Duration', 'Clip', 'Clip Media'] + #csvHeader = ["Event", "Status", "Shot Name", "Reel", "Track", "Speed", "Src In", "Src Out","Src Duration", "Dst In", "Dst Out", "Dst Duration", "Clip", "Clip Media"] # Get a CSV writer object - f = open(savePath, 'w') + f = open(savePath, "w") csvWriter = csv.writer( - f, delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL) + f, delimiter=',', quotechar="|", quoting=csv.QUOTE_MINIMAL) # This is a list of the Column titles csvHeader = [] diff --git a/openpype/hosts/hiero/startup/Python/Startup/Startup.py b/openpype/hosts/hiero/api/startup/Python/Startup/Startup.py similarity index 100% rename from openpype/hosts/hiero/startup/Python/Startup/Startup.py rename to openpype/hosts/hiero/api/startup/Python/Startup/Startup.py diff --git a/openpype/hosts/hiero/startup/Python/Startup/otioexporter/OTIOExportTask.py b/openpype/hosts/hiero/api/startup/Python/Startup/otioexporter/OTIOExportTask.py similarity index 96% rename from openpype/hosts/hiero/startup/Python/Startup/otioexporter/OTIOExportTask.py rename to openpype/hosts/hiero/api/startup/Python/Startup/otioexporter/OTIOExportTask.py index 7e1a8df2dc..e4ce2fe827 100644 --- a/openpype/hosts/hiero/startup/Python/Startup/otioexporter/OTIOExportTask.py +++ b/openpype/hosts/hiero/api/startup/Python/Startup/otioexporter/OTIOExportTask.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- __author__ = "Daniel Flehner Heen" @@ -9,7 +8,7 @@ import hiero.core from hiero.core import util import opentimelineio as otio -from openpype.hosts.hiero.otio import hiero_export +from openpype.hosts.hiero.api.otio import hiero_export class OTIOExportTask(hiero.core.TaskBase): diff --git a/openpype/hosts/hiero/startup/Python/Startup/otioexporter/OTIOExportUI.py b/openpype/hosts/hiero/api/startup/Python/Startup/otioexporter/OTIOExportUI.py similarity index 91% rename from openpype/hosts/hiero/startup/Python/Startup/otioexporter/OTIOExportUI.py rename to openpype/hosts/hiero/api/startup/Python/Startup/otioexporter/OTIOExportUI.py index 9b83eefedf..af5593e484 100644 --- a/openpype/hosts/hiero/startup/Python/Startup/otioexporter/OTIOExportUI.py +++ b/openpype/hosts/hiero/api/startup/Python/Startup/otioexporter/OTIOExportUI.py @@ -1,11 +1,13 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- __author__ = "Daniel Flehner Heen" __credits__ = ["Jakub Jezek", "Daniel Flehner Heen"] import hiero.ui -import OTIOExportTask +from .OTIOExportTask import ( + OTIOExportTask, + OTIOExportPreset +) try: # Hiero >= 11.x @@ -20,14 +22,14 @@ except ImportError: FormLayout = QFormLayout # lint:ok -from openpype.hosts.hiero.otio import hiero_export +from openpype.hosts.hiero.api.otio import hiero_export class OTIOExportUI(hiero.ui.TaskUIBase): def __init__(self, preset): """Initialize""" hiero.ui.TaskUIBase.__init__( self, - OTIOExportTask.OTIOExportTask, + OTIOExportTask, preset, "OTIO Exporter" ) @@ -67,6 +69,6 @@ class OTIOExportUI(hiero.ui.TaskUIBase): hiero.ui.taskUIRegistry.registerTaskUI( - OTIOExportTask.OTIOExportPreset, + OTIOExportPreset, OTIOExportUI ) diff --git a/openpype/hosts/hiero/api/startup/Python/Startup/otioexporter/__init__.py b/openpype/hosts/hiero/api/startup/Python/Startup/otioexporter/__init__.py new file mode 100644 index 0000000000..33d3fc6c59 --- /dev/null +++ b/openpype/hosts/hiero/api/startup/Python/Startup/otioexporter/__init__.py @@ -0,0 +1,7 @@ +from .OTIOExportTask import OTIOExportTask +from .OTIOExportUI import OTIOExportUI + +__all__ = [ + "OTIOExportTask", + "OTIOExportUI" +] diff --git a/openpype/hosts/hiero/startup/Python/Startup/project_helpers.py b/openpype/hosts/hiero/api/startup/Python/Startup/project_helpers.py similarity index 81% rename from openpype/hosts/hiero/startup/Python/Startup/project_helpers.py rename to openpype/hosts/hiero/api/startup/Python/Startup/project_helpers.py index 7e274bd0a3..64b5c37d7b 100644 --- a/openpype/hosts/hiero/startup/Python/Startup/project_helpers.py +++ b/openpype/hosts/hiero/api/startup/Python/Startup/project_helpers.py @@ -21,7 +21,7 @@ def __trackActiveProjectHandler(event): global gTrackedActiveProject selection = event.sender.selection() binSelection = selection - if len(binSelection) > 0 and hasattr(binSelection[0], 'project'): + if len(binSelection) > 0 and hasattr(binSelection[0], "project"): proj = binSelection[0].project() # We only store this if its a valid, active User Project @@ -30,18 +30,18 @@ def __trackActiveProjectHandler(event): hiero.core.events.registerInterest( - 'kSelectionChanged/kBin', __trackActiveProjectHandler) + "kSelectionChanged/kBin", __trackActiveProjectHandler) hiero.core.events.registerInterest( - 'kSelectionChanged/kTimeline', __trackActiveProjectHandler) + "kSelectionChanged/kTimeline", __trackActiveProjectHandler) hiero.core.events.registerInterest( - 'kSelectionChanged/Spreadsheet', __trackActiveProjectHandler) + "kSelectionChanged/Spreadsheet", __trackActiveProjectHandler) def activeProject(): """hiero.ui.activeProject() -> returns the current Project - Note: There is not technically a notion of a 'active' Project in Hiero/NukeStudio, as it is a multi-project App. - This method determines what is 'active' by going down the following rules... + Note: There is not technically a notion of a "active" Project in Hiero/NukeStudio, as it is a multi-project App. + This method determines what is "active" by going down the following rules... # 1 - If the current Viewer (hiero.ui.currentViewer) contains a Clip or Sequence, this item is assumed to give the active Project # 2 - If nothing is currently in the Viewer, look to the active View, determine project from active selection @@ -54,7 +54,7 @@ def activeProject(): # Case 1 : Look for what the current Viewr tells us - this might not be what we want, and relies on hiero.ui.currentViewer() being robust. cv = hiero.ui.currentViewer().player().sequence() - if hasattr(cv, 'project'): + if hasattr(cv, "project"): activeProject = cv.project() else: # Case 2: We can't determine a project from the current Viewer, so try seeing what's selected in the activeView @@ -66,16 +66,16 @@ def activeProject(): # Handle the case where nothing is selected in the active view if len(selection) == 0: - # It's possible that there is no selection in a Timeline/Spreadsheet, but these views have 'sequence' method, so try that... + # It's possible that there is no selection in a Timeline/Spreadsheet, but these views have "sequence" method, so try that... if isinstance(hiero.ui.activeView(), (hiero.ui.TimelineEditor, hiero.ui.SpreadsheetView)): activeSequence = activeView.sequence() - if hasattr(currentItem, 'project'): + if hasattr(currentItem, "project"): activeProject = activeSequence.project() # The active view has a selection... assume that the first item in the selection has the active Project else: currentItem = selection[0] - if hasattr(currentItem, 'project'): + if hasattr(currentItem, "project"): activeProject = currentItem.project() # Finally, Cases 3 and 4... @@ -156,9 +156,14 @@ class SaveAllProjects(QAction): for proj in allProjects: try: proj.save() - print 'Saved Project: %s to: %s ' % (proj.name(), proj.path()) + print("Saved Project: {} to: {} ".format( + proj.name(), proj.path() + )) except: - print 'Unable to save Project: %s to: %s. Check file permissions.' % (proj.name(), proj.path()) + print(( + "Unable to save Project: {} to: {}. " + "Check file permissions.").format( + proj.name(), proj.path())) def eventHandler(self, event): event.menu.addAction(self) @@ -190,32 +195,38 @@ class SaveNewProjectVersion(QAction): v = None prefix = None try: - (prefix, v) = version_get(path, 'v') - except ValueError, msg: - print msg + (prefix, v) = version_get(path, "v") + except ValueError as msg: + print(msg) if (prefix is not None) and (v is not None): v = int(v) newPath = version_set(path, prefix, v, v + 1) try: proj.saveAs(newPath) - print 'Saved new project version: %s to: %s ' % (oldName, newPath) + print("Saved new project version: {} to: {} ".format( + oldName, newPath)) except: - print 'Unable to save Project: %s. Check file permissions.' % (oldName) + print(( + "Unable to save Project: {}. Check file permissions." + ).format(oldName)) else: newPath = path.replace(".hrox", "_v01.hrox") answer = nuke.ask( - '%s does not contain a version number.\nDo you want to save as %s?' % (proj, newPath)) + "%s does not contain a version number.\nDo you want to save as %s?" % (proj, newPath)) if answer: try: proj.saveAs(newPath) - print 'Saved new project version: %s to: %s ' % (oldName, newPath) + print("Saved new project version: {} to: {} ".format( + oldName, newPath)) except: - print 'Unable to save Project: %s. Check file permissions.' % (oldName) + print(( + "Unable to save Project: {}. Check file " + "permissions.").format(oldName)) def eventHandler(self, event): self.selectedProjects = [] - if hasattr(event.sender, 'selection') and event.sender.selection() is not None and len(event.sender.selection()) != 0: + if hasattr(event.sender, "selection") and event.sender.selection() is not None and len(event.sender.selection()) != 0: selection = event.sender.selection() self.selectedProjects = uniquify( [item.project() for item in selection]) diff --git a/openpype/hosts/hiero/api/startup/Python/Startup/selection_tracker.py b/openpype/hosts/hiero/api/startup/Python/Startup/selection_tracker.py new file mode 100644 index 0000000000..a9789cf508 --- /dev/null +++ b/openpype/hosts/hiero/api/startup/Python/Startup/selection_tracker.py @@ -0,0 +1,9 @@ +"""Puts the selection project into "hiero.selection""" + +import hiero + + +def selectionChanged(event): + hiero.selection = event.sender.selection() + +hiero.core.events.registerInterest("kSelectionChanged", selectionChanged) diff --git a/openpype/hosts/hiero/startup/Python/Startup/setFrameRate.py b/openpype/hosts/hiero/api/startup/Python/Startup/setFrameRate.py similarity index 90% rename from openpype/hosts/hiero/startup/Python/Startup/setFrameRate.py rename to openpype/hosts/hiero/api/startup/Python/Startup/setFrameRate.py index ceb96a6fce..07ae48aef5 100644 --- a/openpype/hosts/hiero/startup/Python/Startup/setFrameRate.py +++ b/openpype/hosts/hiero/api/startup/Python/Startup/setFrameRate.py @@ -23,7 +23,7 @@ class SetFrameRateDialog(QDialog): self._itemSelection = itemSelection self._frameRateField = QLineEdit() - self._frameRateField.setToolTip('Enter custom frame rate here.') + self._frameRateField.setToolTip("Enter custom frame rate here.") self._frameRateField.setValidator(QDoubleValidator(1, 99, 3, self)) self._frameRateField.textChanged.connect(self._textChanged) layout.addRow("Enter fps: ",self._frameRateField) @@ -35,13 +35,13 @@ class SetFrameRateDialog(QDialog): self._buttonbox.button(QDialogButtonBox.Ok).setEnabled(False) layout.addRow("",self._buttonbox) self.setLayout(layout) - + def _updateOkButtonState(self): # Cancel is always an option but only enable Ok if there is some text. currentFramerate = float(self.currentFramerateString()) enableOk = False enableOk = ((currentFramerate > 0.0) and (currentFramerate <= 250.0)) - print 'enabledOk',enableOk + print("enabledOk", enableOk) self._buttonbox.button(QDialogButtonBox.Ok).setEnabled(enableOk) def _textChanged(self, newText): @@ -50,32 +50,32 @@ class SetFrameRateDialog(QDialog): # Returns the current frame rate as a string def currentFramerateString(self): return str(self._frameRateField.text()) - + # Presents the Dialog and sets the Frame rate from a selection def showDialogAndSetFrameRateFromSelection(self): - + if self._itemSelection is not None: if self.exec_(): # For the Undo loop... - + # Construct an TimeBase object for setting the Frame Rate (fps) fps = hiero.core.TimeBase().fromString(self.currentFramerateString()) - + # Set the frame rate for the selected BinItmes for item in self._itemSelection: - item.setFramerate(fps) + item.setFramerate(fps) return # This is just a convenience method for returning QActions with a title, triggered method and icon. def makeAction(title, method, icon = None): action = QAction(title,None) action.setIcon(QIcon(icon)) - + # We do this magic, so that the title string from the action is used to set the frame rate! def methodWrapper(): method(title) - + action.triggered.connect( methodWrapper ) return action @@ -88,13 +88,13 @@ class SetFrameRateMenu: # ant: Could use hiero.core.defaultFrameRates() here but messes up with string matching because we seem to mix decimal points - self.frameRates = ['8','12','12.50','15','23.98','24','25','29.97','30','48','50','59.94','60'] + self.frameRates = ["8","12","12.50","15","23.98","24","25","29.97","30","48","50","59.94","60"] hiero.core.events.registerInterest("kShowContextMenu/kBin", self.binViewEventHandler) - + self.menuActions = [] def createFrameRateMenus(self,selection): - selectedClipFPS = [str(bi.activeItem().framerate()) for bi in selection if (isinstance(bi,hiero.core.BinItem) and hasattr(bi,'activeItem'))] + selectedClipFPS = [str(bi.activeItem().framerate()) for bi in selection if (isinstance(bi,hiero.core.BinItem) and hasattr(bi,"activeItem"))] selectedClipFPS = hiero.core.util.uniquify(selectedClipFPS) sameFrameRate = len(selectedClipFPS)==1 self.menuActions = [] @@ -106,39 +106,41 @@ class SetFrameRateMenu: self.menuActions+=[makeAction(fps,self.setFrameRateFromMenuSelection, icon="icons:remove active.png")] else: self.menuActions+=[makeAction(fps,self.setFrameRateFromMenuSelection, icon=None)] - + # Now add Custom... menu - self.menuActions+=[makeAction('Custom...',self.setFrameRateFromMenuSelection, icon=None)] - + self.menuActions += [makeAction( + "Custom...", self.setFrameRateFromMenuSelection, icon=None) + ] + frameRateMenu = QMenu("Set Frame Rate") for a in self.menuActions: frameRateMenu.addAction(a) - + return frameRateMenu def setFrameRateFromMenuSelection(self, menuSelectionFPS): - - selectedBinItems = [bi.activeItem() for bi in self._selection if (isinstance(bi,hiero.core.BinItem) and hasattr(bi,'activeItem'))] + + selectedBinItems = [bi.activeItem() for bi in self._selection if (isinstance(bi,hiero.core.BinItem) and hasattr(bi,"activeItem"))] currentProject = selectedBinItems[0].project() - + with currentProject.beginUndo("Set Frame Rate"): - if menuSelectionFPS == 'Custom...': + if menuSelectionFPS == "Custom...": self._frameRatesDialog = SetFrameRateDialog(itemSelection = selectedBinItems ) self._frameRatesDialog.showDialogAndSetFrameRateFromSelection() else: for b in selectedBinItems: b.setFramerate(hiero.core.TimeBase().fromString(menuSelectionFPS)) - + return # This handles events from the Project Bin View def binViewEventHandler(self,event): - if not hasattr(event.sender, 'selection'): + if not hasattr(event.sender, "selection"): # Something has gone wrong, we should only be here if raised # by the Bin view which gives a selection. return - + # Reset the selection to None... self._selection = None s = event.sender.selection() @@ -151,9 +153,9 @@ class SetFrameRateMenu: if len(self._selection)==0: return # Creating the menu based on items selected, to highlight which frame rates are contained - + self._frameRateMenu = self.createFrameRateMenus(self._selection) - + # Insert the Set Frame Rate Button before the Set Media Colour Transform Action for action in event.menu.actions(): if str(action.text()) == "Set Media Colour Transform": diff --git a/openpype/hosts/hiero/startup/Python/Startup/version_everywhere.py b/openpype/hosts/hiero/api/startup/Python/Startup/version_everywhere.py similarity index 95% rename from openpype/hosts/hiero/startup/Python/Startup/version_everywhere.py rename to openpype/hosts/hiero/api/startup/Python/Startup/version_everywhere.py index e85e02bfa5..3d60b213d5 100644 --- a/openpype/hosts/hiero/startup/Python/Startup/version_everywhere.py +++ b/openpype/hosts/hiero/api/startup/Python/Startup/version_everywhere.py @@ -16,29 +16,29 @@ except: from PySide2.QtCore import * -def whereAmI(self, searchType='TrackItem'): +def whereAmI(self, searchType="TrackItem"): """returns a list of TrackItem or Sequnece objects in the Project which contain this Clip. By default this will return a list of TrackItems where the Clip is used in its project. - You can also return a list of Sequences by specifying the searchType to be 'Sequence'. + You can also return a list of Sequences by specifying the searchType to be "Sequence". Should consider putting this into hiero.core.Clip by default? Example usage: - shotsForClip = clip.whereAmI('TrackItem') - sequencesForClip = clip.whereAmI('Sequence') + shotsForClip = clip.whereAmI("TrackItem") + sequencesForClip = clip.whereAmI("Sequence") """ proj = self.project() - if ('TrackItem' not in searchType) and ('Sequence' not in searchType): - print "searchType argument must be 'TrackItem' or 'Sequence'" + if ("TrackItem" not in searchType) and ("Sequence" not in searchType): + print("searchType argument must be \"TrackItem\" or \"Sequence\"") return None # If user specifies a TrackItem, then it will return searches = hiero.core.findItemsInProject(proj, searchType) if len(searches) == 0: - print 'Unable to find %s in any items of type: %s' % (str(self), - str(searchType)) + print("Unable to find {} in any items of type: {}".format( + str(self), searchType)) return None # Case 1: Looking for Shots (trackItems) @@ -110,7 +110,7 @@ class VersionAllMenu(object): for shot in sequenceShotManifest[seq]: updateReportString += ' %s\n (New Version: %s)\n' % ( shot.name(), shot.currentVersion().name()) - updateReportString += '\n' + updateReportString += "\n" infoBox = QMessageBox(hiero.ui.mainWindow()) infoBox.setIcon(QMessageBox.Information) @@ -202,7 +202,7 @@ class VersionAllMenu(object): if len(bins) > 0: # Grab the Clips inside of a Bin and append them to a list for bin in bins: - clips = hiero.core.findItemsInBin(bin, 'Clip') + clips = hiero.core.findItemsInBin(bin, "Clip") for clip in clips: if clip not in clipItems: clipItems.append(clip) @@ -291,7 +291,7 @@ class VersionAllMenu(object): for clip in clipSelection: # Look to see if it exists in a TrackItem somewhere... - shotUsage = clip.whereAmI('TrackItem') + shotUsage = clip.whereAmI("TrackItem") # Next, depending on the versionOption, make the appropriate update # There's probably a more neat/compact way of doing this... @@ -326,7 +326,7 @@ class VersionAllMenu(object): # This handles events from the Project Bin View def binViewEventHandler(self, event): - if not hasattr(event.sender, 'selection'): + if not hasattr(event.sender, "selection"): # Something has gone wrong, we should only be here if raised # by the Bin view which gives a selection. return diff --git a/openpype/hosts/hiero/startup/Python/StartupUI/PimpMySpreadsheet.py b/openpype/hosts/hiero/api/startup/Python/StartupUI/PimpMySpreadsheet.py similarity index 74% rename from openpype/hosts/hiero/startup/Python/StartupUI/PimpMySpreadsheet.py rename to openpype/hosts/hiero/api/startup/Python/StartupUI/PimpMySpreadsheet.py index 3d40aa0293..39a65045a7 100644 --- a/openpype/hosts/hiero/startup/Python/StartupUI/PimpMySpreadsheet.py +++ b/openpype/hosts/hiero/api/startup/Python/StartupUI/PimpMySpreadsheet.py @@ -15,55 +15,55 @@ except: from PySide2.QtWidgets import * from PySide2.QtCore import * -# Set to True, if you wat 'Set Status' right-click menu, False if not +# Set to True, if you wat "Set Status" right-click menu, False if not kAddStatusMenu = True -# Set to True, if you wat 'Assign Artist' right-click menu, False if not +# Set to True, if you wat "Assign Artist" right-click menu, False if not kAssignArtistMenu = True # Global list of Artist Name Dictionaries # Note: Override this to add different names, icons, department, IDs. gArtistList = [{ - 'artistName': 'John Smith', - 'artistIcon': 'icons:TagActor.png', - 'artistDepartment': '3D', - 'artistID': 0 + "artistName": "John Smith", + "artistIcon": "icons:TagActor.png", + "artistDepartment": "3D", + "artistID": 0 }, { - 'artistName': 'Savlvador Dali', - 'artistIcon': 'icons:TagActor.png', - 'artistDepartment': 'Roto', - 'artistID': 1 + "artistName": "Savlvador Dali", + "artistIcon": "icons:TagActor.png", + "artistDepartment": "Roto", + "artistID": 1 }, { - 'artistName': 'Leonardo Da Vinci', - 'artistIcon': 'icons:TagActor.png', - 'artistDepartment': 'Paint', - 'artistID': 2 + "artistName": "Leonardo Da Vinci", + "artistIcon": "icons:TagActor.png", + "artistDepartment": "Paint", + "artistID": 2 }, { - 'artistName': 'Claude Monet', - 'artistIcon': 'icons:TagActor.png', - 'artistDepartment': 'Comp', - 'artistID': 3 + "artistName": "Claude Monet", + "artistIcon": "icons:TagActor.png", + "artistDepartment": "Comp", + "artistID": 3 }, { - 'artistName': 'Pablo Picasso', - 'artistIcon': 'icons:TagActor.png', - 'artistDepartment': 'Animation', - 'artistID': 4 + "artistName": "Pablo Picasso", + "artistIcon": "icons:TagActor.png", + "artistDepartment": "Animation", + "artistID": 4 }] # Global Dictionary of Status Tags. # Note: This can be overwritten if you want to add a new status cellType or custom icon -# Override the gStatusTags dictionary by adding your own 'Status':'Icon.png' key-value pairs. -# Add new custom keys like so: gStatusTags['For Client'] = 'forClient.png' +# Override the gStatusTags dictionary by adding your own "Status":"Icon.png" key-value pairs. +# Add new custom keys like so: gStatusTags["For Client"] = "forClient.png" gStatusTags = { - 'Approved': 'icons:status/TagApproved.png', - 'Unapproved': 'icons:status/TagUnapproved.png', - 'Ready To Start': 'icons:status/TagReadyToStart.png', - 'Blocked': 'icons:status/TagBlocked.png', - 'On Hold': 'icons:status/TagOnHold.png', - 'In Progress': 'icons:status/TagInProgress.png', - 'Awaiting Approval': 'icons:status/TagAwaitingApproval.png', - 'Omitted': 'icons:status/TagOmitted.png', - 'Final': 'icons:status/TagFinal.png' + "Approved": "icons:status/TagApproved.png", + "Unapproved": "icons:status/TagUnapproved.png", + "Ready To Start": "icons:status/TagReadyToStart.png", + "Blocked": "icons:status/TagBlocked.png", + "On Hold": "icons:status/TagOnHold.png", + "In Progress": "icons:status/TagInProgress.png", + "Awaiting Approval": "icons:status/TagAwaitingApproval.png", + "Omitted": "icons:status/TagOmitted.png", + "Final": "icons:status/TagFinal.png" } @@ -78,17 +78,17 @@ class CustomSpreadsheetColumns(QObject): # Ideally, we'd set this list on a Per Item basis, but this is expensive for a large mixed selection standardColourSpaces = [ - 'linear', 'sRGB', 'rec709', 'Cineon', 'Gamma1.8', 'Gamma2.2', - 'Panalog', 'REDLog', 'ViperLog' + "linear", "sRGB", "rec709", "Cineon", "Gamma1.8", "Gamma2.2", + "Panalog", "REDLog", "ViperLog" ] arriColourSpaces = [ - 'Video - Rec709', 'LogC - Camera Native', 'Video - P3', 'ACES', - 'LogC - Film', 'LogC - Wide Gamut' + "Video - Rec709", "LogC - Camera Native", "Video - P3", "ACES", + "LogC - Film", "LogC - Wide Gamut" ] r3dColourSpaces = [ - 'Linear', 'Rec709', 'REDspace', 'REDlog', 'PDlog685', 'PDlog985', - 'CustomPDlog', 'REDgamma', 'SRGB', 'REDlogFilm', 'REDgamma2', - 'REDgamma3' + "Linear", "Rec709", "REDspace", "REDlog", "PDlog685", "PDlog985", + "CustomPDlog", "REDgamma", "SRGB", "REDlogFilm", "REDgamma2", + "REDgamma3" ] gColourSpaces = standardColourSpaces + arriColourSpaces + r3dColourSpaces @@ -97,52 +97,52 @@ class CustomSpreadsheetColumns(QObject): # This is the list of Columns available gCustomColumnList = [ { - 'name': 'Tags', - 'cellType': 'readonly' + "name": "Tags", + "cellType": "readonly" }, { - 'name': 'Colourspace', - 'cellType': 'dropdown' + "name": "Colourspace", + "cellType": "dropdown" }, { - 'name': 'Notes', - 'cellType': 'readonly' + "name": "Notes", + "cellType": "readonly" }, { - 'name': 'FileType', - 'cellType': 'readonly' + "name": "FileType", + "cellType": "readonly" }, { - 'name': 'Shot Status', - 'cellType': 'dropdown' + "name": "Shot Status", + "cellType": "dropdown" }, { - 'name': 'Thumbnail', - 'cellType': 'readonly' + "name": "Thumbnail", + "cellType": "readonly" }, { - 'name': 'MediaType', - 'cellType': 'readonly' + "name": "MediaType", + "cellType": "readonly" }, { - 'name': 'Width', - 'cellType': 'readonly' + "name": "Width", + "cellType": "readonly" }, { - 'name': 'Height', - 'cellType': 'readonly' + "name": "Height", + "cellType": "readonly" }, { - 'name': 'Pixel Aspect', - 'cellType': 'readonly' + "name": "Pixel Aspect", + "cellType": "readonly" }, { - 'name': 'Artist', - 'cellType': 'dropdown' + "name": "Artist", + "cellType": "dropdown" }, { - 'name': 'Department', - 'cellType': 'readonly' + "name": "Department", + "cellType": "readonly" }, ] @@ -156,7 +156,7 @@ class CustomSpreadsheetColumns(QObject): """ Return the name of a custom column """ - return self.gCustomColumnList[column]['name'] + return self.gCustomColumnList[column]["name"] def getTagsString(self, item): """ @@ -173,7 +173,7 @@ class CustomSpreadsheetColumns(QObject): """ Convenience method for returning all the Notes in a Tag as a string """ - notes = '' + notes = "" tags = item.tags() for tag in tags: note = tag.note() @@ -186,67 +186,67 @@ class CustomSpreadsheetColumns(QObject): Return the data in a cell """ currentColumn = self.gCustomColumnList[column] - if currentColumn['name'] == 'Tags': + if currentColumn["name"] == "Tags": return self.getTagsString(item) - if currentColumn['name'] == 'Colourspace': + if currentColumn["name"] == "Colourspace": try: colTransform = item.sourceMediaColourTransform() except: - colTransform = '--' + colTransform = "--" return colTransform - if currentColumn['name'] == 'Notes': + if currentColumn["name"] == "Notes": try: note = self.getNotes(item) except: - note = '' + note = "" return note - if currentColumn['name'] == 'FileType': - fileType = '--' + if currentColumn["name"] == "FileType": + fileType = "--" M = item.source().mediaSource().metadata() - if M.hasKey('foundry.source.type'): - fileType = M.value('foundry.source.type') - elif M.hasKey('media.input.filereader'): - fileType = M.value('media.input.filereader') + if M.hasKey("foundry.source.type"): + fileType = M.value("foundry.source.type") + elif M.hasKey("media.input.filereader"): + fileType = M.value("media.input.filereader") return fileType - if currentColumn['name'] == 'Shot Status': + if currentColumn["name"] == "Shot Status": status = item.status() if not status: status = "--" return str(status) - if currentColumn['name'] == 'MediaType': + if currentColumn["name"] == "MediaType": M = item.mediaType() - return str(M).split('MediaType')[-1].replace('.k', '') + return str(M).split("MediaType")[-1].replace(".k", "") - if currentColumn['name'] == 'Thumbnail': + if currentColumn["name"] == "Thumbnail": return str(item.eventNumber()) - if currentColumn['name'] == 'Width': + if currentColumn["name"] == "Width": return str(item.source().format().width()) - if currentColumn['name'] == 'Height': + if currentColumn["name"] == "Height": return str(item.source().format().height()) - if currentColumn['name'] == 'Pixel Aspect': + if currentColumn["name"] == "Pixel Aspect": return str(item.source().format().pixelAspect()) - if currentColumn['name'] == 'Artist': + if currentColumn["name"] == "Artist": if item.artist(): - name = item.artist()['artistName'] + name = item.artist()["artistName"] return name else: - return '--' + return "--" - if currentColumn['name'] == 'Department': + if currentColumn["name"] == "Department": if item.artist(): - dep = item.artist()['artistDepartment'] + dep = item.artist()["artistDepartment"] return dep else: - return '--' + return "--" return "" @@ -262,10 +262,10 @@ class CustomSpreadsheetColumns(QObject): Return the tooltip for a cell """ currentColumn = self.gCustomColumnList[column] - if currentColumn['name'] == 'Tags': + if currentColumn["name"] == "Tags": return str([item.name() for item in item.tags()]) - if currentColumn['name'] == 'Notes': + if currentColumn["name"] == "Notes": return str(self.getNotes(item)) return "" @@ -296,24 +296,24 @@ class CustomSpreadsheetColumns(QObject): Return the icon for a cell """ currentColumn = self.gCustomColumnList[column] - if currentColumn['name'] == 'Colourspace': + if currentColumn["name"] == "Colourspace": return QIcon("icons:LUT.png") - if currentColumn['name'] == 'Shot Status': + if currentColumn["name"] == "Shot Status": status = item.status() if status: return QIcon(gStatusTags[status]) - if currentColumn['name'] == 'MediaType': + if currentColumn["name"] == "MediaType": mediaType = item.mediaType() if mediaType == hiero.core.TrackItem.kVideo: return QIcon("icons:VideoOnly.png") elif mediaType == hiero.core.TrackItem.kAudio: return QIcon("icons:AudioOnly.png") - if currentColumn['name'] == 'Artist': + if currentColumn["name"] == "Artist": try: - return QIcon(item.artist()['artistIcon']) + return QIcon(item.artist()["artistIcon"]) except: return None return None @@ -322,9 +322,9 @@ class CustomSpreadsheetColumns(QObject): """ Return the size hint for a cell """ - currentColumnName = self.gCustomColumnList[column]['name'] + currentColumnName = self.gCustomColumnList[column]["name"] - if currentColumnName == 'Thumbnail': + if currentColumnName == "Thumbnail": return QSize(90, 50) return QSize(50, 50) @@ -335,7 +335,7 @@ class CustomSpreadsheetColumns(QObject): with the default cell painting. """ currentColumn = self.gCustomColumnList[column] - if currentColumn['name'] == 'Tags': + if currentColumn["name"] == "Tags": if option.state & QStyle.State_Selected: painter.fillRect(option.rect, option.palette.highlight()) iconSize = 20 @@ -348,14 +348,14 @@ class CustomSpreadsheetColumns(QObject): painter.setClipRect(option.rect) for tag in item.tags(): M = tag.metadata() - if not (M.hasKey('tag.status') - or M.hasKey('tag.artistID')): + if not (M.hasKey("tag.status") + or M.hasKey("tag.artistID")): QIcon(tag.icon()).paint(painter, r, Qt.AlignLeft) r.translate(r.width() + 2, 0) painter.restore() return True - if currentColumn['name'] == 'Thumbnail': + if currentColumn["name"] == "Thumbnail": imageView = None pen = QPen() r = QRect(option.rect.x() + 2, (option.rect.y() + @@ -409,35 +409,35 @@ class CustomSpreadsheetColumns(QObject): self.currentView = view currentColumn = self.gCustomColumnList[column] - if currentColumn['cellType'] == 'readonly': + if currentColumn["cellType"] == "readonly": cle = QLabel() cle.setEnabled(False) cle.setVisible(False) return cle - if currentColumn['name'] == 'Colourspace': + if currentColumn["name"] == "Colourspace": cb = QComboBox() for colourspace in self.gColourSpaces: cb.addItem(colourspace) cb.currentIndexChanged.connect(self.colourspaceChanged) return cb - if currentColumn['name'] == 'Shot Status': + if currentColumn["name"] == "Shot Status": cb = QComboBox() - cb.addItem('') + cb.addItem("") for key in gStatusTags.keys(): cb.addItem(QIcon(gStatusTags[key]), key) - cb.addItem('--') + cb.addItem("--") cb.currentIndexChanged.connect(self.statusChanged) return cb - if currentColumn['name'] == 'Artist': + if currentColumn["name"] == "Artist": cb = QComboBox() - cb.addItem('') + cb.addItem("") for artist in gArtistList: - cb.addItem(artist['artistName']) - cb.addItem('--') + cb.addItem(artist["artistName"]) + cb.addItem("--") cb.currentIndexChanged.connect(self.artistNameChanged) return cb return None @@ -479,15 +479,15 @@ class CustomSpreadsheetColumns(QObject): status = self.sender().currentText() project = selection[0].project() with project.beginUndo("Set Status"): - # A string of '--' characters denotes clear the status - if status != '--': + # A string of "--" characters denotes clear the status + if status != "--": for trackItem in selection: trackItem.setStatus(status) else: for trackItem in selection: tTags = trackItem.tags() for tag in tTags: - if tag.metadata().hasKey('tag.status'): + if tag.metadata().hasKey("tag.status"): trackItem.removeTag(tag) break @@ -500,15 +500,15 @@ class CustomSpreadsheetColumns(QObject): name = self.sender().currentText() project = selection[0].project() with project.beginUndo("Assign Artist"): - # A string of '--' denotes clear the assignee... - if name != '--': + # A string of "--" denotes clear the assignee... + if name != "--": for trackItem in selection: trackItem.setArtistByName(name) else: for trackItem in selection: tTags = trackItem.tags() for tag in tTags: - if tag.metadata().hasKey('tag.artistID'): + if tag.metadata().hasKey("tag.artistID"): trackItem.removeTag(tag) break @@ -518,7 +518,7 @@ def _getArtistFromID(self, artistID): global gArtistList artist = [ element for element in gArtistList - if element['artistID'] == int(artistID) + if element["artistID"] == int(artistID) ] if not artist: return None @@ -530,7 +530,7 @@ def _getArtistFromName(self, artistName): global gArtistList artist = [ element for element in gArtistList - if element['artistName'] == artistName + if element["artistName"] == artistName ] if not artist: return None @@ -542,8 +542,8 @@ def _artist(self): artist = None tags = self.tags() for tag in tags: - if tag.metadata().hasKey('tag.artistID'): - artistID = tag.metadata().value('tag.artistID') + if tag.metadata().hasKey("tag.artistID"): + artistID = tag.metadata().value("tag.artistID") artist = self.getArtistFromID(artistID) return artist @@ -554,30 +554,30 @@ def _updateArtistTag(self, artistDict): artistTag = None tags = self.tags() for tag in tags: - if tag.metadata().hasKey('tag.artistID'): + if tag.metadata().hasKey("tag.artistID"): artistTag = tag break if not artistTag: - artistTag = hiero.core.Tag('Artist') - artistTag.setIcon(artistDict['artistIcon']) - artistTag.metadata().setValue('tag.artistID', - str(artistDict['artistID'])) - artistTag.metadata().setValue('tag.artistName', - str(artistDict['artistName'])) - artistTag.metadata().setValue('tag.artistDepartment', - str(artistDict['artistDepartment'])) + artistTag = hiero.core.Tag("Artist") + artistTag.setIcon(artistDict["artistIcon"]) + artistTag.metadata().setValue("tag.artistID", + str(artistDict["artistID"])) + artistTag.metadata().setValue("tag.artistName", + str(artistDict["artistName"])) + artistTag.metadata().setValue("tag.artistDepartment", + str(artistDict["artistDepartment"])) self.sequence().editFinished() self.addTag(artistTag) self.sequence().editFinished() return - artistTag.setIcon(artistDict['artistIcon']) - artistTag.metadata().setValue('tag.artistID', str(artistDict['artistID'])) - artistTag.metadata().setValue('tag.artistName', - str(artistDict['artistName'])) - artistTag.metadata().setValue('tag.artistDepartment', - str(artistDict['artistDepartment'])) + artistTag.setIcon(artistDict["artistIcon"]) + artistTag.metadata().setValue("tag.artistID", str(artistDict["artistID"])) + artistTag.metadata().setValue("tag.artistName", + str(artistDict["artistName"])) + artistTag.metadata().setValue("tag.artistDepartment", + str(artistDict["artistDepartment"])) self.sequence().editFinished() return @@ -588,8 +588,9 @@ def _setArtistByName(self, artistName): artist = self.getArtistFromName(artistName) if not artist: - print 'Artist name: %s was not found in the gArtistList.' % str( - artistName) + print(( + "Artist name: {} was not found in " + "the gArtistList.").format(artistName)) return # Do the update. @@ -602,8 +603,8 @@ def _setArtistByID(self, artistID): artist = self.getArtistFromID(artistID) if not artist: - print 'Artist name: %s was not found in the gArtistList.' % str( - artistID) + print("Artist name: {} was not found in the gArtistList.".format( + artistID)) return # Do the update. @@ -625,15 +626,15 @@ def _status(self): status = None tags = self.tags() for tag in tags: - if tag.metadata().hasKey('tag.status'): - status = tag.metadata().value('tag.status') + if tag.metadata().hasKey("tag.status"): + status = tag.metadata().value("tag.status") return status def _setStatus(self, status): """setShotStatus(status) -> Method to set the Status of a Shot. Adds a special kind of status Tag to a TrackItem - Example: myTrackItem.setStatus('Final') + Example: myTrackItem.setStatus("Final") @param status - a string, corresponding to the Status name """ @@ -641,25 +642,25 @@ def _setStatus(self, status): # Get a valid Tag object from the Global list of statuses if not status in gStatusTags.keys(): - print 'Status requested was not a valid Status string.' + print("Status requested was not a valid Status string.") return # A shot should only have one status. Check if one exists and set accordingly statusTag = None tags = self.tags() for tag in tags: - if tag.metadata().hasKey('tag.status'): + if tag.metadata().hasKey("tag.status"): statusTag = tag break if not statusTag: - statusTag = hiero.core.Tag('Status') + statusTag = hiero.core.Tag("Status") statusTag.setIcon(gStatusTags[status]) - statusTag.metadata().setValue('tag.status', status) + statusTag.metadata().setValue("tag.status", status) self.addTag(statusTag) statusTag.setIcon(gStatusTags[status]) - statusTag.metadata().setValue('tag.status', status) + statusTag.metadata().setValue("tag.status", status) self.sequence().editFinished() return @@ -743,7 +744,7 @@ class SetStatusMenu(QMenu): # This handles events from the Project Bin View def eventHandler(self, event): - if not hasattr(event.sender, 'selection'): + if not hasattr(event.sender, "selection"): # Something has gone wrong, we should only be here if raised # by the Timeline/Spreadsheet view which gives a selection. return @@ -781,9 +782,9 @@ class AssignArtistMenu(QMenu): for artist in self.artists: self.menuActions += [ titleStringTriggeredAction( - artist['artistName'], + artist["artistName"], self.setArtistFromMenuSelection, - icon=artist['artistIcon']) + icon=artist["artistIcon"]) ] def setArtistFromMenuSelection(self, menuSelectionArtist): @@ -818,7 +819,7 @@ class AssignArtistMenu(QMenu): # This handles events from the Project Bin View def eventHandler(self, event): - if not hasattr(event.sender, 'selection'): + if not hasattr(event.sender, "selection"): # Something has gone wrong, we should only be here if raised # by the Timeline/Spreadsheet view which gives a selection. return @@ -833,7 +834,7 @@ class AssignArtistMenu(QMenu): event.menu.addMenu(self) -# Add the 'Set Status' context menu to Timeline and Spreadsheet +# Add the "Set Status" context menu to Timeline and Spreadsheet if kAddStatusMenu: setStatusMenu = SetStatusMenu() diff --git a/openpype/hosts/hiero/startup/Python/StartupUI/Purge.py b/openpype/hosts/hiero/api/startup/Python/StartupUI/Purge.py similarity index 89% rename from openpype/hosts/hiero/startup/Python/StartupUI/Purge.py rename to openpype/hosts/hiero/api/startup/Python/StartupUI/Purge.py index 4d2ab255ad..7b3cb11be3 100644 --- a/openpype/hosts/hiero/startup/Python/StartupUI/Purge.py +++ b/openpype/hosts/hiero/api/startup/Python/StartupUI/Purge.py @@ -23,7 +23,7 @@ class PurgeUnusedAction(QAction): self.triggered.connect(self.PurgeUnused) hiero.core.events.registerInterest("kShowContextMenu/kBin", self.eventHandler) - self.setIcon(QIcon('icons:TagDelete.png')) + self.setIcon(QIcon("icons:TagDelete.png")) # Method to return whether a Bin is empty... def binIsEmpty(self, b): @@ -67,19 +67,19 @@ class PurgeUnusedAction(QAction): msgBox.setDefaultButton(QMessageBox.Ok) ret = msgBox.exec_() if ret == QMessageBox.Cancel: - print 'Not purging anything.' + print("Not purging anything.") elif ret == QMessageBox.Ok: - with proj.beginUndo('Purge Unused Clips'): + with proj.beginUndo("Purge Unused Clips"): BINS = [] for clip in CLIPSTOREMOVE: BI = clip.binItem() B = BI.parentBin() BINS += [B] - print 'Removing:', BI + print("Removing: {}".format(BI)) try: B.removeItem(BI) except: - print 'Unable to remove: ' + BI + print("Unable to remove: {}".format(BI)) return # For each sequence, iterate through each track Item, see if the Clip is in the CLIPS list. @@ -104,24 +104,24 @@ class PurgeUnusedAction(QAction): ret = msgBox.exec_() if ret == QMessageBox.Cancel: - print 'Cancel' + print("Cancel") return elif ret == QMessageBox.Ok: BINS = [] - with proj.beginUndo('Purge Unused Clips'): + with proj.beginUndo("Purge Unused Clips"): # Delete the rest of the Clips for clip in CLIPSTOREMOVE: BI = clip.binItem() B = BI.parentBin() BINS += [B] - print 'Removing:', BI + print("Removing: {}".format(BI)) try: B.removeItem(BI) except: - print 'Unable to remove: ' + BI + print("Unable to remove: {}".format(BI)) def eventHandler(self, event): - if not hasattr(event.sender, 'selection'): + if not hasattr(event.sender, "selection"): # Something has gone wrong, we shouldn't only be here if raised # by the Bin view which will give a selection. return diff --git a/openpype/hosts/hiero/startup/Python/StartupUI/nukeStyleKeyboardShortcuts.py b/openpype/hosts/hiero/api/startup/Python/StartupUI/nukeStyleKeyboardShortcuts.py similarity index 72% rename from openpype/hosts/hiero/startup/Python/StartupUI/nukeStyleKeyboardShortcuts.py rename to openpype/hosts/hiero/api/startup/Python/StartupUI/nukeStyleKeyboardShortcuts.py index 41c192ab15..4172b2ff85 100644 --- a/openpype/hosts/hiero/startup/Python/StartupUI/nukeStyleKeyboardShortcuts.py +++ b/openpype/hosts/hiero/api/startup/Python/StartupUI/nukeStyleKeyboardShortcuts.py @@ -13,21 +13,22 @@ except: #---------------------------------------------- a = hiero.ui.findMenuAction('Import File(s)...') -# Note: You probably best to make this 'Ctrl+R' - currently conflicts with 'Red' in the Viewer! -a.setShortcut(QKeySequence('R')) +# Note: You probably best to make this 'Ctrl+R' - currently conflicts with "Red" in the Viewer! +a.setShortcut(QKeySequence("R")) #---------------------------------------------- a = hiero.ui.findMenuAction('Import Folder(s)...') a.setShortcut(QKeySequence('Shift+R')) #---------------------------------------------- -a = hiero.ui.findMenuAction('Import EDL/XML/AAF...') +a = hiero.ui.findMenuAction("Import EDL/XML/AAF...") a.setShortcut(QKeySequence('Ctrl+Shift+O')) #---------------------------------------------- -a = hiero.ui.findMenuAction('Metadata View') -a.setShortcut(QKeySequence('I')) +a = hiero.ui.findMenuAction("Metadata View") +a.setShortcut(QKeySequence("I")) #---------------------------------------------- -a = hiero.ui.findMenuAction('Edit Settings') -a.setShortcut(QKeySequence('S')) +a = hiero.ui.findMenuAction("Edit Settings") +a.setShortcut(QKeySequence("S")) #---------------------------------------------- -a = hiero.ui.findMenuAction('Monitor Output') -a.setShortcut(QKeySequence('Ctrl+U')) +a = hiero.ui.findMenuAction("Monitor Output") +if a: + a.setShortcut(QKeySequence('Ctrl+U')) #---------------------------------------------- diff --git a/openpype/hosts/hiero/startup/Python/StartupUI/otioimporter/OTIOImport.py b/openpype/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py similarity index 94% rename from openpype/hosts/hiero/startup/Python/StartupUI/otioimporter/OTIOImport.py rename to openpype/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py index 7efb352ed2..17c044f3ec 100644 --- a/openpype/hosts/hiero/startup/Python/StartupUI/otioimporter/OTIOImport.py +++ b/openpype/hosts/hiero/api/startup/Python/StartupUI/otioimporter/OTIOImport.py @@ -44,16 +44,16 @@ def get_transition_type(otio_item, otio_track): _out = None if _in and _out: - return 'dissolve' + return "dissolve" elif _in and not _out: - return 'fade_out' + return "fade_out" elif not _in and _out: - return 'fade_in' + return "fade_in" else: - return 'unknown' + return "unknown" def find_trackitem(name, hiero_track): @@ -84,10 +84,10 @@ def apply_transition(otio_track, otio_item, track): # Figure out track kind for getattr below if isinstance(track, hiero.core.VideoTrack): - kind = '' + kind = "" else: - kind = 'Audio' + kind = "Audio" try: # Gather TrackItems involved in trasition @@ -98,7 +98,7 @@ def apply_transition(otio_track, otio_item, track): ) # Create transition object - if transition_type == 'dissolve': + if transition_type == "dissolve": transition_func = getattr( hiero.core.Transition, 'create{kind}DissolveTransition'.format(kind=kind) @@ -111,7 +111,7 @@ def apply_transition(otio_track, otio_item, track): otio_item.out_offset.value ) - elif transition_type == 'fade_in': + elif transition_type == "fade_in": transition_func = getattr( hiero.core.Transition, 'create{kind}FadeInTransition'.format(kind=kind) @@ -121,7 +121,7 @@ def apply_transition(otio_track, otio_item, track): otio_item.out_offset.value ) - elif transition_type == 'fade_out': + elif transition_type == "fade_out": transition_func = getattr( hiero.core.Transition, 'create{kind}FadeOutTransition'.format(kind=kind) @@ -150,11 +150,11 @@ def apply_transition(otio_track, otio_item, track): def prep_url(url_in): url = unquote(url_in) - if url.startswith('file://localhost/'): - return url.replace('file://localhost/', '') + if url.startswith("file://localhost/"): + return url.replace("file://localhost/", "") url = '{url}'.format( - sep=url.startswith(os.sep) and '' or os.sep, + sep=url.startswith(os.sep) and "" or os.sep, url=url.startswith(os.sep) and url[1:] or url ) @@ -228,8 +228,8 @@ def add_metadata(metadata, hiero_item): continue if value is not None: - if not key.startswith('tag.'): - key = 'tag.' + key + if not key.startswith("tag."): + key = "tag." + key hiero_item.metadata().setValue(key, str(value)) @@ -371,10 +371,10 @@ def build_sequence( if not sequence: # Create a Sequence - sequence = hiero.core.Sequence(otio_timeline.name or 'OTIOSequence') + sequence = hiero.core.Sequence(otio_timeline.name or "OTIOSequence") # Set sequence settings from otio timeline if available - if hasattr(otio_timeline, 'global_start_time'): + if hasattr(otio_timeline, "global_start_time"): if otio_timeline.global_start_time: start_time = otio_timeline.global_start_time sequence.setFramerate(start_time.rate) @@ -414,7 +414,7 @@ def build_sequence( if isinstance(otio_clip, otio.schema.Stack): bar = hiero.ui.mainWindow().statusBar() bar.showMessage( - 'Nested sequences are created separately.', + "Nested sequences are created separately.", timeout=3000 ) build_sequence(otio_clip, project, otio_track.kind) diff --git a/openpype/hosts/hiero/startup/Python/StartupUI/otioimporter/__init__.py b/openpype/hosts/hiero/api/startup/Python/StartupUI/otioimporter/__init__.py similarity index 84% rename from openpype/hosts/hiero/startup/Python/StartupUI/otioimporter/__init__.py rename to openpype/hosts/hiero/api/startup/Python/StartupUI/otioimporter/__init__.py index 0f0a643909..91be4d02aa 100644 --- a/openpype/hosts/hiero/startup/Python/StartupUI/otioimporter/__init__.py +++ b/openpype/hosts/hiero/api/startup/Python/StartupUI/otioimporter/__init__.py @@ -9,19 +9,19 @@ import hiero.core import PySide2.QtWidgets as qw -from openpype.hosts.hiero.otio.hiero_import import load_otio +from openpype.hosts.hiero.api.otio.hiero_import import load_otio class OTIOProjectSelect(qw.QDialog): def __init__(self, projects, *args, **kwargs): super(OTIOProjectSelect, self).__init__(*args, **kwargs) - self.setWindowTitle('Please select active project') + self.setWindowTitle("Please select active project") self.layout = qw.QVBoxLayout() self.label = qw.QLabel( - 'Unable to determine which project to import sequence to.\n' - 'Please select one.' + "Unable to determine which project to import sequence to.\n" + "Please select one." ) self.layout.addWidget(self.label) @@ -45,7 +45,7 @@ def get_sequence(view): elif isinstance(view, hiero.ui.BinView): for item in view.selection(): - if not hasattr(item, 'acitveItem'): + if not hasattr(item, "acitveItem"): continue if isinstance(item.activeItem(), hiero.core.Sequence): @@ -57,13 +57,13 @@ def get_sequence(view): def OTIO_menu_action(event): # Menu actions otio_import_action = hiero.ui.createMenuAction( - 'Import OTIO...', + "Import OTIO...", open_otio_file, icon=None ) otio_add_track_action = hiero.ui.createMenuAction( - 'New Track(s) from OTIO...', + "New Track(s) from OTIO...", open_otio_file, icon=None ) @@ -80,19 +80,19 @@ def OTIO_menu_action(event): otio_add_track_action.setEnabled(True) for action in event.menu.actions(): - if action.text() == 'Import': + if action.text() == "Import": action.menu().addAction(otio_import_action) action.menu().addAction(otio_add_track_action) - elif action.text() == 'New Track': + elif action.text() == "New Track": action.menu().addAction(otio_add_track_action) def open_otio_file(): files = hiero.ui.openFileBrowser( - caption='Please select an OTIO file of choice', - pattern='*.otio', - requiredExtension='.otio' + caption="Please select an OTIO file of choice", + pattern="*.otio", + requiredExtension=".otio" ) selection = None @@ -117,7 +117,7 @@ def open_otio_file(): else: bar = hiero.ui.mainWindow().statusBar() bar.showMessage( - 'OTIO Import aborted by user', + "OTIO Import aborted by user", timeout=3000 ) return diff --git a/openpype/hosts/hiero/startup/Python/StartupUI/setPosterFrame.py b/openpype/hosts/hiero/api/startup/Python/StartupUI/setPosterFrame.py similarity index 94% rename from openpype/hosts/hiero/startup/Python/StartupUI/setPosterFrame.py rename to openpype/hosts/hiero/api/startup/Python/StartupUI/setPosterFrame.py index 18398aa119..8614d51bb0 100644 --- a/openpype/hosts/hiero/startup/Python/StartupUI/setPosterFrame.py +++ b/openpype/hosts/hiero/api/startup/Python/StartupUI/setPosterFrame.py @@ -10,15 +10,15 @@ except: def setPosterFrame(posterFrame=.5): - ''' + """ Update the poster frame of the given clipItmes posterFrame = .5 uses the centre frame, a value of 0 uses the first frame, a value of 1 uses the last frame - ''' + """ view = hiero.ui.activeView() selectedBinItems = view.selection() selectedClipItems = [(item.activeItem() - if hasattr(item, 'activeItem') else item) + if hasattr(item, "activeItem") else item) for item in selectedBinItems] for clip in selectedClipItems: diff --git a/openpype/hosts/hiero/startup/TaskPresets/10.5/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml b/openpype/hosts/hiero/api/startup/TaskPresets/10.5/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml similarity index 100% rename from openpype/hosts/hiero/startup/TaskPresets/10.5/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml rename to openpype/hosts/hiero/api/startup/TaskPresets/10.5/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml diff --git a/openpype/hosts/hiero/startup/TaskPresets/11.1/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml b/openpype/hosts/hiero/api/startup/TaskPresets/11.1/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml similarity index 100% rename from openpype/hosts/hiero/startup/TaskPresets/11.1/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml rename to openpype/hosts/hiero/api/startup/TaskPresets/11.1/Processors/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml diff --git a/openpype/hosts/hiero/startup/TaskPresets/11.2/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml b/openpype/hosts/hiero/api/startup/TaskPresets/11.2/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml similarity index 100% rename from openpype/hosts/hiero/startup/TaskPresets/11.2/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml rename to openpype/hosts/hiero/api/startup/TaskPresets/11.2/hiero.exporters.FnShotProcessor.ShotProcessor/pipeline.xml diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 85b4e273d5..b13603897b 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -1,7 +1,7 @@ import pyblish import openpype from openpype.hosts.hiero import api as phiero -from openpype.hosts.hiero.otio import hiero_export +from openpype.hosts.hiero.api.otio import hiero_export import hiero from compiler.ast import flatten diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index 7db155048f..d48d6949bd 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -4,7 +4,7 @@ import hiero.ui from openpype.hosts.hiero import api as phiero from avalon import api as avalon from pprint import pformat -from openpype.hosts.hiero.otio import hiero_export +from openpype.hosts.hiero.api.otio import hiero_export from Qt.QtGui import QPixmap import tempfile diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/precollect_retime.py b/openpype/hosts/hiero/plugins/publish_old_workflow/precollect_retime.py index de05414c88..f0e0f1a1a3 100644 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/precollect_retime.py +++ b/openpype/hosts/hiero/plugins/publish_old_workflow/precollect_retime.py @@ -1,7 +1,7 @@ from pyblish import api import hiero import math -from openpype.hosts.hiero.otio.hiero_export import create_otio_time_range +from openpype.hosts.hiero.api.otio.hiero_export import create_otio_time_range class PrecollectRetime(api.InstancePlugin): """Calculate Retiming of selected track items.""" diff --git a/openpype/hosts/hiero/startup/Python/Startup/otioexporter/__init__.py b/openpype/hosts/hiero/startup/Python/Startup/otioexporter/__init__.py deleted file mode 100644 index 3c09655f01..0000000000 --- a/openpype/hosts/hiero/startup/Python/Startup/otioexporter/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from OTIOExportTask import OTIOExportTask -from OTIOExportUI import OTIOExportUI - -__all__ = [ - 'OTIOExportTask', - 'OTIOExportUI' -] diff --git a/openpype/hosts/hiero/startup/Python/Startup/selection_tracker.py b/openpype/hosts/hiero/startup/Python/Startup/selection_tracker.py deleted file mode 100644 index b7e05fed7c..0000000000 --- a/openpype/hosts/hiero/startup/Python/Startup/selection_tracker.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Puts the selection project into 'hiero.selection'""" - -import hiero - - -def selectionChanged(event): - hiero.selection = event.sender.selection() - -hiero.core.events.registerInterest('kSelectionChanged', selectionChanged) From 1d8adf52fd44fbb1b306638286018126f8ab3aea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Dec 2021 13:40:21 +0100 Subject: [PATCH 120/307] set explicit window hints in launcher --- openpype/tools/launcher/window.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index c8acbe77c2..a8f65894f2 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -243,7 +243,11 @@ class LauncherWindow(QtWidgets.QDialog): # Allow minimize self.setWindowFlags( - self.windowFlags() | QtCore.Qt.WindowMinimizeButtonHint + QtCore.Qt.Window + | QtCore.Qt.CustomizeWindowHint + | QtCore.Qt.WindowTitleHint + | QtCore.Qt.WindowMinimizeButtonHint + | QtCore.Qt.WindowCloseButtonHint ) project_model = ProjectModel(self.dbcon) From 5ace3472d782e9c2707f0cad626d8b9ae916c23b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Dec 2021 15:27:34 +0100 Subject: [PATCH 121/307] debugging for python3 --- openpype/hosts/hiero/api/lib.py | 7 ++++++- openpype/hosts/hiero/api/plugin.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index ad9b0a6819..7c90c0d618 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -735,9 +735,14 @@ def get_selected_track_items(sequence=None): def set_selected_track_items(track_items_list, sequence=None): _sequence = sequence or get_current_sequence() + # make sure only trackItems are in list selection + only_track_items = [ + i for i in track_items_list + if isinstance(i, hiero.core.TrackItem)] + # Getting selection timeline_editor = hiero.ui.getTimelineEditor(_sequence) - return timeline_editor.setSelection(track_items_list) + return timeline_editor.setSelection(only_track_items) def _read_doc_from_path(path): diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 75d1c1b18f..2bbb1df8c1 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -191,7 +191,7 @@ class CreatorWidget(QtWidgets.QDialog): content_layout = content_layout or self.content_layout[-1] # fix order of process by defined order value - ordered_keys = data.keys() + ordered_keys = list(data.keys()) for k, v in data.items(): try: # try removing a key from index which should From 64c4e5f2bbae767e6f2e126c884853a1fab39eaf Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Wed, 1 Dec 2021 16:42:18 +0100 Subject: [PATCH 122/307] Add static and allView option in imagePlaneLoader --- .../maya/plugins/load/load_image_plane.py | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_image_plane.py b/openpype/hosts/maya/plugins/load/load_image_plane.py index f2640dc2eb..f7a5a7ea18 100644 --- a/openpype/hosts/maya/plugins/load/load_image_plane.py +++ b/openpype/hosts/maya/plugins/load/load_image_plane.py @@ -13,10 +13,14 @@ class CameraWindow(QtWidgets.QDialog): self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint) self.camera = None + self.static_image_plane = False + self.show_in_all_views = False self.widgets = { "label": QtWidgets.QLabel("Select camera for image plane."), "list": QtWidgets.QListWidget(), + "staticImagePlane": QtWidgets.QCheckBox(), + "showInAllViews": QtWidgets.QCheckBox(), "warning": QtWidgets.QLabel("No cameras selected!"), "buttons": QtWidgets.QWidget(), "okButton": QtWidgets.QPushButton("Ok"), @@ -31,6 +35,9 @@ class CameraWindow(QtWidgets.QDialog): for camera in cameras: self.widgets["list"].addItem(camera) + self.widgets["staticImagePlane"].setText("Make Image Plane Static") + self.widgets["showInAllViews"].setText("Show Image Plane in All Views") + # Build buttons. layout = QtWidgets.QHBoxLayout(self.widgets["buttons"]) layout.addWidget(self.widgets["okButton"]) @@ -40,6 +47,8 @@ class CameraWindow(QtWidgets.QDialog): layout = QtWidgets.QVBoxLayout(self) layout.addWidget(self.widgets["label"]) layout.addWidget(self.widgets["list"]) + layout.addWidget(self.widgets["staticImagePlane"]) + layout.addWidget(self.widgets["showInAllViews"]) layout.addWidget(self.widgets["buttons"]) layout.addWidget(self.widgets["warning"]) @@ -54,6 +63,8 @@ class CameraWindow(QtWidgets.QDialog): if self.camera is None: self.widgets["warning"].setVisible(True) return + self.show_in_all_views = self.widgets["showInAllViews"].isChecked() + self.static_image_plane = self.widgets["staticImagePlane"].isChecked() self.close() @@ -65,15 +76,15 @@ class CameraWindow(QtWidgets.QDialog): class ImagePlaneLoader(api.Loader): """Specific loader of plate for image planes on selected camera.""" - families = ["plate", "render"] + families = ["image", "plate", "render"] label = "Load imagePlane." representations = ["mov", "exr", "preview", "png"] icon = "image" color = "orange" - def load(self, context, name, namespace, data): + def load(self, context, name, namespace, data, option=None): import pymel.core as pm - + new_nodes = [] image_plane_depth = 1000 asset = context['asset']['name'] @@ -85,18 +96,16 @@ class ImagePlaneLoader(api.Loader): # Get camera from user selection. camera = None - default_cameras = [ - "frontShape", "perspShape", "sideShape", "topShape" - ] - cameras = [ - x for x in pm.ls(type="camera") if x.name() not in default_cameras - ] + cameras = pm.ls(type="camera") camera_names = {x.getParent().name(): x for x in cameras} camera_names["Create new camera."] = "create_camera" window = CameraWindow(camera_names.keys()) window.exec_() camera = camera_names[window.camera] + is_static_image_plane = window.static_image_plane + is_in_all_views = window.show_in_all_views + if camera == "create_camera": camera = pm.createNode("camera") @@ -111,7 +120,7 @@ class ImagePlaneLoader(api.Loader): # Create image plane image_plane_transform, image_plane_shape = pm.imagePlane( - camera=camera, showInAllViews=False + camera=camera, showInAllViews=is_in_all_views ) image_plane_shape.depth.set(image_plane_depth) @@ -119,6 +128,9 @@ class ImagePlaneLoader(api.Loader): context["representation"]["data"]["path"] ) + if is_static_image_plane: + image_plane_shape.detach() + start_frame = pm.playbackOptions(q=True, min=True) end_frame = pm.playbackOptions(q=True, max=True) From d48fdaeceb3af83131b009c748145a6678367f60 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Dec 2021 18:29:56 +0100 Subject: [PATCH 123/307] Fix - missing extension for new workfile --- openpype/hooks/pre_foundry_apps.py | 2 +- repos/avalon-core | 2 +- .../hosts/nuke/test_publish_in_nuke.py | 41 ++++++++++++++----- tests/lib/db_handler.py | 8 ++-- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/openpype/hooks/pre_foundry_apps.py b/openpype/hooks/pre_foundry_apps.py index 85f68c6b60..70554cbedb 100644 --- a/openpype/hooks/pre_foundry_apps.py +++ b/openpype/hooks/pre_foundry_apps.py @@ -13,7 +13,7 @@ class LaunchFoundryAppsWindows(PreLaunchHook): # Should be as last hook because must change launch arguments to string order = 1000 - app_groups = ["nuke", "nukex", "hiero", "nukestudio"] + app_groups = ["nuke", "nukex", "hiero", "nukestudio", "aftereffects"] platforms = ["windows"] def execute(self): diff --git a/repos/avalon-core b/repos/avalon-core index 7e5efd6885..e37f4f92ed 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 7e5efd6885330d84bb8495975bcab84df49bfa3d +Subproject commit e37f4f92ed25f89c870fdcb7f9538da7d0d7de90 diff --git a/tests/integration/hosts/nuke/test_publish_in_nuke.py b/tests/integration/hosts/nuke/test_publish_in_nuke.py index 3f3f191ac7..abadb0fb92 100644 --- a/tests/integration/hosts/nuke/test_publish_in_nuke.py +++ b/tests/integration/hosts/nuke/test_publish_in_nuke.py @@ -1,9 +1,12 @@ import pytest import os import shutil +import logging from tests.lib.testing_classes import PublishTest +log = logging.getLogger("test_publish_in_nuke") + class TestPublishInNuke(PublishTest): """Basic test case for publishing in Nuke @@ -21,11 +24,11 @@ class TestPublishInNuke(PublishTest): PERSIST = True TEST_FILES = [ - ("1Bciy2pCwMKl1UIpxuPnlX_LHMo_Xkq0K", "test_Nuke_publish.zip", "") + ("1635L4gww9nEkP-1EclfWXNdeDuRjDhey", "test_Nuke_publish.zip", "") ] - APP = "Nuke" - APP_VARIANT = "12" + APP = "nuke" + APP_VARIANT = "12-2" APP_NAME = "{}/{}".format(APP, APP_VARIANT) @@ -37,26 +40,42 @@ class TestPublishInNuke(PublishTest): Maya expects workfile in proper folder, so copy is done first. """ - src_path = os.path.join(download_test_data, - "input", - "workfile", - "test_project_test_asset_TestTask_v001.psd") + print("last_workfile_path") + log.info("log last_workfile_path") + src_path = os.path.join( + download_test_data, + "input", + "workfile", + "test_project_test_asset_CompositingInNuke_v001.nk") dest_folder = os.path.join(download_test_data, self.PROJECT, self.ASSET, "work", self.TASK) os.makedirs(dest_folder) - dest_path = os.path.join(dest_folder, - "test_project_test_asset_TestTask_v001.psd") + dest_path = os.path.join( + dest_folder, "test_project_test_asset_CompositingInNuke_v001.nk") shutil.copy(src_path, dest_path) yield dest_path @pytest.fixture(scope="module") def startup_scripts(self, monkeypatch_session, download_test_data): - """Points Maya to userSetup file from input data""" - pass + """Points Nuke to userSetup file from input data""" + print("startup_scripts") + log.info("log startup_scripts") + startup_path = os.path.join(download_test_data, + "input", + "startup") + startup_path = "C:\\projects\\test_nuke_publish\\input\\startup" + original_pythonpath = os.environ.get("NUKE_PATH") + monkeypatch_session.setenv("NUKE_PATH", + "{}{}{}".format(original_pythonpath, + os.pathsep, + startup_path)) + print("NUKE_PATH:: {}{}{}".format(startup_path, + os.pathsep, + original_pythonpath)) def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" diff --git a/tests/lib/db_handler.py b/tests/lib/db_handler.py index 4dde5ba46e..88cde4d05f 100644 --- a/tests/lib/db_handler.py +++ b/tests/lib/db_handler.py @@ -165,7 +165,7 @@ class DBHandler: if collection: if not db_name: raise ValueError("db_name must be present") - coll_part = "--nsInclude={}.{}".format(db_name, collection) + coll_part = "--collection={}".format(collection) query = "\"{}\" --uri=\"{}\" --out={} {} {}".format( "mongodump", uri, output_path, db_part, coll_part ) @@ -220,11 +220,11 @@ class DBHandler: return query -#handler = DBHandler(uri="mongodb://localhost:27017") +handler = DBHandler(uri="mongodb://localhost:27017") # -#backup_dir = "c:\\projects\\test_nuke_publish\\input\\dumps" +backup_dir = "c:\\projects\\test_nuke_publish\\input\\dumps" # # -#handler.backup_to_dump("avalon", backup_dir, True, collection="test_project") +handler.backup_to_dump("avalon", backup_dir, True, collection="test_project") #handler.setup_from_dump("test_db", backup_dir, True, db_name_out="avalon", collection="test_project") # # handler.setup_from_sql_file("test_db", "c:\\projects\\sql\\item.sql", # # collection="test_project", From 291cfdc3ebf0c06e8f9dc8e85305f527344dad61 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 1 Dec 2021 18:54:53 +0100 Subject: [PATCH 124/307] Delete debug prints --- .../royal_render/plugins/publish/collect_sequences_from_job.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/modules/default_modules/royal_render/plugins/publish/collect_sequences_from_job.py b/openpype/modules/default_modules/royal_render/plugins/publish/collect_sequences_from_job.py index 2505d671af..3eed79fd19 100644 --- a/openpype/modules/default_modules/royal_render/plugins/publish/collect_sequences_from_job.py +++ b/openpype/modules/default_modules/royal_render/plugins/publish/collect_sequences_from_job.py @@ -168,9 +168,6 @@ class CollectSequencesFromJob(pyblish.api.ContextPlugin): start = data.get("frameStart", indices[0]) end = data.get("frameEnd", indices[-1]) - # root = os.path.normpath(root) - # self.log.info("Source: {}}".format(data.get("source", ""))) - ext = list(collection)[0].split('.')[-1] instance.data.update({ From 3d6bf6e8c5eb5e8774a231fa6ff77e1422cf26af Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Wed, 1 Dec 2021 18:56:38 +0100 Subject: [PATCH 125/307] change option by options --- openpype/hosts/maya/plugins/load/load_image_plane.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_image_plane.py b/openpype/hosts/maya/plugins/load/load_image_plane.py index f7a5a7ea18..d7e61b9c64 100644 --- a/openpype/hosts/maya/plugins/load/load_image_plane.py +++ b/openpype/hosts/maya/plugins/load/load_image_plane.py @@ -82,7 +82,7 @@ class ImagePlaneLoader(api.Loader): icon = "image" color = "orange" - def load(self, context, name, namespace, data, option=None): + def load(self, context, name, namespace, data, options=None): import pymel.core as pm new_nodes = [] From 61c87bd4ad51a9913e9354721090f49a0ed4bc26 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 1 Dec 2021 18:56:41 +0100 Subject: [PATCH 126/307] flip updating submodules logic --- tools/build.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/build.sh b/tools/build.sh index bc79f03db7..301f26023a 100755 --- a/tools/build.sh +++ b/tools/build.sh @@ -178,10 +178,10 @@ main () { fi if [ "$disable_submodule_update" == 1 ]; then - echo -e "${BIGreen}>>>${RST} Making sure submodules are up-to-date ..." - git submodule update --init --recursive + echo -e "${BIYellow}***${RST} Not updating submodules ..." else - echo -e "${BIYellow}***${RST} Not updating submodules ..." + echo -e "${BIGreen}>>>${RST} Making sure submodules are up-to-date ..." + git submodule update --init --recursive fi echo -e "${BIGreen}>>>${RST} Building ..." if [[ "$OSTYPE" == "linux-gnu"* ]]; then From 3c7b622b6e3bf56deb28b12138f980b73a4ef6d6 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Thu, 2 Dec 2021 11:53:32 +0100 Subject: [PATCH 127/307] fix rotation after detach --- .../maya/plugins/load/load_image_plane.py | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_image_plane.py b/openpype/hosts/maya/plugins/load/load_image_plane.py index d7e61b9c64..ecfa8d7bc0 100644 --- a/openpype/hosts/maya/plugins/load/load_image_plane.py +++ b/openpype/hosts/maya/plugins/load/load_image_plane.py @@ -96,15 +96,23 @@ class ImagePlaneLoader(api.Loader): # Get camera from user selection. camera = None - cameras = pm.ls(type="camera") - camera_names = {x.getParent().name(): x for x in cameras} - camera_names["Create new camera."] = "create_camera" - window = CameraWindow(camera_names.keys()) - window.exec_() - camera = camera_names[window.camera] + is_static_image_plane = None + is_in_all_views = None + if data: + camera = pm.PyNode(data.get("camera")) + is_static_image_plane = data.get("static_image_plane") + is_in_all_views = data.get("in_all_views") - is_static_image_plane = window.static_image_plane - is_in_all_views = window.show_in_all_views + if not camera: + cameras = pm.ls(type="camera") + camera_names = {x.getParent().name(): x for x in cameras} + camera_names["Create new camera."] = "create_camera" + window = CameraWindow(camera_names.keys()) + window.exec_() + camera = camera_names[window.camera] + + is_static_image_plane = window.static_image_plane + is_in_all_views = window.show_in_all_views if camera == "create_camera": camera = pm.createNode("camera") @@ -129,7 +137,9 @@ class ImagePlaneLoader(api.Loader): ) if is_static_image_plane: + image_plane_shape.setMaintainRatio(True) image_plane_shape.detach() + image_plane_transform.setRotation(camera.getRotation()) start_frame = pm.playbackOptions(q=True, min=True) end_frame = pm.playbackOptions(q=True, max=True) From 4f112d6e015f399e8f8209928bf2a6cc2ca2dc3f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Dec 2021 12:28:21 +0100 Subject: [PATCH 128/307] Fix - added missing targets Was broking regular webpublishes (no via host apps) --- .../plugins/publish/validate_tvpaint_workfile_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/webpublisher/plugins/publish/validate_tvpaint_workfile_data.py b/openpype/hosts/webpublisher/plugins/publish/validate_tvpaint_workfile_data.py index eec6ef1004..a5e4868411 100644 --- a/openpype/hosts/webpublisher/plugins/publish/validate_tvpaint_workfile_data.py +++ b/openpype/hosts/webpublisher/plugins/publish/validate_tvpaint_workfile_data.py @@ -10,6 +10,7 @@ class ValidateWorkfileData(pyblish.api.ContextPlugin): label = "Validate Workfile Data" order = pyblish.api.ValidatorOrder + targets = ["tvpaint_worker"] def process(self, context): # Data collected in `CollectAvalonEntities` From 931896519c05a423f4630001372bdb72d47da847 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Thu, 2 Dec 2021 12:34:34 +0100 Subject: [PATCH 129/307] add fileName in imagePlane cmd --- openpype/hosts/maya/plugins/load/load_image_plane.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_image_plane.py b/openpype/hosts/maya/plugins/load/load_image_plane.py index ecfa8d7bc0..eea5844e8b 100644 --- a/openpype/hosts/maya/plugins/load/load_image_plane.py +++ b/openpype/hosts/maya/plugins/load/load_image_plane.py @@ -128,16 +128,12 @@ class ImagePlaneLoader(api.Loader): # Create image plane image_plane_transform, image_plane_shape = pm.imagePlane( + fileName=context["representation"]["data"]["path"], camera=camera, showInAllViews=is_in_all_views ) image_plane_shape.depth.set(image_plane_depth) - image_plane_shape.imageName.set( - context["representation"]["data"]["path"] - ) - if is_static_image_plane: - image_plane_shape.setMaintainRatio(True) image_plane_shape.detach() image_plane_transform.setRotation(camera.getRotation()) From d27b11774888d1af6dac39c25384bca25c900413 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Thu, 2 Dec 2021 13:52:59 +0100 Subject: [PATCH 130/307] remove context key task from doc --- website/docs/admin_settings_project_anatomy.md | 1 - 1 file changed, 1 deletion(-) diff --git a/website/docs/admin_settings_project_anatomy.md b/website/docs/admin_settings_project_anatomy.md index a8be77d25b..1f742c31ed 100644 --- a/website/docs/admin_settings_project_anatomy.md +++ b/website/docs/admin_settings_project_anatomy.md @@ -61,7 +61,6 @@ We have a few required anatomy templates for OpenPype to work properly, however | `task[type]` | Type of task | | `task[short]` | Shortname of task | | `parent` | Name of hierarchical parent | -| `task` | Name of task | | `version` | Version number | | `subset` | Subset name | | `family` | Main family name | From 611c9bb792ceffa02550a0443fce69e1a9dca6d5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Dec 2021 15:50:48 +0100 Subject: [PATCH 131/307] use PlaceholderLineEdit where placeholder is set --- openpype/tools/loader/app.py | 7 +++++-- openpype/tools/project_manager/project_manager/widgets.py | 3 ++- openpype/tools/publisher/widgets/widgets.py | 5 +++-- openpype/tools/publisher/window.py | 4 ++-- openpype/tools/settings/local_settings/apps_widget.py | 3 ++- openpype/tools/settings/local_settings/general_widget.py | 3 ++- openpype/tools/settings/local_settings/mongo_widget.py | 3 ++- openpype/tools/settings/local_settings/projects_widget.py | 3 ++- openpype/tools/settings/settings/widgets.py | 3 ++- openpype/tools/standalonepublish/widgets/widget_asset.py | 8 ++++++-- openpype/tools/subsetmanager/window.py | 3 ++- openpype/tools/utils/__init__.py | 8 ++++++++ openpype/tools/workfiles/app.py | 7 ++++--- 13 files changed, 42 insertions(+), 18 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index b6becc3e9f..583065633b 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -4,7 +4,10 @@ from Qt import QtWidgets, QtCore from avalon import api, io, pipeline from openpype import style -from openpype.tools.utils import lib +from openpype.tools.utils import ( + lib, + PlaceholderLineEdit +) from openpype.tools.utils.assets_widget import MultiSelectAssetsWidget from .widgets import ( @@ -517,7 +520,7 @@ class SubsetGroupingDialog(QtWidgets.QDialog): self.subsets = parent._subsets_widget self.asset_ids = parent.data["state"]["assetIds"] - name = QtWidgets.QLineEdit() + name = PlaceholderLineEdit(self) name.setPlaceholderText("Remain blank to ungroup..") # Menu for pre-defined subset groups diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py index b4d791b6d5..e4c58a8a2c 100644 --- a/openpype/tools/project_manager/project_manager/widgets.py +++ b/openpype/tools/project_manager/project_manager/widgets.py @@ -10,6 +10,7 @@ from openpype.lib import ( PROJECT_NAME_REGEX ) from openpype.style import load_stylesheet +from openpype.tools.utils import PlaceholderLineEdit from avalon.api import AvalonMongoDB from Qt import QtWidgets, QtCore @@ -345,7 +346,7 @@ class ConfirmProjectDeletion(QtWidgets.QDialog): question_label = QtWidgets.QLabel("Are you sure?", self) - confirm_input = QtWidgets.QLineEdit(self) + confirm_input = PlaceholderLineEdit(self) confirm_input.setPlaceholderText("Type \"Delete\" to confirm...") cancel_btn = _SameSizeBtns("Cancel", self) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index fe00ee78d3..2ebcf73d4e 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -9,6 +9,7 @@ from avalon.vendor import qtawesome from openpype.widgets.attribute_defs import create_widget_for_attr_def from openpype.tools.flickcharm import FlickCharm +from openpype.tools.utils import PlaceholderLineEdit from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS from .models import ( AssetsHierarchyModel, @@ -396,7 +397,7 @@ class AssetsDialog(QtWidgets.QDialog): proxy_model.setSourceModel(model) proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - filter_input = QtWidgets.QLineEdit(self) + filter_input = PlaceholderLineEdit(self) filter_input.setPlaceholderText("Filter assets..") asset_view = QtWidgets.QTreeView(self) @@ -934,7 +935,7 @@ class TasksCombobox(QtWidgets.QComboBox): self.set_selected_items(self._origin_value) -class VariantInputWidget(QtWidgets.QLineEdit): +class VariantInputWidget(PlaceholderLineEdit): """Input widget for variant.""" value_changed = QtCore.Signal() diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index fb5b0c4e92..bb58813e55 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -4,7 +4,7 @@ from openpype import ( resources, style ) - +from openpype.tools.utils import PlaceholderLineEdit from .control import PublisherController from .widgets import ( BorderedLabelWidget, @@ -131,7 +131,7 @@ class PublisherWindow(QtWidgets.QDialog): subset_content_layout.addWidget(subset_attributes_wrap, 7) # Footer - comment_input = QtWidgets.QLineEdit(subset_frame) + comment_input = PlaceholderLineEdit(subset_frame) comment_input.setObjectName("PublishCommentInput") comment_input.setPlaceholderText( "Attach a comment to your publish" diff --git a/openpype/tools/settings/local_settings/apps_widget.py b/openpype/tools/settings/local_settings/apps_widget.py index f06e1ec109..28bc726300 100644 --- a/openpype/tools/settings/local_settings/apps_widget.py +++ b/openpype/tools/settings/local_settings/apps_widget.py @@ -5,6 +5,7 @@ from .widgets import ( ExpandingWidget ) from openpype.tools.settings import CHILD_OFFSET +from openpype.tools.utils import PlaceholderLineEdit class AppVariantWidget(QtWidgets.QWidget): @@ -45,7 +46,7 @@ class AppVariantWidget(QtWidgets.QWidget): content_layout.addWidget(warn_label) return - executable_input_widget = QtWidgets.QLineEdit(content_widget) + executable_input_widget = PlaceholderLineEdit(content_widget) executable_input_widget.setPlaceholderText(self.exec_placeholder) content_layout.addWidget(executable_input_widget) diff --git a/openpype/tools/settings/local_settings/general_widget.py b/openpype/tools/settings/local_settings/general_widget.py index 5bb2bcf378..35add7573e 100644 --- a/openpype/tools/settings/local_settings/general_widget.py +++ b/openpype/tools/settings/local_settings/general_widget.py @@ -3,6 +3,7 @@ import getpass from Qt import QtWidgets, QtCore from openpype.lib import is_admin_password_required from openpype.widgets import PasswordDialog +from openpype.tools.utils import PlaceholderLineEdit class LocalGeneralWidgets(QtWidgets.QWidget): @@ -11,7 +12,7 @@ class LocalGeneralWidgets(QtWidgets.QWidget): self._loading_local_settings = False - username_input = QtWidgets.QLineEdit(self) + username_input = PlaceholderLineEdit(self) username_input.setPlaceholderText(getpass.getuser()) is_admin_input = QtWidgets.QCheckBox(self) diff --git a/openpype/tools/settings/local_settings/mongo_widget.py b/openpype/tools/settings/local_settings/mongo_widget.py index eebafdffdd..3d3dbd0a5d 100644 --- a/openpype/tools/settings/local_settings/mongo_widget.py +++ b/openpype/tools/settings/local_settings/mongo_widget.py @@ -6,6 +6,7 @@ from Qt import QtWidgets from pymongo.errors import ServerSelectionTimeoutError from openpype.api import change_openpype_mongo_url +from openpype.tools.utils import PlaceholderLineEdit class OpenPypeMongoWidget(QtWidgets.QWidget): @@ -25,7 +26,7 @@ class OpenPypeMongoWidget(QtWidgets.QWidget): mongo_url_label = QtWidgets.QLabel("OpenPype Mongo URL", self) # Input - mongo_url_input = QtWidgets.QLineEdit(self) + mongo_url_input = PlaceholderLineEdit(self) mongo_url_input.setPlaceholderText("< OpenPype Mongo URL >") mongo_url_input.setText(os.environ["OPENPYPE_MONGO"]) diff --git a/openpype/tools/settings/local_settings/projects_widget.py b/openpype/tools/settings/local_settings/projects_widget.py index 7e2ad661a0..da45467a4e 100644 --- a/openpype/tools/settings/local_settings/projects_widget.py +++ b/openpype/tools/settings/local_settings/projects_widget.py @@ -2,6 +2,7 @@ import platform import copy from Qt import QtWidgets, QtCore, QtGui from openpype.tools.settings.settings import ProjectListWidget +from openpype.tools.utils import PlaceholderLineEdit from openpype.settings.constants import ( PROJECT_ANATOMY_KEY, DEFAULT_PROJECT_KEY @@ -45,7 +46,7 @@ class DynamicInputItem(QtCore.QObject): parent ): super(DynamicInputItem, self).__init__() - input_widget = QtWidgets.QLineEdit(parent) + input_widget = PlaceholderLineEdit(parent) settings_value = input_def.get("value") placeholder = input_def.get("placeholder") diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 7a7213fa66..ac9870287b 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -11,6 +11,7 @@ from openpype.tools.utils.widgets import ImageButton from openpype.tools.utils.lib import paint_image_with_color from openpype.widgets.nice_checkbox import NiceCheckbox +from openpype.tools.utils import PlaceholderLineEdit from openpype.settings.lib import get_system_settings from .images import ( get_pixmap, @@ -24,7 +25,7 @@ from .constants import ( ) -class SettingsLineEdit(QtWidgets.QLineEdit): +class SettingsLineEdit(PlaceholderLineEdit): focused_in = QtCore.Signal() def focusInEvent(self, event): diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index eb22883c11..f4a4dfe0c4 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -1,8 +1,12 @@ import contextlib from Qt import QtWidgets, QtCore -from . import RecursiveSortFilterProxyModel, AssetModel + +from openpype.tools.utils import PlaceholderLineEdit + from avalon.vendor import qtawesome from avalon import style + +from . import RecursiveSortFilterProxyModel, AssetModel from . import TasksTemplateModel, DeselectableTreeView from . import _iter_model_rows @@ -165,7 +169,7 @@ class AssetWidget(QtWidgets.QWidget): refresh = QtWidgets.QPushButton(icon, "") refresh.setToolTip("Refresh items") - filter = QtWidgets.QLineEdit() + filter = PlaceholderLineEdit() filter.textChanged.connect(proxy.setFilterFixedString) filter.setPlaceholderText("Filter assets..") diff --git a/openpype/tools/subsetmanager/window.py b/openpype/tools/subsetmanager/window.py index cb0e3c1c1e..b7430d0626 100644 --- a/openpype/tools/subsetmanager/window.py +++ b/openpype/tools/subsetmanager/window.py @@ -7,6 +7,7 @@ from avalon import api from avalon.vendor import qtawesome from openpype import style +from openpype.tools.utils import PlaceholderLineEdit from openpype.tools.utils.lib import ( iter_model_rows, qt_app_context @@ -44,7 +45,7 @@ class SubsetManagerWindow(QtWidgets.QDialog): header_widget = QtWidgets.QWidget(left_side_widget) # Filter input - filter_input = QtWidgets.QLineEdit(header_widget) + filter_input = PlaceholderLineEdit(header_widget) filter_input.setPlaceholderText("Filter subsets..") # Refresh button diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index e69de29bb2..7f15e64767 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -0,0 +1,8 @@ +from .widgets import ( + PlaceholderLineEdit, +) + + +__all__ = ( + "PlaceholderLineEdit", +) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index a4b1717a1c..d33294e4ad 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -15,6 +15,7 @@ from openpype.tools.utils.lib import ( schedule, qt_app_context ) +from openpype.tools.utils import PlaceholderLineEdit from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget from openpype.tools.utils.tasks_widget import TasksWidget from openpype.tools.utils.delegates import PrettyTimeDelegate @@ -139,7 +140,7 @@ class NameWindow(QtWidgets.QDialog): preview_label = QtWidgets.QLabel("Preview filename", inputs_widget) # Subversion input - subversion_input = QtWidgets.QLineEdit(inputs_widget) + subversion_input = PlaceholderLineEdit(inputs_widget) subversion_input.setPlaceholderText("Will be part of filename.") # Extensions combobox @@ -394,9 +395,9 @@ class FilesWidget(QtWidgets.QWidget): files_view.setColumnWidth(0, 330) # Filtering input - filter_input = QtWidgets.QLineEdit(self) - filter_input.textChanged.connect(proxy_model.setFilterFixedString) + filter_input = PlaceholderLineEdit(self) filter_input.setPlaceholderText("Filter files..") + filter_input.textChanged.connect(proxy_model.setFilterFixedString) # Home Page # Build buttons widget for files widget From 991328c516a16c206bf5abacc58738a95aba9b11 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Dec 2021 16:29:26 +0100 Subject: [PATCH 132/307] moved clockify module --- openpype/modules/base.py | 1 + openpype/modules/{default_modules => }/clockify/__init__.py | 0 openpype/modules/{default_modules => }/clockify/clockify_api.py | 0 .../modules/{default_modules => }/clockify/clockify_module.py | 0 openpype/modules/{default_modules => }/clockify/constants.py | 0 .../clockify/ftrack/server/action_clockify_sync_server.py | 0 .../clockify/ftrack/user/action_clockify_sync_local.py | 0 .../clockify/launcher_actions/ClockifyStart.py | 0 .../clockify/launcher_actions/ClockifySync.py | 0 openpype/modules/{default_modules => }/clockify/widgets.py | 0 10 files changed, 1 insertion(+) rename openpype/modules/{default_modules => }/clockify/__init__.py (100%) rename openpype/modules/{default_modules => }/clockify/clockify_api.py (100%) rename openpype/modules/{default_modules => }/clockify/clockify_module.py (100%) rename openpype/modules/{default_modules => }/clockify/constants.py (100%) rename openpype/modules/{default_modules => }/clockify/ftrack/server/action_clockify_sync_server.py (100%) rename openpype/modules/{default_modules => }/clockify/ftrack/user/action_clockify_sync_local.py (100%) rename openpype/modules/{default_modules => }/clockify/launcher_actions/ClockifyStart.py (100%) rename openpype/modules/{default_modules => }/clockify/launcher_actions/ClockifySync.py (100%) rename openpype/modules/{default_modules => }/clockify/widgets.py (100%) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 7ecfeae7bd..fdca9e90be 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -273,6 +273,7 @@ def _load_modules(): # Import default modules imported from 'openpype.modules' for default_module_name in ( + "clockify", "settings_action", "launcher_action", "project_manager_action", diff --git a/openpype/modules/default_modules/clockify/__init__.py b/openpype/modules/clockify/__init__.py similarity index 100% rename from openpype/modules/default_modules/clockify/__init__.py rename to openpype/modules/clockify/__init__.py diff --git a/openpype/modules/default_modules/clockify/clockify_api.py b/openpype/modules/clockify/clockify_api.py similarity index 100% rename from openpype/modules/default_modules/clockify/clockify_api.py rename to openpype/modules/clockify/clockify_api.py diff --git a/openpype/modules/default_modules/clockify/clockify_module.py b/openpype/modules/clockify/clockify_module.py similarity index 100% rename from openpype/modules/default_modules/clockify/clockify_module.py rename to openpype/modules/clockify/clockify_module.py diff --git a/openpype/modules/default_modules/clockify/constants.py b/openpype/modules/clockify/constants.py similarity index 100% rename from openpype/modules/default_modules/clockify/constants.py rename to openpype/modules/clockify/constants.py diff --git a/openpype/modules/default_modules/clockify/ftrack/server/action_clockify_sync_server.py b/openpype/modules/clockify/ftrack/server/action_clockify_sync_server.py similarity index 100% rename from openpype/modules/default_modules/clockify/ftrack/server/action_clockify_sync_server.py rename to openpype/modules/clockify/ftrack/server/action_clockify_sync_server.py diff --git a/openpype/modules/default_modules/clockify/ftrack/user/action_clockify_sync_local.py b/openpype/modules/clockify/ftrack/user/action_clockify_sync_local.py similarity index 100% rename from openpype/modules/default_modules/clockify/ftrack/user/action_clockify_sync_local.py rename to openpype/modules/clockify/ftrack/user/action_clockify_sync_local.py diff --git a/openpype/modules/default_modules/clockify/launcher_actions/ClockifyStart.py b/openpype/modules/clockify/launcher_actions/ClockifyStart.py similarity index 100% rename from openpype/modules/default_modules/clockify/launcher_actions/ClockifyStart.py rename to openpype/modules/clockify/launcher_actions/ClockifyStart.py diff --git a/openpype/modules/default_modules/clockify/launcher_actions/ClockifySync.py b/openpype/modules/clockify/launcher_actions/ClockifySync.py similarity index 100% rename from openpype/modules/default_modules/clockify/launcher_actions/ClockifySync.py rename to openpype/modules/clockify/launcher_actions/ClockifySync.py diff --git a/openpype/modules/default_modules/clockify/widgets.py b/openpype/modules/clockify/widgets.py similarity index 100% rename from openpype/modules/default_modules/clockify/widgets.py rename to openpype/modules/clockify/widgets.py From 568726d30f25805190e26cdbb874180ac5117601 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Dec 2021 16:29:53 +0100 Subject: [PATCH 133/307] moved log viewer module --- openpype/modules/base.py | 1 + openpype/modules/{default_modules => }/log_viewer/__init__.py | 0 .../modules/{default_modules => }/log_viewer/log_view_module.py | 0 .../modules/{default_modules => }/log_viewer/tray/__init__.py | 0 openpype/modules/{default_modules => }/log_viewer/tray/app.py | 0 openpype/modules/{default_modules => }/log_viewer/tray/models.py | 0 .../modules/{default_modules => }/log_viewer/tray/widgets.py | 0 7 files changed, 1 insertion(+) rename openpype/modules/{default_modules => }/log_viewer/__init__.py (100%) rename openpype/modules/{default_modules => }/log_viewer/log_view_module.py (100%) rename openpype/modules/{default_modules => }/log_viewer/tray/__init__.py (100%) rename openpype/modules/{default_modules => }/log_viewer/tray/app.py (100%) rename openpype/modules/{default_modules => }/log_viewer/tray/models.py (100%) rename openpype/modules/{default_modules => }/log_viewer/tray/widgets.py (100%) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index fdca9e90be..1dc9ccc718 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -273,6 +273,7 @@ def _load_modules(): # Import default modules imported from 'openpype.modules' for default_module_name in ( + "log_viewer", "clockify", "settings_action", "launcher_action", diff --git a/openpype/modules/default_modules/log_viewer/__init__.py b/openpype/modules/log_viewer/__init__.py similarity index 100% rename from openpype/modules/default_modules/log_viewer/__init__.py rename to openpype/modules/log_viewer/__init__.py diff --git a/openpype/modules/default_modules/log_viewer/log_view_module.py b/openpype/modules/log_viewer/log_view_module.py similarity index 100% rename from openpype/modules/default_modules/log_viewer/log_view_module.py rename to openpype/modules/log_viewer/log_view_module.py diff --git a/openpype/modules/default_modules/log_viewer/tray/__init__.py b/openpype/modules/log_viewer/tray/__init__.py similarity index 100% rename from openpype/modules/default_modules/log_viewer/tray/__init__.py rename to openpype/modules/log_viewer/tray/__init__.py diff --git a/openpype/modules/default_modules/log_viewer/tray/app.py b/openpype/modules/log_viewer/tray/app.py similarity index 100% rename from openpype/modules/default_modules/log_viewer/tray/app.py rename to openpype/modules/log_viewer/tray/app.py diff --git a/openpype/modules/default_modules/log_viewer/tray/models.py b/openpype/modules/log_viewer/tray/models.py similarity index 100% rename from openpype/modules/default_modules/log_viewer/tray/models.py rename to openpype/modules/log_viewer/tray/models.py diff --git a/openpype/modules/default_modules/log_viewer/tray/widgets.py b/openpype/modules/log_viewer/tray/widgets.py similarity index 100% rename from openpype/modules/default_modules/log_viewer/tray/widgets.py rename to openpype/modules/log_viewer/tray/widgets.py From 38bfece22befe69a5320f70ba3d01a1804630666 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Dec 2021 16:30:21 +0100 Subject: [PATCH 134/307] moved muster module --- openpype/modules/base.py | 1 + openpype/modules/{default_modules => }/muster/__init__.py | 0 openpype/modules/{default_modules => }/muster/muster.py | 0 openpype/modules/{default_modules => }/muster/rest_api.py | 0 openpype/modules/{default_modules => }/muster/widget_login.py | 0 5 files changed, 1 insertion(+) rename openpype/modules/{default_modules => }/muster/__init__.py (100%) rename openpype/modules/{default_modules => }/muster/muster.py (100%) rename openpype/modules/{default_modules => }/muster/rest_api.py (100%) rename openpype/modules/{default_modules => }/muster/widget_login.py (100%) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 1dc9ccc718..2fa1967690 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -274,6 +274,7 @@ def _load_modules(): # Import default modules imported from 'openpype.modules' for default_module_name in ( "log_viewer", + "muster", "clockify", "settings_action", "launcher_action", diff --git a/openpype/modules/default_modules/muster/__init__.py b/openpype/modules/muster/__init__.py similarity index 100% rename from openpype/modules/default_modules/muster/__init__.py rename to openpype/modules/muster/__init__.py diff --git a/openpype/modules/default_modules/muster/muster.py b/openpype/modules/muster/muster.py similarity index 100% rename from openpype/modules/default_modules/muster/muster.py rename to openpype/modules/muster/muster.py diff --git a/openpype/modules/default_modules/muster/rest_api.py b/openpype/modules/muster/rest_api.py similarity index 100% rename from openpype/modules/default_modules/muster/rest_api.py rename to openpype/modules/muster/rest_api.py diff --git a/openpype/modules/default_modules/muster/widget_login.py b/openpype/modules/muster/widget_login.py similarity index 100% rename from openpype/modules/default_modules/muster/widget_login.py rename to openpype/modules/muster/widget_login.py From db31a6aea9f28d04c196979f1eaf6f9dc230543c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Dec 2021 16:30:46 +0100 Subject: [PATCH 135/307] moved python console interpreted module --- openpype/modules/base.py | 1 + .../{default_modules => }/python_console_interpreter/__init__.py | 0 .../{default_modules => }/python_console_interpreter/module.py | 0 .../python_console_interpreter/window/__init__.py | 0 .../python_console_interpreter/window/widgets.py | 0 5 files changed, 1 insertion(+) rename openpype/modules/{default_modules => }/python_console_interpreter/__init__.py (100%) rename openpype/modules/{default_modules => }/python_console_interpreter/module.py (100%) rename openpype/modules/{default_modules => }/python_console_interpreter/window/__init__.py (100%) rename openpype/modules/{default_modules => }/python_console_interpreter/window/widgets.py (100%) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 2fa1967690..e03fa6c45b 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -273,6 +273,7 @@ def _load_modules(): # Import default modules imported from 'openpype.modules' for default_module_name in ( + "python_console_interpreter", "log_viewer", "muster", "clockify", diff --git a/openpype/modules/default_modules/python_console_interpreter/__init__.py b/openpype/modules/python_console_interpreter/__init__.py similarity index 100% rename from openpype/modules/default_modules/python_console_interpreter/__init__.py rename to openpype/modules/python_console_interpreter/__init__.py diff --git a/openpype/modules/default_modules/python_console_interpreter/module.py b/openpype/modules/python_console_interpreter/module.py similarity index 100% rename from openpype/modules/default_modules/python_console_interpreter/module.py rename to openpype/modules/python_console_interpreter/module.py diff --git a/openpype/modules/default_modules/python_console_interpreter/window/__init__.py b/openpype/modules/python_console_interpreter/window/__init__.py similarity index 100% rename from openpype/modules/default_modules/python_console_interpreter/window/__init__.py rename to openpype/modules/python_console_interpreter/window/__init__.py diff --git a/openpype/modules/default_modules/python_console_interpreter/window/widgets.py b/openpype/modules/python_console_interpreter/window/widgets.py similarity index 100% rename from openpype/modules/default_modules/python_console_interpreter/window/widgets.py rename to openpype/modules/python_console_interpreter/window/widgets.py From 615f9145e140c42940e352bb4b1d9f4ecce35559 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Dec 2021 16:31:22 +0100 Subject: [PATCH 136/307] moved slack module --- openpype/modules/base.py | 1 + .../modules/{default_modules => }/slack/README.md | 0 .../modules/{default_modules => }/slack/__init__.py | 0 .../slack/launch_hooks/pre_python2_vendor.py | 0 .../{default_modules => }/slack/manifest.yml | 0 .../slack/plugins/publish/collect_slack_family.py | 0 .../slack/plugins/publish/integrate_slack_api.py | 0 .../python2_vendor/python-slack-sdk-1/.appveyor.yml | 0 .../python2_vendor/python-slack-sdk-1/.coveragerc | 0 .../slack/python2_vendor/python-slack-sdk-1/.flake8 | 0 .../python-slack-sdk-1/.github/contributing.md | 0 .../python-slack-sdk-1/.github/issue_template.md | 0 .../python-slack-sdk-1/.github/maintainers_guide.md | 0 .../.github/pull_request_template.md | 0 .../python2_vendor/python-slack-sdk-1/.gitignore | 0 .../python2_vendor/python-slack-sdk-1/.travis.yml | 0 .../slack/python2_vendor/python-slack-sdk-1/LICENSE | 0 .../python2_vendor/python-slack-sdk-1/MANIFEST.in | 0 .../python2_vendor/python-slack-sdk-1/README.rst | 0 .../python-slack-sdk-1/docs-src/.gitignore | 0 .../python-slack-sdk-1/docs-src/Makefile | 0 .../docs-src/_themes/slack/conf.py | 0 .../docs-src/_themes/slack/layout.html | 0 .../docs-src/_themes/slack/localtoc.html | 0 .../docs-src/_themes/slack/relations.html | 0 .../docs-src/_themes/slack/sidebar.html | 0 .../docs-src/_themes/slack/static/default.css_t | 0 .../docs-src/_themes/slack/static/docs.css_t | 0 .../docs-src/_themes/slack/static/pygments.css_t | 0 .../docs-src/_themes/slack/theme.conf | 0 .../python-slack-sdk-1/docs-src/about.rst | 0 .../python-slack-sdk-1/docs-src/auth.rst | 0 .../python-slack-sdk-1/docs-src/basic_usage.rst | 0 .../python-slack-sdk-1/docs-src/changelog.rst | 0 .../python-slack-sdk-1/docs-src/conf.py | 0 .../python-slack-sdk-1/docs-src/conversations.rst | 0 .../python-slack-sdk-1/docs-src/faq.rst | 0 .../python-slack-sdk-1/docs-src/index.rst | 0 .../python-slack-sdk-1/docs-src/make.bat | 0 .../python-slack-sdk-1/docs-src/metadata.rst | 0 .../docs-src/real_time_messaging.rst | 0 .../slack/python2_vendor/python-slack-sdk-1/docs.sh | 0 .../python-slack-sdk-1/docs/.buildinfo | 0 .../python-slack-sdk-1/docs/.nojekyll | 0 .../python-slack-sdk-1/docs/_static/ajax-loader.gif | Bin .../python-slack-sdk-1/docs/_static/basic.css | 0 .../python-slack-sdk-1/docs/_static/classic.css | 0 .../docs/_static/comment-bright.png | Bin .../docs/_static/comment-close.png | Bin .../python-slack-sdk-1/docs/_static/comment.png | Bin .../python-slack-sdk-1/docs/_static/default.css | 0 .../python-slack-sdk-1/docs/_static/docs.css | 0 .../python-slack-sdk-1/docs/_static/doctools.js | 0 .../docs/_static/documentation_options.js | 0 .../docs/_static/down-pressed.png | Bin .../python-slack-sdk-1/docs/_static/down.png | Bin .../python-slack-sdk-1/docs/_static/file.png | Bin .../python-slack-sdk-1/docs/_static/jquery-3.2.1.js | 0 .../python-slack-sdk-1/docs/_static/jquery.js | 0 .../docs/_static/language_data.js | 0 .../python-slack-sdk-1/docs/_static/minus.png | Bin .../python-slack-sdk-1/docs/_static/plus.png | Bin .../python-slack-sdk-1/docs/_static/pygments.css | 0 .../python-slack-sdk-1/docs/_static/searchtools.js | 0 .../python-slack-sdk-1/docs/_static/sidebar.js | 0 .../docs/_static/underscore-1.3.1.js | 0 .../python-slack-sdk-1/docs/_static/underscore.js | 0 .../python-slack-sdk-1/docs/_static/up-pressed.png | Bin .../python-slack-sdk-1/docs/_static/up.png | Bin .../python-slack-sdk-1/docs/_static/websupport.js | 0 .../python-slack-sdk-1/docs/about.html | 0 .../python-slack-sdk-1/docs/auth.html | 0 .../python-slack-sdk-1/docs/basic_usage.html | 0 .../python-slack-sdk-1/docs/changelog.html | 0 .../python-slack-sdk-1/docs/conversations.html | 0 .../python2_vendor/python-slack-sdk-1/docs/faq.html | 0 .../python-slack-sdk-1/docs/genindex.html | 0 .../python-slack-sdk-1/docs/index.html | 0 .../python-slack-sdk-1/docs/metadata.html | 0 .../python-slack-sdk-1/docs/objects.inv | 0 .../docs/real_time_messaging.html | 0 .../python-slack-sdk-1/docs/search.html | 0 .../python-slack-sdk-1/docs/searchindex.js | 0 .../python-slack-sdk-1/requirements.txt | 0 .../python2_vendor/python-slack-sdk-1/setup.cfg | 0 .../python2_vendor/python-slack-sdk-1/setup.py | 0 .../python-slack-sdk-1/slackclient/__init__.py | 0 .../python-slack-sdk-1/slackclient/channel.py | 0 .../python-slack-sdk-1/slackclient/client.py | 0 .../python-slack-sdk-1/slackclient/exceptions.py | 0 .../python-slack-sdk-1/slackclient/im.py | 0 .../python-slack-sdk-1/slackclient/server.py | 0 .../python-slack-sdk-1/slackclient/slackrequest.py | 0 .../python-slack-sdk-1/slackclient/user.py | 0 .../python-slack-sdk-1/slackclient/util.py | 0 .../python-slack-sdk-1/slackclient/version.py | 0 .../python-slack-sdk-1/test_requirements.txt | 0 .../python-slack-sdk-1/tests/conftest.py | 0 .../tests/data/channel.created.json | 0 .../python-slack-sdk-1/tests/data/im.created.json | 0 .../python-slack-sdk-1/tests/data/rtm.start.json | 0 .../python-slack-sdk-1/tests/data/slack_logo.png | Bin .../python-slack-sdk-1/tests/test_channel.py | 0 .../python-slack-sdk-1/tests/test_server.py | 0 .../python-slack-sdk-1/tests/test_slackclient.py | 0 .../python-slack-sdk-1/tests/test_slackrequest.py | 0 .../slack/python2_vendor/python-slack-sdk-1/tox.ini | 0 .../{default_modules => }/slack/slack_module.py | 0 108 files changed, 1 insertion(+) rename openpype/modules/{default_modules => }/slack/README.md (100%) rename openpype/modules/{default_modules => }/slack/__init__.py (100%) rename openpype/modules/{default_modules => }/slack/launch_hooks/pre_python2_vendor.py (100%) rename openpype/modules/{default_modules => }/slack/manifest.yml (100%) rename openpype/modules/{default_modules => }/slack/plugins/publish/collect_slack_family.py (100%) rename openpype/modules/{default_modules => }/slack/plugins/publish/integrate_slack_api.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/.appveyor.yml (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/.coveragerc (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/.flake8 (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/.github/contributing.md (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/.github/issue_template.md (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/.github/maintainers_guide.md (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/.github/pull_request_template.md (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/.gitignore (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/.travis.yml (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/LICENSE (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/MANIFEST.in (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/README.rst (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/.gitignore (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/Makefile (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/conf.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/layout.html (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/localtoc.html (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/relations.html (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/sidebar.html (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/default.css_t (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/docs.css_t (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/pygments.css_t (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/theme.conf (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/about.rst (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/auth.rst (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/basic_usage.rst (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/changelog.rst (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/conf.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/conversations.rst (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/faq.rst (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/index.rst (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/make.bat (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/metadata.rst (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs-src/real_time_messaging.rst (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs.sh (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/.buildinfo (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/.nojekyll (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/ajax-loader.gif (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/basic.css (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/classic.css (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-bright.png (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-close.png (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment.png (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/default.css (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/docs.css (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/doctools.js (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/documentation_options.js (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/down-pressed.png (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/down.png (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/file.png (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery-3.2.1.js (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery.js (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/language_data.js (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/minus.png (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/plus.png (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/pygments.css (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/searchtools.js (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/sidebar.js (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore-1.3.1.js (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore.js (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/up-pressed.png (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/up.png (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/_static/websupport.js (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/about.html (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/auth.html (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/basic_usage.html (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/changelog.html (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/conversations.html (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/faq.html (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/genindex.html (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/index.html (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/metadata.html (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/objects.inv (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/real_time_messaging.html (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/search.html (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/docs/searchindex.js (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/requirements.txt (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/setup.cfg (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/setup.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/slackclient/__init__.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/slackclient/channel.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/slackclient/client.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/slackclient/exceptions.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/slackclient/im.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/slackclient/server.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/slackclient/slackrequest.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/slackclient/user.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/slackclient/util.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/slackclient/version.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/test_requirements.txt (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/tests/conftest.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/tests/data/channel.created.json (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/tests/data/im.created.json (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/tests/data/rtm.start.json (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/tests/data/slack_logo.png (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/tests/test_channel.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/tests/test_server.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/tests/test_slackclient.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/tests/test_slackrequest.py (100%) rename openpype/modules/{default_modules => }/slack/python2_vendor/python-slack-sdk-1/tox.ini (100%) rename openpype/modules/{default_modules => }/slack/slack_module.py (100%) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index e03fa6c45b..eb40315dfa 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -274,6 +274,7 @@ def _load_modules(): # Import default modules imported from 'openpype.modules' for default_module_name in ( "python_console_interpreter", + "slack", "log_viewer", "muster", "clockify", diff --git a/openpype/modules/default_modules/slack/README.md b/openpype/modules/slack/README.md similarity index 100% rename from openpype/modules/default_modules/slack/README.md rename to openpype/modules/slack/README.md diff --git a/openpype/modules/default_modules/slack/__init__.py b/openpype/modules/slack/__init__.py similarity index 100% rename from openpype/modules/default_modules/slack/__init__.py rename to openpype/modules/slack/__init__.py diff --git a/openpype/modules/default_modules/slack/launch_hooks/pre_python2_vendor.py b/openpype/modules/slack/launch_hooks/pre_python2_vendor.py similarity index 100% rename from openpype/modules/default_modules/slack/launch_hooks/pre_python2_vendor.py rename to openpype/modules/slack/launch_hooks/pre_python2_vendor.py diff --git a/openpype/modules/default_modules/slack/manifest.yml b/openpype/modules/slack/manifest.yml similarity index 100% rename from openpype/modules/default_modules/slack/manifest.yml rename to openpype/modules/slack/manifest.yml diff --git a/openpype/modules/default_modules/slack/plugins/publish/collect_slack_family.py b/openpype/modules/slack/plugins/publish/collect_slack_family.py similarity index 100% rename from openpype/modules/default_modules/slack/plugins/publish/collect_slack_family.py rename to openpype/modules/slack/plugins/publish/collect_slack_family.py diff --git a/openpype/modules/default_modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py similarity index 100% rename from openpype/modules/default_modules/slack/plugins/publish/integrate_slack_api.py rename to openpype/modules/slack/plugins/publish/integrate_slack_api.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.appveyor.yml b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.appveyor.yml similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.appveyor.yml rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.appveyor.yml diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.coveragerc b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.coveragerc similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.coveragerc rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.coveragerc diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.flake8 b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.flake8 similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.flake8 rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.flake8 diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.github/contributing.md b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/contributing.md similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.github/contributing.md rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/contributing.md diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.github/issue_template.md b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/issue_template.md similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.github/issue_template.md rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/issue_template.md diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.github/maintainers_guide.md b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/maintainers_guide.md similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.github/maintainers_guide.md rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/maintainers_guide.md diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.github/pull_request_template.md b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/pull_request_template.md similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.github/pull_request_template.md rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/pull_request_template.md diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.gitignore b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.gitignore similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.gitignore rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.gitignore diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.travis.yml b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.travis.yml similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.travis.yml rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.travis.yml diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/LICENSE b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/LICENSE similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/LICENSE rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/LICENSE diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/MANIFEST.in b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/MANIFEST.in similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/MANIFEST.in rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/MANIFEST.in diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/README.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/README.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/README.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/README.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/.gitignore b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/.gitignore similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/.gitignore rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/.gitignore diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/Makefile b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/Makefile similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/Makefile rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/Makefile diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/conf.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/conf.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/conf.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/conf.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/layout.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/layout.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/layout.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/layout.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/localtoc.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/localtoc.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/localtoc.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/localtoc.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/relations.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/relations.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/relations.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/relations.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/sidebar.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/sidebar.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/sidebar.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/sidebar.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/default.css_t b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/default.css_t similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/default.css_t rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/default.css_t diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/docs.css_t b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/docs.css_t similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/docs.css_t rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/docs.css_t diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/pygments.css_t b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/pygments.css_t similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/pygments.css_t rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/pygments.css_t diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/theme.conf b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/theme.conf similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/theme.conf rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/theme.conf diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/about.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/about.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/about.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/about.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/auth.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/auth.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/auth.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/auth.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/basic_usage.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/basic_usage.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/basic_usage.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/basic_usage.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/changelog.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/changelog.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/changelog.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/changelog.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conf.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conf.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conf.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conf.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conversations.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conversations.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conversations.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conversations.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/faq.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/faq.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/faq.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/faq.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/index.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/index.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/index.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/index.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/make.bat b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/make.bat similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/make.bat rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/make.bat diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/metadata.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/metadata.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/metadata.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/metadata.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/real_time_messaging.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/real_time_messaging.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/real_time_messaging.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/real_time_messaging.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs.sh b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs.sh similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs.sh rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs.sh diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/.buildinfo b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/.buildinfo similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/.buildinfo rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/.buildinfo diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/.nojekyll b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/.nojekyll similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/.nojekyll rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/.nojekyll diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/ajax-loader.gif b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/ajax-loader.gif similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/ajax-loader.gif rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/ajax-loader.gif diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/basic.css b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/basic.css similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/basic.css rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/basic.css diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/classic.css b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/classic.css similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/classic.css rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/classic.css diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-bright.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-bright.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-bright.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-bright.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-close.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-close.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-close.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-close.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/default.css b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/default.css similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/default.css rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/default.css diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/docs.css b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/docs.css similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/docs.css rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/docs.css diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/doctools.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/doctools.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/doctools.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/doctools.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/documentation_options.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/documentation_options.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/documentation_options.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/documentation_options.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/down-pressed.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/down-pressed.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/down-pressed.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/down-pressed.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/down.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/down.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/down.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/down.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/file.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/file.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/file.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/file.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery-3.2.1.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery-3.2.1.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery-3.2.1.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery-3.2.1.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/language_data.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/language_data.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/language_data.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/language_data.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/minus.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/minus.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/minus.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/minus.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/plus.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/plus.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/plus.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/plus.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/pygments.css b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/pygments.css similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/pygments.css rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/pygments.css diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/searchtools.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/searchtools.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/searchtools.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/searchtools.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/sidebar.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/sidebar.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/sidebar.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/sidebar.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore-1.3.1.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore-1.3.1.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore-1.3.1.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore-1.3.1.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/up-pressed.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/up-pressed.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/up-pressed.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/up-pressed.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/up.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/up.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/up.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/up.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/websupport.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/websupport.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/websupport.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/websupport.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/about.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/about.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/about.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/about.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/auth.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/auth.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/auth.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/auth.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/basic_usage.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/basic_usage.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/basic_usage.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/basic_usage.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/changelog.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/changelog.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/changelog.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/changelog.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/conversations.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/conversations.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/conversations.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/conversations.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/faq.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/faq.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/faq.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/faq.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/genindex.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/genindex.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/genindex.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/genindex.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/index.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/index.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/index.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/index.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/metadata.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/metadata.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/metadata.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/metadata.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/objects.inv b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/objects.inv similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/objects.inv rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/objects.inv diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/real_time_messaging.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/real_time_messaging.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/real_time_messaging.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/real_time_messaging.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/search.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/search.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/search.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/search.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/searchindex.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/searchindex.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/searchindex.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/searchindex.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/requirements.txt b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/requirements.txt similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/requirements.txt rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/requirements.txt diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/setup.cfg b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/setup.cfg similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/setup.cfg rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/setup.cfg diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/setup.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/setup.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/setup.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/setup.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/__init__.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/__init__.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/__init__.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/__init__.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/channel.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/channel.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/channel.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/channel.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/client.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/client.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/client.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/client.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/exceptions.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/exceptions.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/exceptions.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/exceptions.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/im.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/im.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/im.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/im.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/server.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/server.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/server.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/server.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/slackrequest.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/slackrequest.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/slackrequest.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/slackrequest.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/user.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/user.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/user.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/user.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/util.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/util.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/util.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/util.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/version.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/version.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/version.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/version.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/test_requirements.txt b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/test_requirements.txt similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/test_requirements.txt rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/test_requirements.txt diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/conftest.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/conftest.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/conftest.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/conftest.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/data/channel.created.json b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/channel.created.json similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/data/channel.created.json rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/channel.created.json diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/data/im.created.json b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/im.created.json similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/data/im.created.json rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/im.created.json diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/data/rtm.start.json b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/rtm.start.json similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/data/rtm.start.json rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/rtm.start.json diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/data/slack_logo.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/slack_logo.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/data/slack_logo.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/slack_logo.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/test_channel.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_channel.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/test_channel.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_channel.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/test_server.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_server.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/test_server.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_server.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackclient.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackclient.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackclient.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackclient.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackrequest.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackrequest.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackrequest.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackrequest.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tox.ini b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tox.ini similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tox.ini rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tox.ini diff --git a/openpype/modules/default_modules/slack/slack_module.py b/openpype/modules/slack/slack_module.py similarity index 100% rename from openpype/modules/default_modules/slack/slack_module.py rename to openpype/modules/slack/slack_module.py From 8b79961e286a18bb8dcb893c64470bee99177e3d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Dec 2021 16:31:34 +0100 Subject: [PATCH 137/307] moved webserver module --- openpype/modules/base.py | 1 + openpype/modules/{default_modules => }/webserver/__init__.py | 0 openpype/modules/{default_modules => }/webserver/base_routes.py | 0 .../{default_modules => }/webserver/host_console_listener.py | 0 openpype/modules/{default_modules => }/webserver/server.py | 0 .../modules/{default_modules => }/webserver/webserver_module.py | 0 6 files changed, 1 insertion(+) rename openpype/modules/{default_modules => }/webserver/__init__.py (100%) rename openpype/modules/{default_modules => }/webserver/base_routes.py (100%) rename openpype/modules/{default_modules => }/webserver/host_console_listener.py (100%) rename openpype/modules/{default_modules => }/webserver/server.py (100%) rename openpype/modules/{default_modules => }/webserver/webserver_module.py (100%) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index eb40315dfa..e300dd0806 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -273,6 +273,7 @@ def _load_modules(): # Import default modules imported from 'openpype.modules' for default_module_name in ( + "webserver", "python_console_interpreter", "slack", "log_viewer", diff --git a/openpype/modules/default_modules/webserver/__init__.py b/openpype/modules/webserver/__init__.py similarity index 100% rename from openpype/modules/default_modules/webserver/__init__.py rename to openpype/modules/webserver/__init__.py diff --git a/openpype/modules/default_modules/webserver/base_routes.py b/openpype/modules/webserver/base_routes.py similarity index 100% rename from openpype/modules/default_modules/webserver/base_routes.py rename to openpype/modules/webserver/base_routes.py diff --git a/openpype/modules/default_modules/webserver/host_console_listener.py b/openpype/modules/webserver/host_console_listener.py similarity index 100% rename from openpype/modules/default_modules/webserver/host_console_listener.py rename to openpype/modules/webserver/host_console_listener.py diff --git a/openpype/modules/default_modules/webserver/server.py b/openpype/modules/webserver/server.py similarity index 100% rename from openpype/modules/default_modules/webserver/server.py rename to openpype/modules/webserver/server.py diff --git a/openpype/modules/default_modules/webserver/webserver_module.py b/openpype/modules/webserver/webserver_module.py similarity index 100% rename from openpype/modules/default_modules/webserver/webserver_module.py rename to openpype/modules/webserver/webserver_module.py From d65431c34b120c29b3a791cf7839092ae34db096 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Dec 2021 17:20:29 +0100 Subject: [PATCH 138/307] OP-2042 - remove pytest deprecation warnings --- openpype/pype_commands.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 519e7c285b..7f6e5f1c0c 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -216,6 +216,7 @@ class PypeCommands: task_name, app_name ) + print("env:: {}".format(env)) os.environ.update(env) os.environ["OPENPYPE_PUBLISH_DATA"] = batch_dir @@ -364,7 +365,10 @@ class PypeCommands: if pyargs: pyargs_str = "--pyargs {}".format(pyargs) - cmd = "pytest {} {} {}".format(folder, mark_str, pyargs_str) + depr_str = "--disable-pytest-warnings" + + cmd = "pytest {} {} {} {}".format(depr_str, folder, + mark_str, pyargs_str) print("Running {}".format(cmd)) subprocess.run(cmd) From 9dbae0e52e43b569051c28e4ef74179548f68065 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Dec 2021 17:50:18 +0100 Subject: [PATCH 139/307] OP-2042 - added NUKE_PATH to settings It was overwriting existing value of NUKE_PATH before --- openpype/settings/defaults/system_settings/applications.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 8c119658be..4e50201036 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -139,7 +139,7 @@ "icon": "{}/app_icons/nuke.png", "host_name": "nuke", "environment": { - "NUKE_PATH": "{OPENPYPE_STUDIO_PLUGINS}/nuke" + "NUKE_PATH": ["{NUKE_PATH}", "{OPENPYPE_STUDIO_PLUGINS}/nuke"] }, "variants": { "13-0": { @@ -245,7 +245,7 @@ "icon": "{}/app_icons/nuke.png", "host_name": "nuke", "environment": { - "NUKE_PATH": "{OPENPYPE_STUDIO_PLUGINS}/nuke" + "NUKE_PATH": ["{NUKE_PATH}", "{OPENPYPE_STUDIO_PLUGINS}/nuke"] }, "variants": { "13-0": { From 591e81a4148396dd5e6e71a0f55133f7c6b13a1d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Dec 2021 17:51:45 +0100 Subject: [PATCH 140/307] OP-2042 - comment out examples --- tests/lib/db_handler.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/lib/db_handler.py b/tests/lib/db_handler.py index 88cde4d05f..0aa4c69ca6 100644 --- a/tests/lib/db_handler.py +++ b/tests/lib/db_handler.py @@ -220,15 +220,16 @@ class DBHandler: return query -handler = DBHandler(uri="mongodb://localhost:27017") -# -backup_dir = "c:\\projects\\test_nuke_publish\\input\\dumps" +# Examples +# handler = DBHandler(uri="mongodb://localhost:27017") # # -handler.backup_to_dump("avalon", backup_dir, True, collection="test_project") -#handler.setup_from_dump("test_db", backup_dir, True, db_name_out="avalon", collection="test_project") -# # handler.setup_from_sql_file("test_db", "c:\\projects\\sql\\item.sql", -# # collection="test_project", -# # drop=False, mode="upsert") -# handler.setup_from_sql("test_db", "c:\\projects\\sql", +# backup_dir = "c:\\projects\\test_nuke_publish\\input\\dumps" +# # # +# handler.backup_to_dump("avalon", backup_dir, True, collection="test_project") +# handler.setup_from_dump("test_db", backup_dir, True, db_name_out="avalon", collection="test_project") +# handler.setup_from_sql_file("test_db", "c:\\projects\\sql\\item.sql", # collection="test_project", # drop=False, mode="upsert") +# handler.setup_from_sql("test_db", "c:\\projects\\sql", +# collection="test_project", +# drop=False, mode="upsert") From fe86bbde299ffc9dd19b3bc9225cfb289c21ee3c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Dec 2021 18:12:54 +0100 Subject: [PATCH 141/307] OP-2042 - working example of test publish in Nuke --- .../hosts/nuke/test_publish_in_nuke.py | 42 ++++++++----------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/tests/integration/hosts/nuke/test_publish_in_nuke.py b/tests/integration/hosts/nuke/test_publish_in_nuke.py index abadb0fb92..574ad8de00 100644 --- a/tests/integration/hosts/nuke/test_publish_in_nuke.py +++ b/tests/integration/hosts/nuke/test_publish_in_nuke.py @@ -14,17 +14,20 @@ class TestPublishInNuke(PublishTest): Uses generic TestCase to prepare fixtures for test data, testing DBs, env vars. - Opens Maya, run publish on prepared workile. + Opens Nuke, run publish on prepared workile. Then checks content of DB (if subset, version, representations were created. Checks tmp folder if all expected files were published. + How to run: + {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/nuke # noqa: E501 + """ - PERSIST = True + PERSIST = True # True - keep test_db, test_openpype, outputted test files TEST_FILES = [ - ("1635L4gww9nEkP-1EclfWXNdeDuRjDhey", "test_Nuke_publish.zip", "") + ("1SUurHj2aiQ21ZIMJfGVBI2KjR8kIjBGI", "test_Nuke_publish.zip", "") ] APP = "nuke" @@ -40,7 +43,6 @@ class TestPublishInNuke(PublishTest): Maya expects workfile in proper folder, so copy is done first. """ - print("last_workfile_path") log.info("log last_workfile_path") src_path = os.path.join( download_test_data, @@ -62,20 +64,14 @@ class TestPublishInNuke(PublishTest): @pytest.fixture(scope="module") def startup_scripts(self, monkeypatch_session, download_test_data): """Points Nuke to userSetup file from input data""" - print("startup_scripts") - log.info("log startup_scripts") startup_path = os.path.join(download_test_data, "input", "startup") - startup_path = "C:\\projects\\test_nuke_publish\\input\\startup" - original_pythonpath = os.environ.get("NUKE_PATH") + original_nuke_path = os.environ.get("NUKE_PATH", "") monkeypatch_session.setenv("NUKE_PATH", - "{}{}{}".format(original_pythonpath, + "{}{}{}".format(startup_path, os.pathsep, - startup_path)) - print("NUKE_PATH:: {}{}{}".format(startup_path, - os.pathsep, - original_pythonpath)) + original_nuke_path)) def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" @@ -88,25 +84,21 @@ class TestPublishInNuke(PublishTest): "Only versions with 1 expected" assert 1 == dbcon.count_documents({"type": "subset", - "name": "modelMain"}), \ - "modelMain subset must be present" + "name": "renderCompositingInNukeMain"} # noqa: E501 + ), \ + "renderCompositingInNukeMain subset must be present" assert 1 == dbcon.count_documents({"type": "subset", "name": "workfileTest_task"}), \ "workfileTest_task subset must be present" - assert 11 == dbcon.count_documents({"type": "representation"}), \ + assert 10 == dbcon.count_documents({"type": "representation"}), \ "Not expected no of representations" - assert 2 == dbcon.count_documents({"type": "representation", - "context.subset": "modelMain", - "context.ext": "abc"}), \ - "Not expected no of representations with ext 'abc'" - - assert 2 == dbcon.count_documents({"type": "representation", - "context.subset": "modelMain", - "context.ext": "ma"}), \ - "Not expected no of representations with ext 'abc'" + assert 1 == dbcon.count_documents({"type": "representation", + "context.subset": "renderCompositingInNukeMain", # noqa: E501 + "context.ext": "exr"}), \ + "Not expected no of representations with ext 'exr'" if __name__ == "__main__": From f80a41bd89c392642ce66173dc4f0d608a192db9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Dec 2021 18:28:28 +0100 Subject: [PATCH 142/307] add support of mxf format to be able recognize opatom mxf --- openpype/scripts/otio_burnin.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index 68f4728bc7..15a62ef38e 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -161,6 +161,23 @@ def _dnxhd_codec_args(stream_data, source_ffmpeg_cmd): return output +def _mxf_format_args(ffprobe_data, source_ffmpeg_cmd): + input_format = ffprobe_data["format"] + format_tags = input_format.get("tags") or {} + product_name = format_tags.get("product_name") or "" + output = [] + if "opatom" in product_name.lower(): + output.extend(["-f", "mxf_opatom"]) + return output + + +def get_format_args(ffprobe_data, source_ffmpeg_cmd): + input_format = ffprobe_data.get("format") or {} + if input_format.get("format_name") == "mxf": + return _mxf_format_args(ffprobe_data, source_ffmpeg_cmd) + return [] + + def get_codec_args(ffprobe_data, source_ffmpeg_cmd): stream_data = ffprobe_data["streams"][0] codec_name = stream_data.get("codec_name") @@ -595,9 +612,9 @@ def burnins_from_data( if source_timecode is None: source_timecode = stream.get("tags", {}).get("timecode") + # Use "format" key from ffprobe data + # - this is used e.g. in mxf extension if source_timecode is None: - # Use "format" key from ffprobe data - # - this is used e.g. in mxf extension input_format = burnin.ffprobe_data.get("format") or {} source_timecode = input_format.get("timecode") if source_timecode is None: @@ -692,6 +709,9 @@ def burnins_from_data( ffmpeg_args.append("-g 1") else: + ffmpeg_args.extend( + get_format_args(burnin.ffprobe_data, source_ffmpeg_cmd) + ) ffmpeg_args.extend( get_codec_args(burnin.ffprobe_data, source_ffmpeg_cmd) ) From e35920f9bed75a55770b219d0374d0e19b07ee7b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Dec 2021 18:40:39 +0100 Subject: [PATCH 143/307] OP-2042 - replaced testing zip files Zip files now stored on OP shared GDrive PS implementation is not working, fixed in OP-2019 --- tests/integration/hosts/maya/test_publish_in_maya.py | 9 +++++++-- .../hosts/photoshop/test_publish_in_photoshop.py | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/integration/hosts/maya/test_publish_in_maya.py b/tests/integration/hosts/maya/test_publish_in_maya.py index 1babf30029..566829b2e2 100644 --- a/tests/integration/hosts/maya/test_publish_in_maya.py +++ b/tests/integration/hosts/maya/test_publish_in_maya.py @@ -13,17 +13,22 @@ class TestPublishInMaya(PublishTest): Uses generic TestCase to prepare fixtures for test data, testing DBs, env vars. - Opens Maya, run publish on prepared workile. + Always pulls and uses test data from GDrive! + + Opens Maya, runs publish on prepared workile. Then checks content of DB (if subset, version, representations were created. Checks tmp folder if all expected files were published. + How to run: + {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/maya # noqa: E501 + """ PERSIST = True TEST_FILES = [ - ("1pOwjA_VVBc6ooTZyFxtAwLS2KZHaBlkY", "test_maya_publish.zip", "") + ("1BTSIIULJTuDc8VvXseuiJV_fL6-Bu7FP", "test_maya_publish.zip", "") ] APP = "maya" diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py index b634d422f3..3ef40f3041 100644 --- a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py @@ -11,17 +11,22 @@ class TestPublishInPhotoshop(PublishTest): Uses generic TestCase to prepare fixtures for test data, testing DBs, env vars. - Opens Maya, run publish on prepared workile. + Always pulls and uses test data from GDrive! + + Opens Photoshop, runs publish on prepared workile. Then checks content of DB (if subset, version, representations were created. Checks tmp folder if all expected files were published. + How to run: + {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/photoshop # noqa: E501 + """ PERSIST = True TEST_FILES = [ - ("1Bciy2pCwMKl1UIpxuPnlX_LHMo_Xkq0K", "test_photoshop_publish.zip", "") + ("1zD2v5cBgkyOm_xIgKz3WKn8aFB_j8qC-", "test_photoshop_publish.zip", "") ] APP = "photoshop" From 58dbe19ac6a4a8759ddca74d1a7725c533e98843 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Dec 2021 18:40:53 +0100 Subject: [PATCH 144/307] fix loading of settings --- openpype/modules/default_modules/job_queue/module.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/job_queue/module.py b/openpype/modules/default_modules/job_queue/module.py index 719d7c8f38..f1d7251e85 100644 --- a/openpype/modules/default_modules/job_queue/module.py +++ b/openpype/modules/default_modules/job_queue/module.py @@ -50,11 +50,12 @@ class JobQueueModule(OpenPypeModule): name = "job_queue" def initialize(self, modules_settings): - server_url = modules_settings.get("server_url") or "" + module_settings = modules_settings.get(self.name) or {} + server_url = module_settings.get("server_url") or "" self._server_url = self.url_conversion(server_url) jobs_root_mapping = self._roots_mapping_conversion( - modules_settings.get("jobs_root") + module_settings.get("jobs_root") ) self._jobs_root_mapping = jobs_root_mapping From fcec9a820d7c41d97c3d06c7925a4774558ded95 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 2 Dec 2021 19:02:04 +0100 Subject: [PATCH 145/307] fixes python3 compatibility --- openpype/hosts/hiero/api/__init__.py | 2 ++ openpype/hosts/hiero/api/lib.py | 34 +++++++++++++------ openpype/hosts/hiero/api/otio/hiero_export.py | 2 -- .../plugins/publish/precollect_instances.py | 12 +++---- .../publish/collect_otio_subset_resources.py | 3 +- 5 files changed, 31 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/hiero/api/__init__.py b/openpype/hosts/hiero/api/__init__.py index 8d0105ae5f..f3c32b268c 100644 --- a/openpype/hosts/hiero/api/__init__.py +++ b/openpype/hosts/hiero/api/__init__.py @@ -23,6 +23,7 @@ from .pipeline import ( from .lib import ( pype_tag_name, + flatten, get_track_items, get_current_project, get_current_sequence, @@ -75,6 +76,7 @@ __all__ = [ # Lib functions "pype_tag_name", + "flatten", "get_track_items", "get_current_project", "get_current_sequence", diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 7c90c0d618..f1985a2e12 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -37,14 +37,14 @@ self.default_bin_name = "openpypeBin" AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") -def get_current_project(remove_untitled=False): - def flatten(l): - for i in l: - if isinstance(i, (list, tuple)): - yield from flatten(i) - else: - yield i +def flatten(input_list): + for item in input_list: + if isinstance(item, (list, tuple)): + yield from flatten(item) + else: + yield item +def get_current_project(remove_untitled=False): projects = flatten(hiero.core.projects()) if not remove_untitled: return next(iter(projects)) @@ -256,7 +256,7 @@ def set_track_item_pype_tag(track_item, data=None): Returns: hiero.core.Tag """ - data = data or dict() + data = data or {} # basic Tag's attribute tag_data = { @@ -290,7 +290,7 @@ def get_track_item_pype_data(track_item): Returns: dict: data found on pype tag """ - data = dict() + data = {} # get pype data tag from track item tag = get_track_item_pype_tag(track_item) @@ -305,8 +305,20 @@ def get_track_item_pype_data(track_item): try: # capture exceptions which are related to strings only - value = ast.literal_eval(v) - except (ValueError, SyntaxError): + if re.match(r"^[\d]+$", v): + value = int(v) + elif re.match(r"^True$", v): + value = True + elif re.match(r"^False$", v): + value = False + elif re.match(r"^None$", v): + value = None + elif re.match(r"^[\w\d_]+$", v): + value = v + else: + value = ast.literal_eval(v) + except (ValueError, SyntaxError) as msg: + log.warning(msg) value = v data.update({key: value}) diff --git a/openpype/hosts/hiero/api/otio/hiero_export.py b/openpype/hosts/hiero/api/otio/hiero_export.py index 5067c05515..50a00a1624 100644 --- a/openpype/hosts/hiero/api/otio/hiero_export.py +++ b/openpype/hosts/hiero/api/otio/hiero_export.py @@ -81,13 +81,11 @@ def create_time_effects(otio_clip, track_item): otio_effect = otio.schema.LinearTimeWarp() otio_effect.name = "Speed" otio_effect.time_scalar = speed - otio_effect.metadata = {} # freeze frame effect if speed == 0.: otio_effect = otio.schema.FreezeFrame() otio_effect.name = "FreezeFrame" - otio_effect.metadata = {} if otio_effect: # add otio effect to clip effects diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index b13603897b..bf3a779ab1 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -4,8 +4,6 @@ from openpype.hosts.hiero import api as phiero from openpype.hosts.hiero.api.otio import hiero_export import hiero -from compiler.ast import flatten - # # developer reload modules from pprint import pformat @@ -339,10 +337,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin): continue track_index = track.trackIndex() - _sub_track_items = flatten(track.subTrackItems()) + _sub_track_items = phiero.flatten(track.subTrackItems()) # continue only if any subtrack items are collected - if len(_sub_track_items) < 1: + if not list(_sub_track_items): continue enabled_sti = [] @@ -357,7 +355,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): enabled_sti.append(_sti) # continue only if any subtrack items are collected - if len(enabled_sti) < 1: + if not enabled_sti: continue # add collection of subtrackitems to dict @@ -371,7 +369,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): Returns list of Clip's hiero.core.Annotation """ annotations = [] - subTrackItems = flatten(clip.subTrackItems()) + subTrackItems = phiero.flatten(clip.subTrackItems()) annotations += [item for item in subTrackItems if isinstance( item, hiero.core.Annotation)] return annotations @@ -382,7 +380,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): Returns list of Clip's hiero.core.SubTrackItem """ subtracks = [] - subTrackItems = flatten(clip.parent().subTrackItems()) + subTrackItems = phiero.flatten(clip.parent().subTrackItems()) for item in subTrackItems: if "TimeWarp" in item.name(): continue diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index dd670ff850..571d0d56a4 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -171,8 +171,7 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): instance.data["representations"].append(repre) self.log.debug(">>>>>>>> {}".format(repre)) - import pprint - self.log.debug(pprint.pformat(instance.data)) + self.log.debug(instance.data) def _create_representation(self, start, end, **kwargs): """ From d4e5ab90cf3bc81a8ffe94b5b8736d7403adb2c3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Dec 2021 19:02:35 +0100 Subject: [PATCH 146/307] OP-2042 - added better documentation how to run it --- tests/integration/hosts/maya/test_publish_in_maya.py | 1 + tests/integration/hosts/nuke/test_publish_in_nuke.py | 1 + tests/integration/hosts/photoshop/test_publish_in_photoshop.py | 1 + 3 files changed, 3 insertions(+) diff --git a/tests/integration/hosts/maya/test_publish_in_maya.py b/tests/integration/hosts/maya/test_publish_in_maya.py index 566829b2e2..b53b26f66d 100644 --- a/tests/integration/hosts/maya/test_publish_in_maya.py +++ b/tests/integration/hosts/maya/test_publish_in_maya.py @@ -22,6 +22,7 @@ class TestPublishInMaya(PublishTest): Checks tmp folder if all expected files were published. How to run: + (in cmd with activated {OPENPYPE_ROOT}/.venv) {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/maya # noqa: E501 """ diff --git a/tests/integration/hosts/nuke/test_publish_in_nuke.py b/tests/integration/hosts/nuke/test_publish_in_nuke.py index 574ad8de00..6dc880d757 100644 --- a/tests/integration/hosts/nuke/test_publish_in_nuke.py +++ b/tests/integration/hosts/nuke/test_publish_in_nuke.py @@ -21,6 +21,7 @@ class TestPublishInNuke(PublishTest): Checks tmp folder if all expected files were published. How to run: + (in cmd with activated {OPENPYPE_ROOT}/.venv) {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/nuke # noqa: E501 """ diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py index 3ef40f3041..43e03b2cc3 100644 --- a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py @@ -20,6 +20,7 @@ class TestPublishInPhotoshop(PublishTest): Checks tmp folder if all expected files were published. How to run: + (in cmd with activated {OPENPYPE_ROOT}/.venv) {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/photoshop # noqa: E501 """ From 5d8550399535492c6bc52ba58f5c2aff9200f066 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Dec 2021 19:03:43 +0100 Subject: [PATCH 147/307] OP-2042 - removed forgotten app for debugging --- openpype/hooks/pre_foundry_apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hooks/pre_foundry_apps.py b/openpype/hooks/pre_foundry_apps.py index 70554cbedb..85f68c6b60 100644 --- a/openpype/hooks/pre_foundry_apps.py +++ b/openpype/hooks/pre_foundry_apps.py @@ -13,7 +13,7 @@ class LaunchFoundryAppsWindows(PreLaunchHook): # Should be as last hook because must change launch arguments to string order = 1000 - app_groups = ["nuke", "nukex", "hiero", "nukestudio", "aftereffects"] + app_groups = ["nuke", "nukex", "hiero", "nukestudio"] platforms = ["windows"] def execute(self): From 1762f4635b5fe520825343b4b80ff7fec02b9c42 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Dec 2021 19:04:44 +0100 Subject: [PATCH 148/307] OP-2042 - fixed wrong value Belongs to merged PR for AE testing --- openpype/lib/remote_publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index 4b4d233f1e..6c594b50e8 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -27,7 +27,7 @@ def headless_publish(log, close_plugin_name=None, is_test=False): publish_and_log(dbcon, _id, log, close_plugin_name) else: - publish(log, 'CloseAE') + publish(log, close_plugin_name) def get_webpublish_conn(): From 07fdcc6f4de08087a4f22343bb61a83cc65b0df8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Dec 2021 19:07:46 +0100 Subject: [PATCH 149/307] OP-2019 - fixed wrong value --- openpype/lib/remote_publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index 3483898af7..b91c5f01fc 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -27,7 +27,7 @@ def headless_publish(log, close_plugin_name=None, is_test=False): publish_and_log(dbcon, _id, log, close_plugin_name) else: - publish(log, 'CloseAE') + publish(log, close_plugin_name) def get_webpublish_conn(): From 9a5f90620ed57bffe00888d5319d928064636b64 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Dec 2021 19:27:50 +0100 Subject: [PATCH 150/307] Commit current develop avalon-core --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index e37f4f92ed..1d94a75ddf 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit e37f4f92ed25f89c870fdcb7f9538da7d0d7de90 +Subproject commit 1d94a75ddf37a354955a9e7f4bb8695187f3e0ed From ceeaf28dd7e461cd2271796092651b014f6c6f8f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Dec 2021 10:14:14 +0100 Subject: [PATCH 151/307] OP-2042 - clean up files from different PR --- .../aftereffects/plugins/publish/closeAE.py | 29 ------ .../plugins/publish/extract_local_render.py | 3 +- openpype/lib/remote_publish.py | 46 --------- .../system_settings/applications.json | 17 ---- .../test_publish_in_aftereffects.py | 96 ------------------- 5 files changed, 2 insertions(+), 189 deletions(-) delete mode 100644 openpype/hosts/aftereffects/plugins/publish/closeAE.py delete mode 100644 tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py diff --git a/openpype/hosts/aftereffects/plugins/publish/closeAE.py b/openpype/hosts/aftereffects/plugins/publish/closeAE.py deleted file mode 100644 index e6e9623474..0000000000 --- a/openpype/hosts/aftereffects/plugins/publish/closeAE.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -"""Close AE after publish. For Webpublishing only.""" -import os - -import pyblish.api - -from avalon import aftereffects - - -class CloseAE(pyblish.api.ContextPlugin): - """Close AE after publish. For Webpublishing only. - """ - - order = pyblish.api.IntegratorOrder + 14 - label = "Close AE" - optional = True - active = True - - hosts = ["aftereffects"] - targets = ["remotepublish"] - - def process(self, context): - self.log.info("CloseAE") - - stub = aftereffects.stub() - self.log.info("Shutting down AE") - stub.save() - stub.close() - self.log.info("AE closed") diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index b36ab24bde..37337e7fee 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -19,9 +19,10 @@ class ExtractLocalRender(openpype.api.Extractor): staging_dir = instance.data["stagingDir"] self.log.info("staging_dir::{}".format(staging_dir)) + stub.render(staging_dir) + # pull file name from Render Queue Output module render_q = stub.get_render_info() - stub.render(staging_dir) if not render_q: raise ValueError("No file extension set in Render Queue") _, ext = os.path.splitext(os.path.basename(render_q.file_name)) diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index 6c594b50e8..d7db4d1ab9 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -11,25 +11,6 @@ from openpype.lib.mongo import OpenPypeMongoConnection from openpype.lib.plugin_tools import parse_json -def headless_publish(log, close_plugin_name=None, is_test=False): - """Runs publish in a opened host with a context and closes Python process. - - Host is being closed via ClosePS pyblish plugin which triggers 'exit' - method in ConsoleTrayApp. - """ - if not is_test: - dbcon = get_webpublish_conn() - _id = os.environ.get("BATCH_LOG_ID") - if not _id: - log.warning("Unable to store log records, " - "batch will be unfinished!") - return - - publish_and_log(dbcon, _id, log, close_plugin_name) - else: - publish(log, close_plugin_name) - - def get_webpublish_conn(): """Get connection to OP 'webpublishes' collection.""" mongo_client = OpenPypeMongoConnection.get_mongo_client() @@ -56,33 +37,6 @@ def start_webpublish_log(dbcon, batch_id, user): }).inserted_id -def publish(log, close_plugin_name=None): - """Loops through all plugins, logs to console. Used for tests. - - Args: - log (OpenPypeLogger) - close_plugin_name (str): name of plugin with responsibility to - close host app - """ - # Error exit as soon as any error occurs. - error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}" - - close_plugin = _get_close_plugin(close_plugin_name, log) - - for result in pyblish.util.publish_iter(): - for record in result["records"]: - log.info("{}: {}".format( - result["plugin"].label, record.msg)) - - if result["error"]: - log.error(error_format.format(**result)) - uninstall() - if close_plugin: # close host app explicitly after error - context = pyblish.api.Context() - close_plugin().process(context) - sys.exit(1) - - def publish_and_log(dbcon, _id, log, close_plugin_name=None): """Loops through all plugins, logs ok and fails into OP DB. diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 4e50201036..c730e02984 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1098,23 +1098,6 @@ "linux": [] }, "environment": {} - }, - "2022": { - "enabled": true, - "variant_label": "2022", - "executables": { - "windows": [ - "C:\\Program Files\\Adobe\\Adobe After Effects 2022\\Support Files\\AfterFX.exe" - ], - "darwin": [], - "linux": [] - }, - "arguments": { - "windows": [], - "darwin": [], - "linux": [] - }, - "environment": {} } } }, diff --git a/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py b/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py deleted file mode 100644 index d4e88dfd4c..0000000000 --- a/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py +++ /dev/null @@ -1,96 +0,0 @@ -import pytest -import os -import shutil - -from tests.lib.testing_classes import PublishTest - - -class TestPublishInAfterEffects(PublishTest): - """Basic test case for publishing in AfterEffects - - Uses generic TestCase to prepare fixtures for test data, testing DBs, - env vars. - - Opens AfterEffects, run publish on prepared workile. - - Then checks content of DB (if subset, version, representations were - created. - Checks tmp folder if all expected files were published. - - """ - PERSIST = True - - TEST_FILES = [ - ("1qsrq6OJWVpOeXE2LTWrdbsLqEVu155Uf", - "test_aftereffects_publish.zip", - "") - ] - - APP = "aftereffects" - APP_VARIANT = "2021" - - APP_NAME = "{}/{}".format(APP, APP_VARIANT) - - TIMEOUT = 120 # publish timeout - - @pytest.fixture(scope="module") - def last_workfile_path(self, download_test_data): - """Get last_workfile_path from source data. - - Maya expects workfile in proper folder, so copy is done first. - """ - src_path = os.path.join(download_test_data, - "input", - "workfile", - "test_project_test_asset_TestTask_v001.aep") - dest_folder = os.path.join(download_test_data, - self.PROJECT, - self.ASSET, - "work", - self.TASK) - os.makedirs(dest_folder) - dest_path = os.path.join(dest_folder, - "test_project_test_asset_TestTask_v001.aep") - shutil.copy(src_path, dest_path) - - yield dest_path - - @pytest.fixture(scope="module") - def startup_scripts(self, monkeypatch_session, download_test_data): - """Points AfterEffects to userSetup file from input data""" - pass - - def test_db_asserts(self, dbcon, publish_finished): - """Host and input data dependent expected results in DB.""" - print("test_db_asserts") - assert 5 == dbcon.count_documents({"type": "version"}), \ - "Not expected no of versions" - - assert 0 == dbcon.count_documents({"type": "version", - "name": {"$ne": 1}}), \ - "Only versions with 1 expected" - - assert 1 == dbcon.count_documents({"type": "subset", - "name": "modelMain"}), \ - "modelMain subset must be present" - - assert 1 == dbcon.count_documents({"type": "subset", - "name": "workfileTest_task"}), \ - "workfileTest_task subset must be present" - - assert 11 == dbcon.count_documents({"type": "representation"}), \ - "Not expected no of representations" - - assert 2 == dbcon.count_documents({"type": "representation", - "context.subset": "modelMain", - "context.ext": "abc"}), \ - "Not expected no of representations with ext 'abc'" - - assert 2 == dbcon.count_documents({"type": "representation", - "context.subset": "modelMain", - "context.ext": "ma"}), \ - "Not expected no of representations with ext 'abc'" - - -if __name__ == "__main__": - test_case = TestPublishInAfterEffects() From 486966efbc7daddefb6e264ec05a5718f8fce395 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Dec 2021 10:16:05 +0100 Subject: [PATCH 152/307] Reverting change to avalon-core --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index 1d94a75ddf..9499f6517a 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 1d94a75ddf37a354955a9e7f4bb8695187f3e0ed +Subproject commit 9499f6517a1ff2d3bf94c5d34c0aece146734760 From f9a1445a748f15a6592ad06ec34b748af1fddad0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Dec 2021 10:28:41 +0100 Subject: [PATCH 153/307] OP-2019 - revert to develop version --- .../integration/hosts/photoshop/test_publish_in_photoshop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py index b634d422f3..396468a966 100644 --- a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py @@ -25,7 +25,7 @@ class TestPublishInPhotoshop(PublishTest): ] APP = "photoshop" - APP_VARIANT = "2021" + APP_VARIANT = "2020" APP_NAME = "{}/{}".format(APP, APP_VARIANT) @@ -56,7 +56,7 @@ class TestPublishInPhotoshop(PublishTest): @pytest.fixture(scope="module") def startup_scripts(self, monkeypatch_session, download_test_data): """Points Maya to userSetup file from input data""" - pass + os.environ["IS_HEADLESS"] = "true" def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" From 477d177bd9f9939f3c74c63ac47be5b68d52d372 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Dec 2021 11:42:22 +0100 Subject: [PATCH 154/307] OP-2019 - fixes for PS test class --- .../photoshop/test_publish_in_photoshop.py | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py index 396468a966..4754f60486 100644 --- a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py @@ -11,7 +11,12 @@ class TestPublishInPhotoshop(PublishTest): Uses generic TestCase to prepare fixtures for test data, testing DBs, env vars. - Opens Maya, run publish on prepared workile. + Opens Photoshop, run publish on prepared workile. + + Test zip file sets 3 required env vars: + - HEADLESS_PUBLISH - this triggers publish immediately app is open + - IS_TEST - this differentiate between regular webpublish + - PYBLISH_TARGETS Then checks content of DB (if subset, version, representations were created. @@ -21,11 +26,11 @@ class TestPublishInPhotoshop(PublishTest): PERSIST = True TEST_FILES = [ - ("1Bciy2pCwMKl1UIpxuPnlX_LHMo_Xkq0K", "test_photoshop_publish.zip", "") + ("1zD2v5cBgkyOm_xIgKz3WKn8aFB_j8qC-", "test_photoshop_publish.zip", "") ] APP = "photoshop" - APP_VARIANT = "2020" + APP_VARIANT = "2021" APP_NAME = "{}/{}".format(APP, APP_VARIANT) @@ -56,12 +61,12 @@ class TestPublishInPhotoshop(PublishTest): @pytest.fixture(scope="module") def startup_scripts(self, monkeypatch_session, download_test_data): """Points Maya to userSetup file from input data""" - os.environ["IS_HEADLESS"] = "true" + pass def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" print("test_db_asserts") - assert 5 == dbcon.count_documents({"type": "version"}), \ + assert 3 == dbcon.count_documents({"type": "version"}), \ "Not expected no of versions" assert 0 == dbcon.count_documents({"type": "version", @@ -69,25 +74,21 @@ class TestPublishInPhotoshop(PublishTest): "Only versions with 1 expected" assert 1 == dbcon.count_documents({"type": "subset", - "name": "modelMain"}), \ + "name": "imageMainBackgroundcopy"} + ), \ "modelMain subset must be present" assert 1 == dbcon.count_documents({"type": "subset", - "name": "workfileTest_task"}), \ + "name": "workfileTesttask"}), \ "workfileTest_task subset must be present" - assert 11 == dbcon.count_documents({"type": "representation"}), \ + assert 6 == dbcon.count_documents({"type": "representation"}), \ "Not expected no of representations" - assert 2 == dbcon.count_documents({"type": "representation", - "context.subset": "modelMain", - "context.ext": "abc"}), \ - "Not expected no of representations with ext 'abc'" - - assert 2 == dbcon.count_documents({"type": "representation", - "context.subset": "modelMain", - "context.ext": "ma"}), \ - "Not expected no of representations with ext 'abc'" + assert 1 == dbcon.count_documents({"type": "representation", + "context.subset": "imageMainBackgroundcopy", # noqa: E501 + "context.ext": "png"}), \ + "Not expected no of representations with ext 'png'" if __name__ == "__main__": From f3eb3d8c60cd6cfe8d0bcff95d088d6a4836df3a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Dec 2021 11:49:15 +0100 Subject: [PATCH 155/307] Current avalon-core develop After Adobe fixes --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index 9499f6517a..1d94a75ddf 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 9499f6517a1ff2d3bf94c5d34c0aece146734760 +Subproject commit 1d94a75ddf37a354955a9e7f4bb8695187f3e0ed From 24f557f23355dd605b0a060e6d6af69deef189dc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 13:28:19 +0100 Subject: [PATCH 156/307] add webpublisher to hosts enum --- openpype/settings/entities/enum_entity.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index ab3cebbd42..fb6099e82a 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -154,7 +154,8 @@ class HostsEnumEntity(BaseEnumEntity): "resolve", "tvpaint", "unreal", - "standalonepublisher" + "standalonepublisher", + "webpublisher" ] def _item_initialization(self): From d5b2127bcc04bbc5abab0de8c670e600c024cae2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 13:52:56 +0100 Subject: [PATCH 157/307] moved avalon apps module --- openpype/modules/{default_modules => }/avalon_apps/__init__.py | 0 openpype/modules/{default_modules => }/avalon_apps/avalon_app.py | 0 openpype/modules/{default_modules => }/avalon_apps/rest_api.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename openpype/modules/{default_modules => }/avalon_apps/__init__.py (100%) rename openpype/modules/{default_modules => }/avalon_apps/avalon_app.py (100%) rename openpype/modules/{default_modules => }/avalon_apps/rest_api.py (100%) diff --git a/openpype/modules/default_modules/avalon_apps/__init__.py b/openpype/modules/avalon_apps/__init__.py similarity index 100% rename from openpype/modules/default_modules/avalon_apps/__init__.py rename to openpype/modules/avalon_apps/__init__.py diff --git a/openpype/modules/default_modules/avalon_apps/avalon_app.py b/openpype/modules/avalon_apps/avalon_app.py similarity index 100% rename from openpype/modules/default_modules/avalon_apps/avalon_app.py rename to openpype/modules/avalon_apps/avalon_app.py diff --git a/openpype/modules/default_modules/avalon_apps/rest_api.py b/openpype/modules/avalon_apps/rest_api.py similarity index 100% rename from openpype/modules/default_modules/avalon_apps/rest_api.py rename to openpype/modules/avalon_apps/rest_api.py From 343159d5af8b1a77e8c7e95fe685c78a39e0ed88 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 14:12:32 +0100 Subject: [PATCH 158/307] define constant with default modules --- openpype/modules/base.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index e300dd0806..29c1c6fd5a 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -29,6 +29,21 @@ from openpype.settings.lib import ( from openpype.lib import PypeLogger +DEFAULT_OPENPYPE_MODULES = ( + "avalon_apps", + "clockify", + "log_viewer", + "muster", + "python_console_interpreter", + "slack", + "webserver", + "launcher_action", + "project_manager_action", + "settings_action", + "standalonepublish_action", +) + + # Inherit from `object` for Python 2 hosts class _ModuleClass(object): """Fake module class for storing OpenPype modules. @@ -272,18 +287,7 @@ def _load_modules(): log = PypeLogger.get_logger("ModulesLoader") # Import default modules imported from 'openpype.modules' - for default_module_name in ( - "webserver", - "python_console_interpreter", - "slack", - "log_viewer", - "muster", - "clockify", - "settings_action", - "launcher_action", - "project_manager_action", - "standalonepublish_action", - ): + for default_module_name in DEFAULT_OPENPYPE_MODULES: try: default_module = __import__( "openpype.modules.{}".format(default_module_name), From 10d959dd03c176c25fed674b09922eaf44302732 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 14:13:16 +0100 Subject: [PATCH 159/307] modified and fixed import of default modules into 'openpype_modules' --- openpype/modules/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 29c1c6fd5a..a1df3cfd14 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -289,10 +289,10 @@ def _load_modules(): # Import default modules imported from 'openpype.modules' for default_module_name in DEFAULT_OPENPYPE_MODULES: try: - default_module = __import__( - "openpype.modules.{}".format(default_module_name), - fromlist=("", ) - ) + import_str = "openpype.modules.{}".format(default_module_name) + new_import_str = "{}.{}".format(modules_key, default_module_name) + default_module = __import__(import_str, fromlist=("", )) + sys.modules[new_import_str] = default_module setattr(openpype_modules, default_module_name, default_module) except Exception: From 2e753e1ca2aac3cc0e43e82cbe0bc31a032a1f43 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Dec 2021 14:39:49 +0100 Subject: [PATCH 160/307] OP-2019 - fixes for db_asserts --- .../test_publish_in_aftereffects.py | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py b/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py index d4e88dfd4c..3d1fa8f804 100644 --- a/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py +++ b/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py @@ -13,6 +13,11 @@ class TestPublishInAfterEffects(PublishTest): Opens AfterEffects, run publish on prepared workile. + Test zip file sets 3 required env vars: + - HEADLESS_PUBLISH - this triggers publish immediately app is open + - IS_TEST - this differentiate between regular webpublish + - PYBLISH_TARGETS + Then checks content of DB (if subset, version, representations were created. Checks tmp folder if all expected files were published. @@ -21,13 +26,13 @@ class TestPublishInAfterEffects(PublishTest): PERSIST = True TEST_FILES = [ - ("1qsrq6OJWVpOeXE2LTWrdbsLqEVu155Uf", + ("1c8261CmHwyMgS-g7S4xL5epAp0jCBmhf", "test_aftereffects_publish.zip", "") ] APP = "aftereffects" - APP_VARIANT = "2021" + APP_VARIANT = "2022" APP_NAME = "{}/{}".format(APP, APP_VARIANT) @@ -63,7 +68,7 @@ class TestPublishInAfterEffects(PublishTest): def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" print("test_db_asserts") - assert 5 == dbcon.count_documents({"type": "version"}), \ + assert 3 == dbcon.count_documents({"type": "version"}), \ "Not expected no of versions" assert 0 == dbcon.count_documents({"type": "version", @@ -71,25 +76,25 @@ class TestPublishInAfterEffects(PublishTest): "Only versions with 1 expected" assert 1 == dbcon.count_documents({"type": "subset", - "name": "modelMain"}), \ + "name": "imageMainBackgroundcopy" + }), \ "modelMain subset must be present" assert 1 == dbcon.count_documents({"type": "subset", - "name": "workfileTest_task"}), \ - "workfileTest_task subset must be present" + "name": "workfileTesttask"}), \ + "workfileTesttask subset must be present" - assert 11 == dbcon.count_documents({"type": "representation"}), \ + assert 1 == dbcon.count_documents({"type": "subset", + "name": "reviewTesttask"}), \ + "reviewTesttask subset must be present" + + assert 6 == dbcon.count_documents({"type": "representation"}), \ "Not expected no of representations" - assert 2 == dbcon.count_documents({"type": "representation", - "context.subset": "modelMain", - "context.ext": "abc"}), \ - "Not expected no of representations with ext 'abc'" - - assert 2 == dbcon.count_documents({"type": "representation", - "context.subset": "modelMain", - "context.ext": "ma"}), \ - "Not expected no of representations with ext 'abc'" + assert 1 == dbcon.count_documents({"type": "representation", + "context.subset": "imageMainBackgroundcopy", #noqa E501 + "context.ext": "png"}), \ + "Not expected no of representations with ext 'png'" if __name__ == "__main__": From ea30af7147a80b7ea941c7b1d8d18886995732d8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Dec 2021 14:48:42 +0100 Subject: [PATCH 161/307] Main avalon-core --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index 1d94a75ddf..85c656fcf9 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 1d94a75ddf37a354955a9e7f4bb8695187f3e0ed +Subproject commit 85c656fcf9beb06ab92d3d6ce47f6472cf88df54 From 1707a783005ad09d87d049cb0c259540b946c58b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 3 Dec 2021 15:07:57 +0100 Subject: [PATCH 162/307] Nuke: fixing node name based on switched asset name - also improving code --- openpype/hosts/nuke/plugins/load/load_clip.py | 58 +++++++++++-------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 4ad2246e21..9ce72c0519 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -116,16 +116,7 @@ class LoadClip(plugin.NukeLoader): "Representation id `{}` is failing to load".format(repr_id)) return - name_data = { - "asset": repr_cont["asset"], - "subset": repr_cont["subset"], - "representation": context["representation"]["name"], - "ext": repr_cont["representation"], - "id": context["representation"]["_id"], - "class_name": self.__class__.__name__ - } - - read_name = self.node_name_template.format(**name_data) + read_name = self._get_node_name(context["representation"]) # Create the Loader with the filename path set read_node = nuke.createNode( @@ -143,7 +134,7 @@ class LoadClip(plugin.NukeLoader): elif iio_colorspace is not None: read_node["colorspace"].setValue(iio_colorspace) - self.set_range_to_node(read_node, first, last, start_at_workfile) + self._set_range_to_node(read_node, first, last, start_at_workfile) # add additional metadata from the version to imprint Avalon knob add_keys = ["frameStart", "frameEnd", @@ -171,7 +162,7 @@ class LoadClip(plugin.NukeLoader): data=data_imprint) if version_data.get("retime", None): - self.make_retimes(read_node, version_data) + self._make_retimes(read_node, version_data) self.set_as_member(read_node) @@ -230,6 +221,9 @@ class LoadClip(plugin.NukeLoader): "Representation id `{}` is failing to load".format(repr_id)) return + read_name = self._get_node_name(representation) + + read_node["name"].setValue(read_name) read_node["file"].setValue(file) # to avoid multiple undo steps for rest of process @@ -242,7 +236,7 @@ class LoadClip(plugin.NukeLoader): elif iio_colorspace is not None: read_node["colorspace"].setValue(iio_colorspace) - self.set_range_to_node(read_node, first, last, start_at_workfile) + self._set_range_to_node(read_node, first, last, start_at_workfile) updated_dict = { "representation": str(representation["_id"]), @@ -279,21 +273,12 @@ class LoadClip(plugin.NukeLoader): self.log.info("udated to version: {}".format(version.get("name"))) if version_data.get("retime", None): - self.make_retimes(read_node, version_data) + self._make_retimes(read_node, version_data) else: self.clear_members(read_node) self.set_as_member(read_node) - def set_range_to_node(self, read_node, first, last, start_at_workfile): - read_node['origfirst'].setValue(int(first)) - read_node['first'].setValue(int(first)) - read_node['origlast'].setValue(int(last)) - read_node['last'].setValue(int(last)) - - # set start frame depending on workfile or version - self.loader_shift(read_node, start_at_workfile) - def remove(self, container): from avalon.nuke import viewer_update_and_undo_stop @@ -307,7 +292,16 @@ class LoadClip(plugin.NukeLoader): for member in members: nuke.delete(member) - def make_retimes(self, parent_node, version_data): + def _set_range_to_node(self, read_node, first, last, start_at_workfile): + read_node['origfirst'].setValue(int(first)) + read_node['first'].setValue(int(first)) + read_node['origlast'].setValue(int(last)) + read_node['last'].setValue(int(last)) + + # set start frame depending on workfile or version + self._loader_shift(read_node, start_at_workfile) + + def _make_retimes(self, parent_node, version_data): ''' Create all retime and timewarping nodes with coppied animation ''' speed = version_data.get('speed', 1) time_warp_nodes = version_data.get('timewarps', []) @@ -360,7 +354,7 @@ class LoadClip(plugin.NukeLoader): for i, n in enumerate(dependent_nodes): last_node.setInput(i, n) - def loader_shift(self, read_node, workfile_start=False): + def _loader_shift(self, read_node, workfile_start=False): """ Set start frame of read node to a workfile start Args: @@ -371,3 +365,17 @@ class LoadClip(plugin.NukeLoader): if workfile_start: read_node['frame_mode'].setValue("start at") read_node['frame'].setValue(str(self.script_start)) + + def _get_node_name(self, representation): + + repr_cont = representation["context"] + name_data = { + "asset": repr_cont["asset"], + "subset": repr_cont["subset"], + "representation": representation["name"], + "ext": repr_cont["representation"], + "id": representation["_id"], + "class_name": self.__class__.__name__ + } + + return self.node_name_template.format(**name_data) \ No newline at end of file From 68b5078ae147130d3d10e4208a3e3ad7d9a20887 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Dec 2021 15:37:00 +0100 Subject: [PATCH 163/307] OP-2042 - always capture stdout in pytest Previously it was printed only in case of failure --- openpype/pype_commands.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 7f6e5f1c0c..ce1a9718b3 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -365,9 +365,10 @@ class PypeCommands: if pyargs: pyargs_str = "--pyargs {}".format(pyargs) - depr_str = "--disable-pytest-warnings" + # disable warnings and show captured stdout even if success + args_str = "--disable-pytest-warnings -rP" - cmd = "pytest {} {} {} {}".format(depr_str, folder, + cmd = "pytest {} {} {} {}".format(args_str, folder, mark_str, pyargs_str) print("Running {}".format(cmd)) subprocess.run(cmd) From d55d996f9c58030c86de4eb158837ea289d834d4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Dec 2021 15:39:43 +0100 Subject: [PATCH 164/307] OP-2042 - added functionality to reuse existing folder for testdata --- .../hosts/nuke/test_publish_in_nuke.py | 16 +++------ tests/lib/testing_classes.py | 34 +++++++++++-------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/tests/integration/hosts/nuke/test_publish_in_nuke.py b/tests/integration/hosts/nuke/test_publish_in_nuke.py index 6dc880d757..483e9fef98 100644 --- a/tests/integration/hosts/nuke/test_publish_in_nuke.py +++ b/tests/integration/hosts/nuke/test_publish_in_nuke.py @@ -25,7 +25,7 @@ class TestPublishInNuke(PublishTest): {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/nuke # noqa: E501 """ - PERSIST = True # True - keep test_db, test_openpype, outputted test files + PERSIST = False # True - keep test_db, test_openpype, outputted test files TEST_FILES = [ ("1SUurHj2aiQ21ZIMJfGVBI2KjR8kIjBGI", "test_Nuke_publish.zip", "") @@ -38,11 +38,12 @@ class TestPublishInNuke(PublishTest): TIMEOUT = 120 # publish timeout + TEST_DATA_FOLDER = None # provide existing folder with test data + @pytest.fixture(scope="module") def last_workfile_path(self, download_test_data): """Get last_workfile_path from source data. - Maya expects workfile in proper folder, so copy is done first. """ log.info("log last_workfile_path") src_path = os.path.join( @@ -50,17 +51,8 @@ class TestPublishInNuke(PublishTest): "input", "workfile", "test_project_test_asset_CompositingInNuke_v001.nk") - dest_folder = os.path.join(download_test_data, - self.PROJECT, - self.ASSET, - "work", - self.TASK) - os.makedirs(dest_folder) - dest_path = os.path.join( - dest_folder, "test_project_test_asset_CompositingInNuke_v001.nk") - shutil.copy(src_path, dest_path) - yield dest_path + yield src_path @pytest.fixture(scope="module") def startup_scripts(self, monkeypatch_session, download_test_data): diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 59d4abb3aa..aa8ef3caab 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -45,6 +45,8 @@ class ModuleUnitTest(BaseTest): ASSET = "test_asset" TASK = "test_task" + TEST_DATA_FOLDER = None + @pytest.fixture(scope='session') def monkeypatch_session(self): """Monkeypatch couldn't be used with module or session fixtures.""" @@ -55,24 +57,28 @@ class ModuleUnitTest(BaseTest): @pytest.fixture(scope="module") def download_test_data(self): - tmpdir = tempfile.mkdtemp() - for test_file in self.TEST_FILES: - file_id, file_name, md5 = test_file + if self.TEST_DATA_FOLDER: + print("Using existing folder {}".format(self.TEST_DATA_FOLDER)) + yield self.TEST_DATA_FOLDER + else: + tmpdir = tempfile.mkdtemp() + for test_file in self.TEST_FILES: + file_id, file_name, md5 = test_file - f_name, ext = os.path.splitext(file_name) + f_name, ext = os.path.splitext(file_name) - RemoteFileHandler.download_file_from_google_drive(file_id, - str(tmpdir), - file_name) + RemoteFileHandler.download_file_from_google_drive(file_id, + str(tmpdir), + file_name) - if ext.lstrip('.') in RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS: - RemoteFileHandler.unzip(os.path.join(tmpdir, file_name)) - print("Temporary folder created:: {}".format(tmpdir)) - yield tmpdir + if ext.lstrip('.') in RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS: #noqa E501 + RemoteFileHandler.unzip(os.path.join(tmpdir, file_name)) + print("Temporary folder created:: {}".format(tmpdir)) + yield tmpdir - if not self.PERSIST: - print("Removing {}".format(tmpdir)) - shutil.rmtree(tmpdir) + if not self.PERSIST: + print("Removing {}".format(tmpdir)) + shutil.rmtree(tmpdir) @pytest.fixture(scope="module") def env_var(self, monkeypatch_session, download_test_data): From d0ada90e44a4746f2dcb154a0e46d0e77998c958 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Dec 2021 16:18:07 +0100 Subject: [PATCH 165/307] OP-2042 - added functionality to implicit choose variant If APP_VARIANT is empty it looks for latest installed variant of an application --- .../hosts/maya/test_publish_in_maya.py | 7 +++---- .../hosts/nuke/test_publish_in_nuke.py | 5 ++--- .../photoshop/test_publish_in_photoshop.py | 7 +++---- tests/lib/testing_classes.py | 19 +++++++++++++++---- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/tests/integration/hosts/maya/test_publish_in_maya.py b/tests/integration/hosts/maya/test_publish_in_maya.py index b53b26f66d..687e6fbc6e 100644 --- a/tests/integration/hosts/maya/test_publish_in_maya.py +++ b/tests/integration/hosts/maya/test_publish_in_maya.py @@ -26,16 +26,15 @@ class TestPublishInMaya(PublishTest): {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/maya # noqa: E501 """ - PERSIST = True + PERSIST = False TEST_FILES = [ ("1BTSIIULJTuDc8VvXseuiJV_fL6-Bu7FP", "test_maya_publish.zip", "") ] APP = "maya" - APP_VARIANT = "2019" - - APP_NAME = "{}/{}".format(APP, APP_VARIANT) + # keep empty to locate latest installed variant or explicit + APP_VARIANT = "" TIMEOUT = 120 # publish timeout diff --git a/tests/integration/hosts/nuke/test_publish_in_nuke.py b/tests/integration/hosts/nuke/test_publish_in_nuke.py index 483e9fef98..14a79fdf3d 100644 --- a/tests/integration/hosts/nuke/test_publish_in_nuke.py +++ b/tests/integration/hosts/nuke/test_publish_in_nuke.py @@ -32,9 +32,8 @@ class TestPublishInNuke(PublishTest): ] APP = "nuke" - APP_VARIANT = "12-2" - - APP_NAME = "{}/{}".format(APP, APP_VARIANT) + # keep empty to locate latest installed variant or explicit + APP_VARIANT = "" TIMEOUT = 120 # publish timeout diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py index 43e03b2cc3..c7f2399494 100644 --- a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py @@ -24,16 +24,15 @@ class TestPublishInPhotoshop(PublishTest): {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/photoshop # noqa: E501 """ - PERSIST = True + PERSIST = False TEST_FILES = [ ("1zD2v5cBgkyOm_xIgKz3WKn8aFB_j8qC-", "test_photoshop_publish.zip", "") ] APP = "photoshop" - APP_VARIANT = "2021" - - APP_NAME = "{}/{}".format(APP, APP_VARIANT) + # keep empty to locate latest installed variant or explicit + APP_VARIANT = "" TIMEOUT = 120 # publish timeout diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index aa8ef3caab..bf1c490ecd 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -11,6 +11,8 @@ import glob from tests.lib.db_handler import DBHandler from tests.lib.file_handler import RemoteFileHandler +from openpype.lib.remote_publish import find_variant_key + class BaseTest: """Empty base test class""" @@ -173,12 +175,15 @@ class PublishTest(ModuleUnitTest): """ APP = "" - APP_VARIANT = "" - - APP_NAME = "{}/{}".format(APP, APP_VARIANT) + APP_VARIANT = "" # keep empty to locate latest installed variant TIMEOUT = 120 # publish timeout + @property + def app_name(self): + if self.APP_VARIANT: + return "{}/{}".format(self.APP, self.APP_VARIANT) + @pytest.fixture(scope="module") def last_workfile_path(self, download_test_data): raise NotImplementedError @@ -224,7 +229,13 @@ class PublishTest(ModuleUnitTest): "task_name": self.TASK } - yield application_manager.launch(self.APP_NAME, **data) + variant = self.APP_VARIANT + if not variant: + variant = find_variant_key(application_manager, self.APP) + + app_name = "{}/{}".format(self.APP, variant) + + yield application_manager.launch(app_name, **data) @pytest.fixture(scope="module") def publish_finished(self, dbcon, launched_app, download_test_data): From b61b3634b289406469e08ab3a9617860ef979840 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Dec 2021 16:20:31 +0100 Subject: [PATCH 166/307] OP-2042 - Hound --- tests/integration/hosts/nuke/test_publish_in_nuke.py | 1 - tests/lib/testing_classes.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/integration/hosts/nuke/test_publish_in_nuke.py b/tests/integration/hosts/nuke/test_publish_in_nuke.py index 14a79fdf3d..fe1745299d 100644 --- a/tests/integration/hosts/nuke/test_publish_in_nuke.py +++ b/tests/integration/hosts/nuke/test_publish_in_nuke.py @@ -1,6 +1,5 @@ import pytest import os -import shutil import logging from tests.lib.testing_classes import PublishTest diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index bf1c490ecd..92b2b2b52b 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -73,7 +73,7 @@ class ModuleUnitTest(BaseTest): str(tmpdir), file_name) - if ext.lstrip('.') in RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS: #noqa E501 + if ext.lstrip('.') in RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS: # noqa: E501 RemoteFileHandler.unzip(os.path.join(tmpdir, file_name)) print("Temporary folder created:: {}".format(tmpdir)) yield tmpdir From d1607f02a4ceefba768f30fdc82bc11f5ee3b465 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 3 Dec 2021 16:59:56 +0100 Subject: [PATCH 167/307] nuke complete menu to openpype --- openpype/hosts/nuke/api/menu.py | 129 +++++++++++++++----------------- 1 file changed, 61 insertions(+), 68 deletions(-) diff --git a/openpype/hosts/nuke/api/menu.py b/openpype/hosts/nuke/api/menu.py index 4636098604..8b5ab832ac 100644 --- a/openpype/hosts/nuke/api/menu.py +++ b/openpype/hosts/nuke/api/menu.py @@ -1,13 +1,11 @@ import os import nuke -from avalon.api import Session from avalon.nuke.pipeline import get_main_window from .lib import WorkfileSettings from openpype.api import Logger, BuildWorkfile, get_current_project_settings from openpype.tools.utils import host_tools -from avalon.nuke.pipeline import get_main_window log = Logger().get_logger(__name__) @@ -15,87 +13,82 @@ menu_label = os.environ["AVALON_LABEL"] def install(): + from openpype.hosts.nuke.api import reload_config + # uninstall original avalon menu + uninstall() + main_window = get_main_window() menubar = nuke.menu("Nuke") - menu = menubar.findItem(menu_label) + menu = menubar.addMenu(menu_label) - # replace reset resolution from avalon core to pype's - name = "Work Files..." - rm_item = [ - (i, item) for i, item in enumerate(menu.items()) if name in item.name() - ][0] - - log.debug("Changing Item: {}".format(rm_item)) - - menu.removeItem(rm_item[1].name()) - menu.addCommand( - name, - lambda: host_tools.show_workfiles(parent=main_window), - index=2 + label = "{0}, {1}".format( + os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"] ) - menu.addSeparator(index=3) - # replace reset resolution from avalon core to pype's - name = "Reset Resolution" - new_name = "Set Resolution" - rm_item = [ - (i, item) for i, item in enumerate(menu.items()) if name in item.name() - ][0] - - log.debug("Changing Item: {}".format(rm_item)) - # rm_item[1].setEnabled(False) - menu.removeItem(rm_item[1].name()) - menu.addCommand( - new_name, - lambda: WorkfileSettings().reset_resolution(), - index=(rm_item[0]) - ) - - # replace reset frame range from avalon core to pype's - name = "Reset Frame Range" - new_name = "Set Frame Range" - rm_item = [ - (i, item) for i, item in enumerate(menu.items()) if name in item.name() - ][0] - log.debug("Changing Item: {}".format(rm_item)) - # rm_item[1].setEnabled(False) - menu.removeItem(rm_item[1].name()) - menu.addCommand( - new_name, - lambda: WorkfileSettings().reset_frame_range_handles(), - index=(rm_item[0]) - ) - - # add colorspace menu item - name = "Set Colorspace" - menu.addCommand( - name, lambda: WorkfileSettings().set_colorspace() - ) - log.debug("Adding menu item: {}".format(name)) - - # add item that applies all setting above - name = "Apply All Settings" - menu.addCommand( - name, - lambda: WorkfileSettings().set_context_settings() - ) - log.debug("Adding menu item: {}".format(name)) + context_action = menu.addCommand(label) + context_action.setEnabled(False) menu.addSeparator() - - # add workfile builder menu item - name = "Build Workfile" menu.addCommand( - name, lambda: BuildWorkfile().process() + "Work Files...", + lambda: host_tools.show_workfiles(parent=main_window) + ) + + menu.addSeparator() + menu.addCommand( + "Create...", + lambda: host_tools.show_creator(parent=main_window) + ) + menu.addCommand( + "Load...", + lambda: host_tools.show_loader( + parent=main_window, + use_context=True + ) + ) + menu.addCommand( + "Publish...", + lambda: host_tools.show_publish(parent=main_window) + ) + menu.addCommand( + "Manage...", + lambda: host_tools.show_scene_inventory(parent=main_window) + ) + + menu.addSeparator() + menu.addCommand( + "Set Resolution", + lambda: WorkfileSettings().reset_resolution() + ) + menu.addCommand( + "Set Frame Range", + lambda: WorkfileSettings().reset_frame_range_handles() + ) + menu.addCommand( + "Set Colorspace", + lambda: WorkfileSettings().set_colorspace() + ) + menu.addCommand( + "Apply All Settings", + lambda: WorkfileSettings().set_context_settings() + ) + + menu.addSeparator() + menu.addCommand( + "Build Workfile", + lambda: BuildWorkfile().process() ) - log.debug("Adding menu item: {}".format(name)) - # Add experimental tools action menu.addSeparator() menu.addCommand( "Experimental tools...", lambda: host_tools.show_experimental_tools_dialog(parent=main_window) ) + # add reload pipeline only in debug mode + if bool(os.getenv("NUKE_DEBUG")): + menu.addSeparator() + menu.addCommand("Reload Pipeline", reload_config) + # adding shortcuts add_shortcuts_from_presets() From 37a9346ff1985cb2056cec2bb35ce15918981e6e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 17:00:12 +0100 Subject: [PATCH 168/307] fix assignment of families in TVpaint --- .../plugins/publish/collect_tvpaint_instances.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py b/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py index c533403e5f..976a14e808 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py @@ -138,7 +138,7 @@ class CollectTVPaintInstances(pyblish.api.ContextPlugin): "family": "render" } subset_name = get_subset_name_with_asset_doc( - self.render_pass_family, + self.render_layer_family, variant, task_name, asset_doc, @@ -223,7 +223,7 @@ class CollectTVPaintInstances(pyblish.api.ContextPlugin): "name": subset_name, "subset": subset_name, "label": subset_name, - "family": self.render_pass_family, + "family": "render", # Add `review` family for thumbnail integration "families": [self.render_pass_family, "review"], "representations": [], @@ -239,9 +239,9 @@ class CollectTVPaintInstances(pyblish.api.ContextPlugin): "name": subset_name, "subset": subset_name, "label": subset_name, - "family": self.render_pass_family, + "family": "render", # Add `review` family for thumbnail integration - "families": [self.render_pass_family, "review"], + "families": [self.render_layer_family, "review"], "representations": [], "layers": layers, "stagingDir": staging_dir From 89490f4b14815d402c12ab8680e69c3241760da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20LORRAIN?= Date: Fri, 3 Dec 2021 17:22:27 +0100 Subject: [PATCH 169/307] add maya default render folder path to settings --- .../maya/plugins/publish/validate_render_image_rule.py | 8 +++++++- .../deadline/plugins/publish/submit_maya_deadline.py | 7 ++++++- openpype/settings/defaults/project_settings/maya.json | 3 ++- .../projects_schema/schemas/schema_maya_create.json | 5 +++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py index dad1691149..e892b239cc 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -23,7 +23,13 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): def process(self, instance): - assert get_file_rule("images") == "renders", ( + default_render_file = instance.context.data.get('project_settings')\ + .get('maya') \ + .get('create') \ + .get('CreateRender') \ + .get('default_render_image_folder') + + assert get_file_rule("images") == default_render_file, ( "Workspace's `images` file rule must be set to: renders" ) diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py index e6c42374ca..51a19e2aad 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py @@ -394,9 +394,14 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): self.log.debug(filepath) # Gather needed data ------------------------------------------------ + default_render_file = instance.context.data.get('project_settings')\ + .get('maya')\ + .get('create')\ + .get('CreateRender')\ + .get('default_render_image_folder') filename = os.path.basename(filepath) comment = context.data.get("comment", "") - dirname = os.path.join(workspace, "renders") + dirname = os.path.join(workspace, default_render_file) renderlayer = instance.data['setMembers'] # rs_beauty deadline_user = context.data.get("user", getpass.getuser()) jobname = "%s - %s" % (filename, instance.name) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 73c75ef3ee..cbcf8cd600 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -43,7 +43,8 @@ "defaults": [ "Main" ], - "aov_separator": "underscore" + "aov_separator": "underscore", + "default_render_image_folder": "renders" }, "CreateAnimation": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index e50357cc40..088d5d1f96 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -58,6 +58,11 @@ {"underscore": "_ (underscore)"}, {"dot": ". (dot)"} ] + }, + { + "type": "text", + "key": "default_render_image_folder", + "label": "Default render image folder" } ] }, From efa7bffe066cac6afcb68616f7196dadfb4df53d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Dec 2021 17:33:50 +0100 Subject: [PATCH 170/307] OP-2042 - added a bit of documentation --- tests/integration/README.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/integration/README.md b/tests/integration/README.md index 81c07ec50c..aeed53d097 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -5,16 +5,23 @@ Contains end-to-end tests for automatic testing of OP. Should run headless publish on all hosts to check basic publish use cases automatically to limit regression issues. +How to run +---------- +- activate `{OPENPYPE_ROOT}/.venv` +- run in cmd +`{OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests {OPENPYPE_ROOT}/tests/integration` + - add `hosts/APP_NAME` after integration part to limit only on specific app (eg. `{OPENPYPE_ROOT}/tests/integration/hosts/maya`) + How to create test for publishing from host ------------------------------------------ -- Extend PublishTest +- Extend PublishTest in `tests/lib/testing_classes.py` - Use `resources\test_data.zip` skeleton file as a template for testing input data - Put workfile into `test_data.zip/input/workfile` - If you require other than base DB dumps provide them to `test_data.zip/input/dumps` -- (Check commented code in `db_handler.py` how to dump specific DB. Currently all collections will be dumped.) - Implement `last_workfile_path` - `startup_scripts` - must contain pointing host to startup script saved into `test_data.zip/input/startup` - -- Script must contain something like + -- Script must contain something like (pseudocode) ``` import openpype from avalon import api, HOST @@ -25,13 +32,18 @@ pyblish.util.publish() EXIT_APP (command to exit host) ``` (Install and publish methods must be triggered only AFTER host app is fully initialized!) -- Zip `test_data.zip`, named it with descriptive name, upload it to Google Drive, right click - `Get link`, copy hash id +- If you would like add any command line arguments for your host app add it to `test_data.zip/input/app_args/app_args.json` (as a json list) +- Provide any required environment variables to `test_data.zip/input/env_vars/env_vars.json` (as a json dictionary) +- Zip `test_data.zip`, named it with descriptive name, upload it to Google Drive, right click - `Get link`, copy hash id (file must be accessible to anyone with a link!) - Put this hash id and zip file name into TEST_FILES [(HASH_ID, FILE_NAME, MD5_OPTIONAL)]. If you want to check MD5 of downloaded file, provide md5 value of zipped file. - Implement any assert checks you need in extended class - Run test class manually (via Pycharm or pytest runner (TODO)) -- If you want test to compare expected files to published one, set PERSIST to True, run test manually +- If you want test to visually compare expected files to published one, set PERSIST to True, run test manually -- Locate temporary `publish` subfolder of temporary folder (found in debugging console log) -- Copy whole folder content into .zip file into `expected` subfolder -- By default tests are comparing only structure of `expected` and published format (eg. if you want to save space, replace published files with empty files, but with expected names!) - -- Zip and upload again, change PERSIST to False \ No newline at end of file + -- Zip and upload again, change PERSIST to False + +- Use `TEST_DATA_FOLDER` variable in your class to reuse existing downloaded and unzipped test data (for faster creation of tests) +- Keep `APP_VARIANT` empty if you want to trigger test on latest version of app, or provide explicit value (as '2022' for Photoshop for example) \ No newline at end of file From cd55209e632526456574bbcc27914352fd5399b0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 3 Dec 2021 17:34:11 +0100 Subject: [PATCH 171/307] fixing menu context label re-drawing --- openpype/hosts/nuke/api/__init__.py | 5 +++++ openpype/hosts/nuke/api/menu.py | 34 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py index e684b48fa3..1567189ed1 100644 --- a/openpype/hosts/nuke/api/__init__.py +++ b/openpype/hosts/nuke/api/__init__.py @@ -54,6 +54,10 @@ def install(): ''' Installing all requarements for Nuke host ''' + # remove all registred callbacks form avalon.nuke + from avalon import pipeline + pipeline._registered_event_handlers.clear() + log.info("Registering Nuke plug-ins..") pyblish.api.register_plugin_path(PUBLISH_PATH) avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) @@ -62,6 +66,7 @@ def install(): # Register Avalon event for workfiles loading. avalon.api.on("workio.open_file", lib.check_inventory_versions) + avalon.api.on("taskChanged", menu.change_context_label) pyblish.api.register_callback( "instanceToggled", on_pyblish_instance_toggled) diff --git a/openpype/hosts/nuke/api/menu.py b/openpype/hosts/nuke/api/menu.py index 8b5ab832ac..86293edb99 100644 --- a/openpype/hosts/nuke/api/menu.py +++ b/openpype/hosts/nuke/api/menu.py @@ -10,10 +10,43 @@ from openpype.tools.utils import host_tools log = Logger().get_logger(__name__) menu_label = os.environ["AVALON_LABEL"] +context_label = None + + +def change_context_label(*args): + global context_label + menubar = nuke.menu("Nuke") + menu = menubar.findItem(menu_label) + + label = "{0}, {1}".format( + os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"] + ) + + rm_item = [ + (i, item) for i, item in enumerate(menu.items()) + if context_label in item.name() + ][0] + + menu.removeItem(rm_item[1].name()) + + context_action = menu.addCommand( + label, + index=(rm_item[0]) + ) + context_action.setEnabled(False) + + log.info("Task label changed from `{}` to `{}`".format( + context_label, label)) + + context_label = label + def install(): from openpype.hosts.nuke.api import reload_config + + global context_label + # uninstall original avalon menu uninstall() @@ -24,6 +57,7 @@ def install(): label = "{0}, {1}".format( os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"] ) + context_label = label context_action = menu.addCommand(label) context_action.setEnabled(False) From 8eb7a30358275f47ca21691dca221fc00f2b4d57 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Dec 2021 17:38:02 +0100 Subject: [PATCH 172/307] OP-2042 - added a functionality to inject additional command line arguments --- openpype/lib/applications.py | 2 ++ tests/lib/testing_classes.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 30be92e886..6eb44a9694 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -716,6 +716,8 @@ class ApplicationLaunchContext: # subprocess.Popen launch arguments (first argument in constructor) self.launch_args = executable.as_args() self.launch_args.extend(application.arguments) + if self.data.get("app_args"): + self.launch_args.extend(self.data.pop("app_args")) # Handle launch environemtns env = self.data.pop("env", None) diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 92b2b2b52b..15ab685739 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -192,9 +192,35 @@ class PublishTest(ModuleUnitTest): def startup_scripts(self, monkeypatch_session, download_test_data): raise NotImplementedError + @pytest.fixture(scope="module") + def app_args(self, download_test_data): + """Returns additional application arguments from a test file. + + Test zip file should contain file at: + FOLDER_DIR/input/app_args/app_args.json + containing a list of command line arguments (like '-x' etc.) + """ + app_args = [] + args_url = os.path.join(download_test_data, "input", + "app_args", "app_args.json") + if not os.path.exists(args_url): + print("App argument file {} doesn't exist".format(args_url)) + else: + try: + with open(args_url) as json_file: + app_args = json.load(json_file) + + if not isinstance(app_args, list): + raise ValueError + except ValueError: + print("{} doesn't contain valid JSON".format(args_url)) + six.reraise(*sys.exc_info()) + + yield app_args + @pytest.fixture(scope="module") def launched_app(self, dbcon, download_test_data, last_workfile_path, - startup_scripts): + startup_scripts, app_args): """Launch host app""" # set publishing folders root_key = "config.roots.work.{}".format("windows") # TEMP @@ -228,6 +254,8 @@ class PublishTest(ModuleUnitTest): "asset_name": self.ASSET, "task_name": self.TASK } + if app_args: + data["app_args"] = app_args variant = self.APP_VARIANT if not variant: From 8e41bfa855566d239b1a874d2289ce2564b60546 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 4 Dec 2021 03:40:56 +0000 Subject: [PATCH 173/307] [Automated] Bump version --- CHANGELOG.md | 39 ++++++++++++++++----------------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37a53d341c..855c3016f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.7.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.7.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.4...HEAD) @@ -14,6 +14,9 @@ **🚀 Enhancements** +- Settings: Webpublisher in hosts enum [\#2367](https://github.com/pypeclub/OpenPype/pull/2367) +- Burnins: Be able recognize mxf OPAtom format [\#2361](https://github.com/pypeclub/OpenPype/pull/2361) +- Local settings: Copyable studio paths [\#2349](https://github.com/pypeclub/OpenPype/pull/2349) - Assets Widget: Clear model on project change [\#2345](https://github.com/pypeclub/OpenPype/pull/2345) - General: OpenPype default modules hierarchy [\#2338](https://github.com/pypeclub/OpenPype/pull/2338) - General: FFprobe error exception contain original error message [\#2328](https://github.com/pypeclub/OpenPype/pull/2328) @@ -21,6 +24,7 @@ - Hiero: Add experimental tools action [\#2323](https://github.com/pypeclub/OpenPype/pull/2323) - Input links: Cleanup and unification of differences [\#2322](https://github.com/pypeclub/OpenPype/pull/2322) - General: Don't validate vendor bin with executing them [\#2317](https://github.com/pypeclub/OpenPype/pull/2317) +- General: Multilayer EXRs support [\#2315](https://github.com/pypeclub/OpenPype/pull/2315) - General: Run process log stderr as info log level [\#2309](https://github.com/pypeclub/OpenPype/pull/2309) - General: Reduce vendor imports [\#2305](https://github.com/pypeclub/OpenPype/pull/2305) - Tools: Cleanup of unused classes [\#2304](https://github.com/pypeclub/OpenPype/pull/2304) @@ -28,16 +32,20 @@ - Ftrack: Synchronize input links [\#2287](https://github.com/pypeclub/OpenPype/pull/2287) - StandalonePublisher: Remove unused plugin ExtractHarmonyZip [\#2277](https://github.com/pypeclub/OpenPype/pull/2277) - Ftrack: Support multiple reviews [\#2271](https://github.com/pypeclub/OpenPype/pull/2271) -- Ftrack: Remove unused clean component plugin [\#2269](https://github.com/pypeclub/OpenPype/pull/2269) - Royal Render: Support for rr channels in separate dirs [\#2268](https://github.com/pypeclub/OpenPype/pull/2268) -- Houdini: Add experimental tools action [\#2267](https://github.com/pypeclub/OpenPype/pull/2267) -- Nuke: extract baked review videos presets [\#2248](https://github.com/pypeclub/OpenPype/pull/2248) -- TVPaint: Workers rendering [\#2209](https://github.com/pypeclub/OpenPype/pull/2209) **🐛 Bug fixes** +- Webpublisher: Fix assignment of families of TVpaint instances [\#2373](https://github.com/pypeclub/OpenPype/pull/2373) +- JobQueue: Fix loading of settings [\#2362](https://github.com/pypeclub/OpenPype/pull/2362) +- Tools: Placeholder color [\#2359](https://github.com/pypeclub/OpenPype/pull/2359) +- Launcher: Minimize button on MacOs [\#2355](https://github.com/pypeclub/OpenPype/pull/2355) +- StandalonePublisher: Fix import of constant [\#2354](https://github.com/pypeclub/OpenPype/pull/2354) +- Adobe products show issue [\#2347](https://github.com/pypeclub/OpenPype/pull/2347) - Maya Look Assigner: Fix Python 3 compatibility [\#2343](https://github.com/pypeclub/OpenPype/pull/2343) +- Remove wrongly used host for hook [\#2342](https://github.com/pypeclub/OpenPype/pull/2342) - Tools: Use Qt context on tools show [\#2340](https://github.com/pypeclub/OpenPype/pull/2340) +- Flame: Fix default argument value in custom dictionary [\#2339](https://github.com/pypeclub/OpenPype/pull/2339) - Timers Manager: Disable auto stop timer on linux platform [\#2334](https://github.com/pypeclub/OpenPype/pull/2334) - nuke: bake preset single input exception [\#2331](https://github.com/pypeclub/OpenPype/pull/2331) - Hiero: fixing multiple templates at a hierarchy parent [\#2330](https://github.com/pypeclub/OpenPype/pull/2330) @@ -52,11 +60,10 @@ - Alternate site for site sync doesnt work for sequences [\#2284](https://github.com/pypeclub/OpenPype/pull/2284) - FFmpeg: Execute ffprobe using list of arguments instead of string command [\#2281](https://github.com/pypeclub/OpenPype/pull/2281) - Nuke: Anatomy fill data use task as dictionary [\#2278](https://github.com/pypeclub/OpenPype/pull/2278) -- Bug: fix variable name \_asset\_id in workfiles application [\#2274](https://github.com/pypeclub/OpenPype/pull/2274) -- Version handling fixes [\#2272](https://github.com/pypeclub/OpenPype/pull/2272) **Merged pull requests:** +- Update of avalon-core [\#2346](https://github.com/pypeclub/OpenPype/pull/2346) - Maya: configurable model top level validation [\#2321](https://github.com/pypeclub/OpenPype/pull/2321) ## [3.6.4](https://github.com/pypeclub/OpenPype/tree/3.6.4) (2021-11-23) @@ -81,13 +88,14 @@ **🚀 Enhancements** +- Ftrack: Remove unused clean component plugin [\#2269](https://github.com/pypeclub/OpenPype/pull/2269) - Tools: Assets widget [\#2265](https://github.com/pypeclub/OpenPype/pull/2265) - SceneInventory: Choose loader in asset switcher [\#2262](https://github.com/pypeclub/OpenPype/pull/2262) - Style: New fonts in OpenPype style [\#2256](https://github.com/pypeclub/OpenPype/pull/2256) - Tools: SceneInventory in OpenPype [\#2255](https://github.com/pypeclub/OpenPype/pull/2255) - Tools: Tasks widget [\#2251](https://github.com/pypeclub/OpenPype/pull/2251) +- Nuke: extract baked review videos presets [\#2248](https://github.com/pypeclub/OpenPype/pull/2248) - Tools: Creator in OpenPype [\#2244](https://github.com/pypeclub/OpenPype/pull/2244) -- Added endpoint for configured extensions [\#2221](https://github.com/pypeclub/OpenPype/pull/2221) **🐛 Bug fixes** @@ -111,24 +119,12 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.0-nightly.6...3.6.0) -### 📖 Documentation - -- Add alternative sites for Site Sync [\#2206](https://github.com/pypeclub/OpenPype/pull/2206) - **🚀 Enhancements** - Tools: Subset manager in OpenPype [\#2243](https://github.com/pypeclub/OpenPype/pull/2243) - General: Skip module directories without init file [\#2239](https://github.com/pypeclub/OpenPype/pull/2239) - General: Static interfaces [\#2238](https://github.com/pypeclub/OpenPype/pull/2238) - Style: Fix transparent image in style [\#2235](https://github.com/pypeclub/OpenPype/pull/2235) -- Add a "following workfile versioning" option on publish [\#2225](https://github.com/pypeclub/OpenPype/pull/2225) -- Modules: Module can add cli commands [\#2224](https://github.com/pypeclub/OpenPype/pull/2224) -- Webpublisher: Separate webpublisher logic [\#2222](https://github.com/pypeclub/OpenPype/pull/2222) -- Add both side availability on Site Sync sites to Loader [\#2220](https://github.com/pypeclub/OpenPype/pull/2220) -- Tools: Center loader and library loader on show [\#2219](https://github.com/pypeclub/OpenPype/pull/2219) -- Maya : Validate shape zero [\#2212](https://github.com/pypeclub/OpenPype/pull/2212) -- Maya : validate unique names [\#2211](https://github.com/pypeclub/OpenPype/pull/2211) -- Tools: OpenPype stylesheet in workfiles tool [\#2208](https://github.com/pypeclub/OpenPype/pull/2208) **🐛 Bug fixes** @@ -138,9 +134,6 @@ - Python 2: Unicode to string conversion [\#2236](https://github.com/pypeclub/OpenPype/pull/2236) - Fix - enum for color coding in PS [\#2234](https://github.com/pypeclub/OpenPype/pull/2234) - Pyblish Tool: Fix targets handling [\#2232](https://github.com/pypeclub/OpenPype/pull/2232) -- Ftrack: Base event fix of 'get\_project\_from\_entity' method [\#2214](https://github.com/pypeclub/OpenPype/pull/2214) -- Maya : multiple subsets review broken [\#2210](https://github.com/pypeclub/OpenPype/pull/2210) -- Fix - different command used for Linux and Mac OS [\#2207](https://github.com/pypeclub/OpenPype/pull/2207) ## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17) diff --git a/openpype/version.py b/openpype/version.py index 5160fbdfb8..f58f22be4f 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.7.0-nightly.4" +__version__ = "3.7.0-nightly.5" diff --git a/pyproject.toml b/pyproject.toml index 86b40762e6..bde4263241 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.7.0-nightly.4" # OpenPype +version = "3.7.0-nightly.5" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From b3ec2fe524dfd05ab60ef88649f40aa4b29ce3f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20LORRAIN?= Date: Mon, 6 Dec 2021 10:28:56 +0100 Subject: [PATCH 174/307] Use the setting in CollectRender --- openpype/hosts/maya/plugins/publish/collect_render.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 580d459a90..e26859ad93 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -221,14 +221,18 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # append full path full_exp_files = [] aov_dict = {} - + default_render_file = context.data.get('project_settings')\ + .get('maya')\ + .get('create')\ + .get('CreateRender')\ + .get('default_render_image_folder') # replace relative paths with absolute. Render products are # returned as list of dictionaries. publish_meta_path = None for aov in exp_files: full_paths = [] for file in aov[aov.keys()[0]]: - full_path = os.path.join(workspace, "renders", file) + full_path = os.path.join(workspace, default_render_file, file) full_path = full_path.replace("\\", "/") full_paths.append(full_path) publish_meta_path = os.path.dirname(full_path) From ca1516fa13b03409c160f3c0f97785143d59fd4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20LORRAIN?= Date: Mon, 6 Dec 2021 10:45:22 +0100 Subject: [PATCH 175/307] Fix syntax --- openpype/hosts/maya/plugins/publish/collect_render.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index e26859ad93..788ed12b41 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -222,17 +222,18 @@ class CollectMayaRender(pyblish.api.ContextPlugin): full_exp_files = [] aov_dict = {} default_render_file = context.data.get('project_settings')\ - .get('maya')\ - .get('create')\ - .get('CreateRender')\ - .get('default_render_image_folder') + .get('maya')\ + .get('create')\ + .get('CreateRender')\ + .get('default_render_image_folder') # replace relative paths with absolute. Render products are # returned as list of dictionaries. publish_meta_path = None for aov in exp_files: full_paths = [] for file in aov[aov.keys()[0]]: - full_path = os.path.join(workspace, default_render_file, file) + full_path = os.path.join(workspace, default_render_file, + file) full_path = full_path.replace("\\", "/") full_paths.append(full_path) publish_meta_path = os.path.dirname(full_path) From 7e04079151479548daf15ec81768c454935c71d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20LORRAIN?= Date: Mon, 6 Dec 2021 14:07:01 +0100 Subject: [PATCH 176/307] Update error message --- .../hosts/maya/plugins/publish/validate_render_image_rule.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py index e892b239cc..a912431b55 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -30,7 +30,9 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): .get('default_render_image_folder') assert get_file_rule("images") == default_render_file, ( - "Workspace's `images` file rule must be set to: renders" + "Workspace's `images` file rule must be set to: {}".format( + default_render_file + ) ) @classmethod From 04c50532bb440f0d7d005c124495153f9e20076c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 6 Dec 2021 15:41:47 +0100 Subject: [PATCH 177/307] OP-2042 - added a functionality to run tests either with start.py or openpype_console --- openpype/pype_commands.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index ce1a9718b3..f01c6f0e42 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -366,12 +366,12 @@ class PypeCommands: pyargs_str = "--pyargs {}".format(pyargs) # disable warnings and show captured stdout even if success - args_str = "--disable-pytest-warnings -rP" + args = ["--disable-pytest-warnings", "-rP"] - cmd = "pytest {} {} {} {}".format(args_str, folder, - mark_str, pyargs_str) - print("Running {}".format(cmd)) - subprocess.run(cmd) + args += [folder, mark_str, pyargs_str] + print("run_tests args: {}".format(args)) + import pytest + pytest.main(args) def syncserver(self, active_site): """Start running sync_server in background.""" From 150f6efd08fcf5e1f83d25110c8b76c88b52856f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 6 Dec 2021 17:00:28 +0100 Subject: [PATCH 178/307] OP-2042 - explicitly sort variants alphabetically --- openpype/lib/remote_publish.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index d7db4d1ab9..976de048f6 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -2,6 +2,7 @@ import os from datetime import datetime import sys from bson.objectid import ObjectId +import collections import pyblish.util import pyblish.api @@ -140,7 +141,9 @@ def find_variant_key(application_manager, host): found_variant_key = None # finds most up-to-date variant if any installed - for variant_key, variant in app_group.variants.items(): + sorted_variants = collections.OrderedDict( + sorted(app_group.variants.items())) + for variant_key, variant in sorted_variants.items(): for executable in variant.executables: if executable.exists(): found_variant_key = variant_key From eda5ff6b91b48bec5e747754cc9f664e7e47c3a8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 6 Dec 2021 17:28:02 +0100 Subject: [PATCH 179/307] OP-2042 - better format of asserts --- .../hosts/nuke/test_publish_in_nuke.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/integration/hosts/nuke/test_publish_in_nuke.py b/tests/integration/hosts/nuke/test_publish_in_nuke.py index fe1745299d..d603b9f177 100644 --- a/tests/integration/hosts/nuke/test_publish_in_nuke.py +++ b/tests/integration/hosts/nuke/test_publish_in_nuke.py @@ -24,7 +24,7 @@ class TestPublishInNuke(PublishTest): {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/nuke # noqa: E501 """ - PERSIST = False # True - keep test_db, test_openpype, outputted test files + PERSIST = True # True - keep test_db, test_openpype, outputted test files TEST_FILES = [ ("1SUurHj2aiQ21ZIMJfGVBI2KjR8kIjBGI", "test_Nuke_publish.zip", "") @@ -67,8 +67,10 @@ class TestPublishInNuke(PublishTest): def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" print("test_db_asserts") - assert 5 == dbcon.count_documents({"type": "version"}), \ - "Not expected no of versions" + versions = dbcon.count_documents({"type": "version"}) + assert 5 == versions, \ + "Not expected no of versions. "\ + "Expected 5, found {}".format(versions) assert 0 == dbcon.count_documents({"type": "version", "name": {"$ne": 1}}), \ @@ -86,10 +88,12 @@ class TestPublishInNuke(PublishTest): assert 10 == dbcon.count_documents({"type": "representation"}), \ "Not expected no of representations" - assert 1 == dbcon.count_documents({"type": "representation", + reprs = dbcon.count_documents({"type": "representation", "context.subset": "renderCompositingInNukeMain", # noqa: E501 - "context.ext": "exr"}), \ - "Not expected no of representations with ext 'exr'" + "context.ext": "exr"}) + assert 1 == reprs, \ + "Not expected no of representations with ext 'exr'."\ + "Expected 1, found {}".format(reprs) if __name__ == "__main__": From 4a93c6927d52b5f363a2c1ffb6130d84dc0613d5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 6 Dec 2021 17:28:23 +0100 Subject: [PATCH 180/307] OP-2042 - added mention of possibility to trigger it via executables --- tests/integration/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integration/README.md b/tests/integration/README.md index aeed53d097..8839e2e43f 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -11,6 +11,9 @@ How to run - run in cmd `{OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests {OPENPYPE_ROOT}/tests/integration` - add `hosts/APP_NAME` after integration part to limit only on specific app (eg. `{OPENPYPE_ROOT}/tests/integration/hosts/maya`) + +OR can use built executables +`openpype_console runtests {ABS_PATH}/tests/integration` How to create test for publishing from host ------------------------------------------ From c0ad1b5d8b635d24ccb81ac69bc9e38544d6eec9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Dec 2021 11:01:55 +0100 Subject: [PATCH 181/307] OP-2042 - use existing value of env var OPENPYPE_DATABASE_NAME if possible For better automatic testing --- start.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/start.py b/start.py index 0f7e82071d..ae6aefe34e 100644 --- a/start.py +++ b/start.py @@ -925,7 +925,9 @@ def boot(): sys.exit(1) os.environ["OPENPYPE_MONGO"] = openpype_mongo - os.environ["OPENPYPE_DATABASE_NAME"] = "openpype" # name of Pype database + # name of Pype database + os.environ["OPENPYPE_DATABASE_NAME"] = \ + os.environ.get("OPENPYPE_DATABASE_NAME") or "openpype" _print(">>> run disk mapping command ...") run_disk_mapping_commands(openpype_mongo) From 709d0ee625dfad38b70590ee1804b0f376ce4a10 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Dec 2021 11:02:16 +0100 Subject: [PATCH 182/307] OP-2042 - reset connection to openpype DB --- tests/lib/testing_classes.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 15ab685739..0a04dc59c2 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -108,6 +108,14 @@ class ModuleUnitTest(BaseTest): import openpype openpype_root = os.path.dirname(os.path.dirname(openpype.__file__)) + + #reset connection to openpype DB with new env var + import openpype.settings.lib as sett_lib + sett_lib._SETTINGS_HANDLER = None + sett_lib._LOCAL_SETTINGS_HANDLER = None + sett_lib.create_settings_handler() + sett_lib.create_local_settings_handler() + # ?? why 2 of those monkeypatch_session.setenv("OPENPYPE_ROOT", openpype_root) monkeypatch_session.setenv("OPENPYPE_REPOS_ROOT", openpype_root) From ad81890d6b92a0ae01f25b74ee8954702fb9a0bc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Dec 2021 12:41:24 +0100 Subject: [PATCH 183/307] hiero: fix workio and flatten --- openpype/hosts/hiero/api/lib.py | 7 ++++--- openpype/hosts/hiero/api/otio/hiero_export.py | 13 +++++++------ openpype/hosts/hiero/api/workio.py | 8 ++------ 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index f1985a2e12..29484a2141 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -37,10 +37,11 @@ self.default_bin_name = "openpypeBin" AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") -def flatten(input_list): - for item in input_list: +def flatten(_list): + for item in _list: if isinstance(item, (list, tuple)): - yield from flatten(item) + for sub_item in flatten(item): + yield sub_item else: yield item diff --git a/openpype/hosts/hiero/api/otio/hiero_export.py b/openpype/hosts/hiero/api/otio/hiero_export.py index 50a00a1624..abf510403e 100644 --- a/openpype/hosts/hiero/api/otio/hiero_export.py +++ b/openpype/hosts/hiero/api/otio/hiero_export.py @@ -28,12 +28,13 @@ self.timeline = None self.include_tags = True -def flatten(l): - for i in l: - if isinstance(i, (list, tuple)): - yield from flatten(i) - else: - yield i +def flatten(_list): + for item in _list: + if isinstance(item, (list, tuple)): + for sub_item in flatten(item): + yield sub_item + else: + yield item def get_current_hiero_project(remove_untitled=False): diff --git a/openpype/hosts/hiero/api/workio.py b/openpype/hosts/hiero/api/workio.py index 15ffbf84d8..dacb11624f 100644 --- a/openpype/hosts/hiero/api/workio.py +++ b/openpype/hosts/hiero/api/workio.py @@ -65,13 +65,9 @@ def open_file(filepath): def current_file(): current_file = hiero.core.projects()[-1].path() - normalised = os.path.normpath(current_file) - - # Unsaved current file - if normalised == "": + if not current_file: return None - - return normalised + return os.path.normpath(current_file) def work_root(session): From 0270b136cc7cd5a32be204133ca632ae237cb72e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Dec 2021 15:57:44 +0100 Subject: [PATCH 184/307] hiero: solve custom ocio path --- openpype/hosts/hiero/api/lib.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 29484a2141..9a22d8cf27 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -4,6 +4,7 @@ Host specific functions where host api is connected import os import re import sys +import platform import ast import shutil import hiero @@ -783,6 +784,13 @@ def _set_hrox_project_knobs(doc, **knobs): # set attributes to Project Tag proj_elem = doc.documentElement().firstChildElement("Project") for k, v in knobs.items(): + if "ocioconfigpath" in k: + paths_to_format = v[platform.system().lower()] + for _path in paths_to_format: + v = _path.format(**os.environ) + if not os.path.exists(v): + continue + log.debug("Project colorspace knob `{}` was set to `{}`".format(k, v)) if isinstance(v, dict): continue proj_elem.setAttribute(str(k), v) From 8a5e1d43d3649dba27de8b44c04db6e6eb25344a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 7 Dec 2021 16:00:42 +0100 Subject: [PATCH 185/307] update clique to 1.6 for py3 compatibility start and end frame for representation --- .../publish/collect_sequences_from_job.py | 2 ++ poetry.lock | 20 +++++++++++-------- pyproject.toml | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/openpype/modules/default_modules/royal_render/plugins/publish/collect_sequences_from_job.py b/openpype/modules/default_modules/royal_render/plugins/publish/collect_sequences_from_job.py index 3eed79fd19..b389b022cf 100644 --- a/openpype/modules/default_modules/royal_render/plugins/publish/collect_sequences_from_job.py +++ b/openpype/modules/default_modules/royal_render/plugins/publish/collect_sequences_from_job.py @@ -192,6 +192,8 @@ class CollectSequencesFromJob(pyblish.api.ContextPlugin): 'name': ext, 'ext': '{}'.format(ext), 'files': list(collection), + "frameStart": start, + "frameEnd": end, "stagingDir": root, "anatomy_template": "render", "fps": fps, diff --git a/poetry.lock b/poetry.lock index c07a20253c..f513b76611 100644 --- a/poetry.lock +++ b/poetry.lock @@ -219,16 +219,16 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "clique" -version = "1.5.0" +version = "1.6.1" description = "Manage collections with common numerical component" category = "main" optional = false -python-versions = "*" +python-versions = ">=2.7, <4.0" [package.extras] -dev = ["lowdown (>=0.1.0,<1)", "pytest (>=2.3.5,<3)", "pytest-cov (>=2,<3)", "pytest-runner (>=2.7,<3)", "sphinx (>=1.2.2,<2)", "sphinx-rtd-theme (>=0.1.6,<1)"] -doc = ["lowdown (>=0.1.0,<1)", "sphinx (>=1.2.2,<2)", "sphinx-rtd-theme (>=0.1.6,<1)"] -test = ["pytest (>=2.3.5,<3)", "pytest-cov (>=2,<3)", "pytest-runner (>=2.7,<3)"] +dev = ["sphinx (>=2,<4)", "sphinx-rtd-theme (>=0.1.6,<1)", "lowdown (>=0.2.0,<1)", "pytest-runner (>=2.7,<3)", "pytest (>=2.3.5,<5)", "pytest-cov (>=2,<3)"] +doc = ["sphinx (>=2,<4)", "sphinx-rtd-theme (>=0.1.6,<1)", "lowdown (>=0.2.0,<1)"] +test = ["pytest-runner (>=2.7,<3)", "pytest (>=2.3.5,<5)", "pytest-cov (>=2,<3)"] [[package]] name = "colorama" @@ -1580,7 +1580,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "3.7.*" -content-hash = "fb6db80d126fe7ef2d1d06d0381b6d11445d6d3e54b33585f6b0a0b6b0b9d372" +content-hash = "877c1c6292735f495d915fc6aa85450eb20fc63f266a9c6bf7ba1125af3579a5" [metadata.files] acre = [] @@ -1749,8 +1749,8 @@ click = [ {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, ] clique = [ - {file = "clique-1.5.0-py2-none-any.whl", hash = "sha256:77efbf5d99a398a50ca4591373def45c9c70fb43232cdc32f521cf5257ce4330"}, - {file = "clique-1.5.0.tar.gz", hash = "sha256:c34a4eac30187a5b7d75bc8cf600ddc50ceef50a423772a4c96f1dc8440af5fa"}, + {file = "clique-1.6.1-py2.py3-none-any.whl", hash = "sha256:8619774fa035661928dd8c93cd805acf2d42533ccea1b536c09815ed426c9858"}, + {file = "clique-1.6.1.tar.gz", hash = "sha256:90165c1cf162d4dd1baef83ceaa1afc886b453e379094fa5b60ea470d1733e66"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -2180,9 +2180,13 @@ protobuf = [ {file = "protobuf-3.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ae692bb6d1992afb6b74348e7bb648a75bb0d3565a3f5eea5bec8f62bd06d87"}, {file = "protobuf-3.17.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:99938f2a2d7ca6563c0ade0c5ca8982264c484fdecf418bd68e880a7ab5730b1"}, {file = "protobuf-3.17.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6902a1e4b7a319ec611a7345ff81b6b004b36b0d2196ce7a748b3493da3d226d"}, + {file = "protobuf-3.17.3-cp38-cp38-win32.whl", hash = "sha256:59e5cf6b737c3a376932fbfb869043415f7c16a0cf176ab30a5bbc419cd709c1"}, + {file = "protobuf-3.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ebcb546f10069b56dc2e3da35e003a02076aaa377caf8530fe9789570984a8d2"}, {file = "protobuf-3.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ffbd23640bb7403574f7aff8368e2aeb2ec9a5c6306580be48ac59a6bac8bde"}, {file = "protobuf-3.17.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:26010f693b675ff5a1d0e1bdb17689b8b716a18709113288fead438703d45539"}, {file = "protobuf-3.17.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e76d9686e088fece2450dbc7ee905f9be904e427341d289acbe9ad00b78ebd47"}, + {file = "protobuf-3.17.3-cp39-cp39-win32.whl", hash = "sha256:a38bac25f51c93e4be4092c88b2568b9f407c27217d3dd23c7a57fa522a17554"}, + {file = "protobuf-3.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:85d6303e4adade2827e43c2b54114d9a6ea547b671cb63fafd5011dc47d0e13d"}, {file = "protobuf-3.17.3-py2.py3-none-any.whl", hash = "sha256:2bfb815216a9cd9faec52b16fd2bfa68437a44b67c56bee59bc3926522ecb04e"}, {file = "protobuf-3.17.3.tar.gz", hash = "sha256:72804ea5eaa9c22a090d2803813e280fb273b62d5ae497aaf3553d141c4fdd7b"}, ] diff --git a/pyproject.toml b/pyproject.toml index 86b40762e6..ece3d4f81b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ opentimelineio = { version = "0.14.0.dev1", source = "openpype" } appdirs = "^1.4.3" blessed = "^1.17" # openpype terminal formatting coolname = "*" -clique = "1.5.*" +clique = "1.6.*" Click = "^7" dnspython = "^2.1.0" ftrack-python-api = "2.0.*" From 6552b576d83854540500fed49ae23e9d26f07110 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Dec 2021 17:01:26 +0100 Subject: [PATCH 186/307] hound suggestions --- .../openpype_flame_to_ftrack/modules/ftrack_lib.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py index 1ceba18e57..26b197ee1d 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py @@ -3,7 +3,6 @@ import sys import six import re import json -from contextlib import contextmanager import app_utils @@ -198,7 +197,6 @@ class FtrackComponentCreator: location.add_component( entity, origin_location, recursive=True) - def _remove_component_from_location(self, entity, location): print(location) # Removing existing members from location @@ -334,8 +332,8 @@ class FtrackComponentCreator: """ queries = [] if sys.version_info[0] < 3: - for key, value in data.iteritems(): - if not isinstance(value, (basestring, int)): + for key, value in data.items(): + if not isinstance(value, (str, int)): print("value: {}".format(value)) if "id" in value.keys(): queries.append( From 4dc9fb847089effdc2b2609c5d5cefdd10333bce Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Dec 2021 17:10:44 +0100 Subject: [PATCH 187/307] hound suggestions --- .../modules/uiwidgets.py | 72 ++++++++++++------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py index c04801da6f..0d4807a4ea 100644 --- a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py +++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py @@ -5,7 +5,8 @@ class FlameLabel(QtWidgets.QLabel): """ Custom Qt Flame Label Widget - For different label looks set label_type as: 'normal', 'background', or 'outline' + For different label looks set label_type as: + 'normal', 'background', or 'outline' To use: @@ -24,23 +25,28 @@ class FlameLabel(QtWidgets.QLabel): # Set label stylesheet based on label_type if label_type == 'normal': - self.setStyleSheet('QLabel {color: #9a9a9a; border-bottom: 1px inset #282828; font: 14px "Discreet"}' - 'QLabel:disabled {color: #6a6a6a}') + self.setStyleSheet( + 'QLabel {color: #9a9a9a; border-bottom: 1px inset #282828; font: 14px "Discreet"}' # noqa + 'QLabel:disabled {color: #6a6a6a}' + ) elif label_type == 'background': self.setAlignment(QtCore.Qt.AlignCenter) self.setStyleSheet( - 'color: #9a9a9a; background-color: #393939; font: 14px "Discreet"') + 'color: #9a9a9a; background-color: #393939; font: 14px "Discreet"' # noqa + ) elif label_type == 'outline': self.setAlignment(QtCore.Qt.AlignCenter) self.setStyleSheet( - 'color: #9a9a9a; background-color: #212121; border: 1px solid #404040; font: 14px "Discreet"') + 'color: #9a9a9a; background-color: #212121; border: 1px solid #404040; font: 14px "Discreet"' # noqa + ) class FlameLineEdit(QtWidgets.QLineEdit): """ Custom Qt Flame Line Edit Widget - Main window should include this: window.setFocusPolicy(QtCore.Qt.StrongFocus) + Main window should include this: + window.setFocusPolicy(QtCore.Qt.StrongFocus) To use: @@ -54,9 +60,11 @@ class FlameLineEdit(QtWidgets.QLineEdit): self.setParent(parent_window) self.setMinimumHeight(28) self.setMinimumWidth(110) - self.setStyleSheet('QLineEdit {color: #9a9a9a; background-color: #373e47; selection-color: #262626; selection-background-color: #b8b1a7; font: 14px "Discreet"}' - 'QLineEdit:focus {background-color: #474e58}' - 'QLineEdit:disabled {color: #6a6a6a; background-color: #373737}') + self.setStyleSheet( + 'QLineEdit {color: #9a9a9a; background-color: #373e47; selection-color: #262626; selection-background-color: #b8b1a7; font: 14px "Discreet"}' # noqa + 'QLineEdit:focus {background-color: #474e58}' # noqa + 'QLineEdit:disabled {color: #6a6a6a; background-color: #373737}' + ) class FlameTreeWidget(QtWidgets.QTreeWidget): @@ -79,11 +87,11 @@ class FlameTreeWidget(QtWidgets.QTreeWidget): self.setAlternatingRowColors(True) self.setFocusPolicy(QtCore.Qt.NoFocus) self.setStyleSheet( - 'QTreeWidget {color: #9a9a9a; background-color: #2a2a2a; alternate-background-color: #2d2d2d; font: 14px "Discreet"}' - 'QTreeWidget::item:selected {color: #d9d9d9; background-color: #474747; border: 1px solid #111111}' - 'QHeaderView {color: #9a9a9a; background-color: #393939; font: 14px "Discreet"}' + 'QTreeWidget {color: #9a9a9a; background-color: #2a2a2a; alternate-background-color: #2d2d2d; font: 14px "Discreet"}' # noqa + 'QTreeWidget::item:selected {color: #d9d9d9; background-color: #474747; border: 1px solid #111111}' # noqa + 'QHeaderView {color: #9a9a9a; background-color: #393939; font: 14px "Discreet"}' # noqa 'QTreeWidget::item:selected {selection-background-color: #111111}' - 'QMenu {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' + 'QMenu {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' # noqa 'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}' ) self.verticalScrollBar().setStyleSheet('color: #818181') @@ -110,9 +118,11 @@ class FlameButton(QtWidgets.QPushButton): self.setMaximumSize(QtCore.QSize(110, 28)) self.setFocusPolicy(QtCore.Qt.NoFocus) self.clicked.connect(do_when_pressed) - self.setStyleSheet('QPushButton {color: #9a9a9a; background-color: #424142; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}' - 'QPushButton:pressed {color: #d9d9d9; background-color: #4f4f4f; border-top: 1px inset #666666; font: italic}' - 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}') + self.setStyleSheet( + 'QPushButton {color: #9a9a9a; background-color: #424142; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}' # noqa + 'QPushButton:pressed {color: #d9d9d9; background-color: #4f4f4f; border-top: 1px inset #666666; font: italic}' # noqa + 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}' # noqa + ) class FlamePushButton(QtWidgets.QPushButton): @@ -135,10 +145,12 @@ class FlamePushButton(QtWidgets.QPushButton): self.setMinimumSize(155, 28) self.setMaximumSize(155, 28) self.setFocusPolicy(QtCore.Qt.NoFocus) - self.setStyleSheet('QPushButton {color: #9a9a9a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #424142, stop: .94 #2e3b48); text-align: left; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}' - 'QPushButton:checked {color: #d9d9d9; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #4f4f4f, stop: .94 #5a7fb4); font: italic; border: 1px inset black; border-bottom: 1px inset #404040; border-right: 1px inset #404040}' - 'QPushButton:disabled {color: #6a6a6a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #383838, stop: .94 #353535); font: light; border-top: 1px solid #575757; border-bottom: 1px solid #242424; border-right: 1px solid #353535; border-left: 1px solid #353535}' - 'QToolTip {color: black; background-color: #ffffde; border: black solid 1px}') + self.setStyleSheet( + 'QPushButton {color: #9a9a9a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #424142, stop: .94 #2e3b48); text-align: left; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}' # noqa + 'QPushButton:checked {color: #d9d9d9; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #4f4f4f, stop: .94 #5a7fb4); font: italic; border: 1px inset black; border-bottom: 1px inset #404040; border-right: 1px inset #404040}' # noqa + 'QPushButton:disabled {color: #6a6a6a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #383838, stop: .94 #353535); font: light; border-top: 1px solid #575757; border-bottom: 1px solid #242424; border-right: 1px solid #353535; border-left: 1px solid #353535}' # noqa + 'QToolTip {color: black; background-color: #ffffde; border: black solid 1px}' # noqa + ) class FlamePushButtonMenu(QtWidgets.QPushButton): @@ -148,12 +160,14 @@ class FlamePushButtonMenu(QtWidgets.QPushButton): To use: push_button_menu_options = ['Item 1', 'Item 2', 'Item 3', 'Item 4'] - menu_push_button = FlamePushButtonMenu('push_button_name', push_button_menu_options, window) + menu_push_button = FlamePushButtonMenu('push_button_name', + push_button_menu_options, window) or push_button_menu_options = ['Item 1', 'Item 2', 'Item 3', 'Item 4'] - menu_push_button = FlamePushButtonMenu(push_button_menu_options[0], push_button_menu_options, window) + menu_push_button = FlamePushButtonMenu(push_button_menu_options[0], + push_button_menu_options, window) """ selection_changed = QtCore.Signal(str) @@ -165,13 +179,17 @@ class FlamePushButtonMenu(QtWidgets.QPushButton): self.setMinimumHeight(28) self.setMinimumWidth(110) self.setFocusPolicy(QtCore.Qt.NoFocus) - self.setStyleSheet('QPushButton {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' - 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}') + self.setStyleSheet( + 'QPushButton {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' # noqa + 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}' # noqa + ) pushbutton_menu = QtWidgets.QMenu(parent_window) pushbutton_menu.setFocusPolicy(QtCore.Qt.NoFocus) - pushbutton_menu.setStyleSheet('QMenu {color: #9a9a9a; background-color:#24303d; font: 14px "Discreet"}' - 'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}') + pushbutton_menu.setStyleSheet( + 'QMenu {color: #9a9a9a; background-color:#24303d; font: 14px "Discreet"}' # noqa + 'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}' + ) self._pushbutton_menu = pushbutton_menu self.setMenu(pushbutton_menu) @@ -191,4 +209,4 @@ class FlamePushButtonMenu(QtWidgets.QPushButton): def _on_action_trigger(self): action = self.sender() self.setText(action.text()) - self.selection_changed.emit(action.text()) \ No newline at end of file + self.selection_changed.emit(action.text()) From 7e1fe023ec8e5d56360aaa5f3985369f104fb728 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 8 Dec 2021 03:41:39 +0000 Subject: [PATCH 188/307] [Automated] Bump version --- CHANGELOG.md | 26 ++++++++++---------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 855c3016f9..4a38bbf7af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.7.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.7.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.4...HEAD) @@ -15,6 +15,7 @@ **🚀 Enhancements** - Settings: Webpublisher in hosts enum [\#2367](https://github.com/pypeclub/OpenPype/pull/2367) +- Hiero: python3 compatibility [\#2365](https://github.com/pypeclub/OpenPype/pull/2365) - Burnins: Be able recognize mxf OPAtom format [\#2361](https://github.com/pypeclub/OpenPype/pull/2361) - Local settings: Copyable studio paths [\#2349](https://github.com/pypeclub/OpenPype/pull/2349) - Assets Widget: Clear model on project change [\#2345](https://github.com/pypeclub/OpenPype/pull/2345) @@ -29,14 +30,16 @@ - General: Reduce vendor imports [\#2305](https://github.com/pypeclub/OpenPype/pull/2305) - Tools: Cleanup of unused classes [\#2304](https://github.com/pypeclub/OpenPype/pull/2304) - Project Manager: Added ability to delete project [\#2298](https://github.com/pypeclub/OpenPype/pull/2298) -- Ftrack: Synchronize input links [\#2287](https://github.com/pypeclub/OpenPype/pull/2287) -- StandalonePublisher: Remove unused plugin ExtractHarmonyZip [\#2277](https://github.com/pypeclub/OpenPype/pull/2277) -- Ftrack: Support multiple reviews [\#2271](https://github.com/pypeclub/OpenPype/pull/2271) -- Royal Render: Support for rr channels in separate dirs [\#2268](https://github.com/pypeclub/OpenPype/pull/2268) +- Nuke: extract baked review videos presets [\#2248](https://github.com/pypeclub/OpenPype/pull/2248) **🐛 Bug fixes** +- Flame: fix ftrack publisher [\#2381](https://github.com/pypeclub/OpenPype/pull/2381) +- hiero: solve custom ocio path [\#2379](https://github.com/pypeclub/OpenPype/pull/2379) +- hiero: fix workio and flatten [\#2378](https://github.com/pypeclub/OpenPype/pull/2378) +- Nuke: fixing menu re-drawing during context change [\#2374](https://github.com/pypeclub/OpenPype/pull/2374) - Webpublisher: Fix assignment of families of TVpaint instances [\#2373](https://github.com/pypeclub/OpenPype/pull/2373) +- Nuke: fixing node name based on switched asset name [\#2369](https://github.com/pypeclub/OpenPype/pull/2369) - JobQueue: Fix loading of settings [\#2362](https://github.com/pypeclub/OpenPype/pull/2362) - Tools: Placeholder color [\#2359](https://github.com/pypeclub/OpenPype/pull/2359) - Launcher: Minimize button on MacOs [\#2355](https://github.com/pypeclub/OpenPype/pull/2355) @@ -56,13 +59,12 @@ - Workfiles tool: Fix task formatting [\#2306](https://github.com/pypeclub/OpenPype/pull/2306) - Delivery: Fix delivery paths created on windows [\#2302](https://github.com/pypeclub/OpenPype/pull/2302) - Maya: Deadline - fix limit groups [\#2295](https://github.com/pypeclub/OpenPype/pull/2295) -- New Publisher: Fix mapping of indexes [\#2285](https://github.com/pypeclub/OpenPype/pull/2285) +- Royal Render: Fix plugin order and OpenPype auto-detection [\#2291](https://github.com/pypeclub/OpenPype/pull/2291) - Alternate site for site sync doesnt work for sequences [\#2284](https://github.com/pypeclub/OpenPype/pull/2284) -- FFmpeg: Execute ffprobe using list of arguments instead of string command [\#2281](https://github.com/pypeclub/OpenPype/pull/2281) -- Nuke: Anatomy fill data use task as dictionary [\#2278](https://github.com/pypeclub/OpenPype/pull/2278) **Merged pull requests:** +- Linux : flip updating submodules logic [\#2357](https://github.com/pypeclub/OpenPype/pull/2357) - Update of avalon-core [\#2346](https://github.com/pypeclub/OpenPype/pull/2346) - Maya: configurable model top level validation [\#2321](https://github.com/pypeclub/OpenPype/pull/2321) @@ -88,13 +90,11 @@ **🚀 Enhancements** -- Ftrack: Remove unused clean component plugin [\#2269](https://github.com/pypeclub/OpenPype/pull/2269) - Tools: Assets widget [\#2265](https://github.com/pypeclub/OpenPype/pull/2265) - SceneInventory: Choose loader in asset switcher [\#2262](https://github.com/pypeclub/OpenPype/pull/2262) - Style: New fonts in OpenPype style [\#2256](https://github.com/pypeclub/OpenPype/pull/2256) - Tools: SceneInventory in OpenPype [\#2255](https://github.com/pypeclub/OpenPype/pull/2255) - Tools: Tasks widget [\#2251](https://github.com/pypeclub/OpenPype/pull/2251) -- Nuke: extract baked review videos presets [\#2248](https://github.com/pypeclub/OpenPype/pull/2248) - Tools: Creator in OpenPype [\#2244](https://github.com/pypeclub/OpenPype/pull/2244) **🐛 Bug fixes** @@ -123,17 +123,11 @@ - Tools: Subset manager in OpenPype [\#2243](https://github.com/pypeclub/OpenPype/pull/2243) - General: Skip module directories without init file [\#2239](https://github.com/pypeclub/OpenPype/pull/2239) -- General: Static interfaces [\#2238](https://github.com/pypeclub/OpenPype/pull/2238) -- Style: Fix transparent image in style [\#2235](https://github.com/pypeclub/OpenPype/pull/2235) **🐛 Bug fixes** - Ftrack: Sync project ftrack id cache issue [\#2250](https://github.com/pypeclub/OpenPype/pull/2250) - Ftrack: Session creation and Prepare project [\#2245](https://github.com/pypeclub/OpenPype/pull/2245) -- Added queue for studio processing in PS [\#2237](https://github.com/pypeclub/OpenPype/pull/2237) -- Python 2: Unicode to string conversion [\#2236](https://github.com/pypeclub/OpenPype/pull/2236) -- Fix - enum for color coding in PS [\#2234](https://github.com/pypeclub/OpenPype/pull/2234) -- Pyblish Tool: Fix targets handling [\#2232](https://github.com/pypeclub/OpenPype/pull/2232) ## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17) diff --git a/openpype/version.py b/openpype/version.py index f58f22be4f..8909c5edac 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.7.0-nightly.5" +__version__ = "3.7.0-nightly.6" diff --git a/pyproject.toml b/pyproject.toml index 033458d436..0b2176d277 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.7.0-nightly.5" # OpenPype +version = "3.7.0-nightly.6" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 23ed14a3973316324e9cac2bdddd2d65ea31ab41 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Dec 2021 11:41:02 +0100 Subject: [PATCH 189/307] OP-2042 - remove superfluous logging --- openpype/lib/path_tools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index 6fd0ad0dfe..9bb0231ca7 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -307,7 +307,6 @@ class HostDirmap: mapping = {} if not project_settings["global"]["sync_server"]["enabled"]: - log.debug("Site Sync not enabled") return mapping from openpype.settings.lib import get_site_local_overrides From 18ea410e299135ede9648f064017a61fdd4e5113 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 8 Dec 2021 12:27:19 +0100 Subject: [PATCH 190/307] adding pr template --- .../PULL_REQUEST_TEMPLATE/feature-tempate.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE/feature-tempate.md diff --git a/.github/PULL_REQUEST_TEMPLATE/feature-tempate.md b/.github/PULL_REQUEST_TEMPLATE/feature-tempate.md new file mode 100644 index 0000000000..0e27c7681c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/feature-tempate.md @@ -0,0 +1,20 @@ +--- +name: Feature +about: PR for feature +title: '' +labels: "type: feature" +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 04689e72c0d1e1a546f737aff18b3c52c221ae90 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 8 Dec 2021 12:42:17 +0100 Subject: [PATCH 191/307] flame: moving `utility_scripts` to api folder also with `scripts` @iLLiCiTiT suggestion --- openpype/hosts/flame/{ => api}/scripts/wiretap_com.py | 0 .../export_preset/openpype_seg_thumbnails_jpg.xml | 0 .../export_preset/openpype_seg_video_h264.xml | 0 .../openpype_flame_to_ftrack/modules/__init__.py | 0 .../openpype_flame_to_ftrack/modules/app_utils.py | 0 .../openpype_flame_to_ftrack/modules/ftrack_lib.py | 0 .../openpype_flame_to_ftrack/modules/panel_app.py | 0 .../openpype_flame_to_ftrack/modules/uiwidgets.py | 0 .../openpype_flame_to_ftrack/openpype_flame_to_ftrack.py | 0 .../hosts/flame/{ => api}/utility_scripts/openpype_in_flame.py | 0 openpype/hosts/flame/api/utils.py | 1 + openpype/hosts/flame/hooks/pre_flame_setup.py | 2 +- 12 files changed, 2 insertions(+), 1 deletion(-) rename openpype/hosts/flame/{ => api}/scripts/wiretap_com.py (100%) rename openpype/hosts/flame/{ => api}/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml (100%) rename openpype/hosts/flame/{ => api}/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml (100%) rename openpype/hosts/flame/{ => api}/utility_scripts/openpype_flame_to_ftrack/modules/__init__.py (100%) rename openpype/hosts/flame/{ => api}/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py (100%) rename openpype/hosts/flame/{ => api}/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py (100%) rename openpype/hosts/flame/{ => api}/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py (100%) rename openpype/hosts/flame/{ => api}/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py (100%) rename openpype/hosts/flame/{ => api}/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py (100%) rename openpype/hosts/flame/{ => api}/utility_scripts/openpype_in_flame.py (100%) diff --git a/openpype/hosts/flame/scripts/wiretap_com.py b/openpype/hosts/flame/api/scripts/wiretap_com.py similarity index 100% rename from openpype/hosts/flame/scripts/wiretap_com.py rename to openpype/hosts/flame/api/scripts/wiretap_com.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml b/openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml rename to openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml b/openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml rename to openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/__init__.py b/openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/__init__.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/__init__.py rename to openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/__init__.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py b/openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py rename to openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py b/openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py rename to openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py b/openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py rename to openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py b/openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py rename to openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py rename to openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_in_flame.py b/openpype/hosts/flame/api/utility_scripts/openpype_in_flame.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_in_flame.py rename to openpype/hosts/flame/api/utility_scripts/openpype_in_flame.py diff --git a/openpype/hosts/flame/api/utils.py b/openpype/hosts/flame/api/utils.py index a750046362..201c7d2fac 100644 --- a/openpype/hosts/flame/api/utils.py +++ b/openpype/hosts/flame/api/utils.py @@ -27,6 +27,7 @@ def _sync_utility_scripts(env=None): fsd_paths = [os.path.join( HOST_DIR, + "api", "utility_scripts" )] diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py index 718c4b574c..159fb37410 100644 --- a/openpype/hosts/flame/hooks/pre_flame_setup.py +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -22,7 +22,7 @@ class FlamePrelaunch(PreLaunchHook): flame_python_exe = "/opt/Autodesk/python/2021/bin/python2.7" wtc_script_path = os.path.join( - opflame.HOST_DIR, "scripts", "wiretap_com.py") + opflame.HOST_DIR, "api", "scripts", "wiretap_com.py") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) From 615f6e65d0d57a2142daaec2da0914d6aaa7e3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 8 Dec 2021 12:48:29 +0100 Subject: [PATCH 192/307] removing unsupported header --- .github/PULL_REQUEST_TEMPLATE/feature-tempate.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE/feature-tempate.md b/.github/PULL_REQUEST_TEMPLATE/feature-tempate.md index 0e27c7681c..6e74a073bd 100644 --- a/.github/PULL_REQUEST_TEMPLATE/feature-tempate.md +++ b/.github/PULL_REQUEST_TEMPLATE/feature-tempate.md @@ -1,12 +1,3 @@ ---- -name: Feature -about: PR for feature -title: '' -labels: "type: feature" -assignees: '' - ---- - **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] From ae8fcc8d9851b23ff88716d8340b2991f9261326 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 8 Dec 2021 12:51:53 +0100 Subject: [PATCH 193/307] pr template: single template only --- .../feature-tempate.md => pull_request_template.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{PULL_REQUEST_TEMPLATE/feature-tempate.md => pull_request_template.md} (100%) diff --git a/.github/PULL_REQUEST_TEMPLATE/feature-tempate.md b/.github/pull_request_template.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE/feature-tempate.md rename to .github/pull_request_template.md From 3287c79e4e988be63bcaf8a3d7a32b667b0b6d30 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 8 Dec 2021 12:57:49 +0100 Subject: [PATCH 194/307] test of pr template header --- .github/pull_request_template.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 6e74a073bd..5f0a04cee3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,11 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' +--- + **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] From b3d2d41267c313a822c186af9b0232f29baad85e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 12:58:37 +0100 Subject: [PATCH 195/307] added working state context to category widget --- openpype/tools/settings/settings/categories.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index a6e4154b2b..79b5de952b 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -1,6 +1,7 @@ import os import sys import traceback +import contextlib from enum import Enum from Qt import QtWidgets, QtCore, QtGui @@ -309,6 +310,12 @@ class SettingsCategoryWidget(QtWidgets.QWidget): ) self.content_layout.addWidget(widget, 0) + @contextlib.contextmanager + def working_state_context(self): + self.set_state(CategoryState.Working) + yield + self.set_state(CategoryState.Idle) + def save(self): if not self.items_are_valid(): return From 347b8da8a7a9a1a6c25d113fa6a2588b55acac04 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 12:59:01 +0100 Subject: [PATCH 196/307] use working_state_context for current actions --- openpype/tools/settings/settings/base.py | 39 ++++++++++++++++-------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index f8378ed18c..1df7b2c75a 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -110,9 +110,10 @@ class BaseWidget(QtWidgets.QWidget): return def discard_changes(): - self.ignore_input_changes.set_ignore(True) - self.entity.discard_changes() - self.ignore_input_changes.set_ignore(False) + with self.category_widget.working_state_context(): + self.ignore_input_changes.set_ignore(True) + self.entity.discard_changes() + self.ignore_input_changes.set_ignore(False) action = QtWidgets.QAction("Discard changes") actions_mapping[action] = discard_changes @@ -124,8 +125,11 @@ class BaseWidget(QtWidgets.QWidget): if not self.entity.can_trigger_add_to_studio_default: return + def add_to_studio_default(): + with self.category_widget.working_state_context(): + self.entity.add_to_studio_default() action = QtWidgets.QAction("Add to studio default") - actions_mapping[action] = self.entity.add_to_studio_default + actions_mapping[action] = add_to_studio_default menu.addAction(action) def _remove_from_studio_default_action(self, menu, actions_mapping): @@ -133,9 +137,10 @@ class BaseWidget(QtWidgets.QWidget): return def remove_from_studio_default(): - self.ignore_input_changes.set_ignore(True) - self.entity.remove_from_studio_default() - self.ignore_input_changes.set_ignore(False) + with self.category_widget.working_state_context(): + self.ignore_input_changes.set_ignore(True) + self.entity.remove_from_studio_default() + self.ignore_input_changes.set_ignore(False) action = QtWidgets.QAction("Remove from studio default") actions_mapping[action] = remove_from_studio_default menu.addAction(action) @@ -144,8 +149,12 @@ class BaseWidget(QtWidgets.QWidget): if not self.entity.can_trigger_add_to_project_override: return + def add_to_project_override(): + with self.category_widget.working_state_context(): + self.entity.add_to_project_override + action = QtWidgets.QAction("Add to project project override") - actions_mapping[action] = self.entity.add_to_project_override + actions_mapping[action] = add_to_project_override menu.addAction(action) def _remove_from_project_override_action(self, menu, actions_mapping): @@ -153,9 +162,11 @@ class BaseWidget(QtWidgets.QWidget): return def remove_from_project_override(): - self.ignore_input_changes.set_ignore(True) - self.entity.remove_from_project_override() - self.ignore_input_changes.set_ignore(False) + with self.category_widget.working_state_context(): + self.ignore_input_changes.set_ignore(True) + self.entity.remove_from_project_override() + self.ignore_input_changes.set_ignore(False) + action = QtWidgets.QAction("Remove from project override") actions_mapping[action] = remove_from_project_override menu.addAction(action) @@ -257,14 +268,16 @@ class BaseWidget(QtWidgets.QWidget): # Simple paste value method def paste_value(): - _set_entity_value(self.entity, value) + with self.category_widget.working_state_context(): + _set_entity_value(self.entity, value) action = QtWidgets.QAction("Paste", menu) output.append((action, paste_value)) # Paste value to matchin entity def paste_value_to_path(): - _set_entity_value(matching_entity, value) + with self.category_widget.working_state_context(): + _set_entity_value(matching_entity, value) if matching_entity is not None: action = QtWidgets.QAction("Paste to same place", menu) From 74b6da96aca81a54e32f8984d865d6f5674c4061 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 12:59:21 +0100 Subject: [PATCH 197/307] added method to return project names from project list --- openpype/tools/settings/settings/widgets.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index ac9870287b..4c7bf87ce8 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -747,6 +747,13 @@ class ProjectListWidget(QtWidgets.QWidget): index, QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent ) + def get_project_names(self): + output = [] + for row in range(self.project_proxy.rowCount()): + index = self.project_proxy.index(row, 0) + output.append(index.data(PROJECT_NAME_ROLE)) + return output + def refresh(self): selected_project = None for index in self.project_list.selectedIndexes(): From e2f24fb87b3b732740649cf7f70fb32784530dcd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 12:59:42 +0100 Subject: [PATCH 198/307] project category widget has ability to retrieve project names --- openpype/tools/settings/settings/categories.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 79b5de952b..029619849e 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -606,6 +606,14 @@ class ProjectWidget(SettingsCategoryWidget): self.project_list_widget = project_list_widget + def get_project_names(self): + if ( + self.modify_defaults_checkbox + and self.modify_defaults_checkbox.isChecked() + ): + return [] + return self.project_list_widget.get_project_names() + def on_saved(self, saved_tab_widget): """Callback on any tab widget save. From 2fdac8639a00e58cb5544a6cdf9822d2bfa3d2c9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 13:00:07 +0100 Subject: [PATCH 199/307] initial version of applying settings from different project --- openpype/tools/settings/settings/base.py | 43 ++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 1df7b2c75a..f03d0c6186 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -1,7 +1,10 @@ import json from Qt import QtWidgets, QtGui, QtCore + +from openpype.settings.entities import ProjectSettings from openpype.tools.settings import CHILD_OFFSET + from .widgets import ExpandingWidget from .lib import create_deffered_value_change_timer @@ -285,6 +288,45 @@ class BaseWidget(QtWidgets.QWidget): return output + def _apply_values_from_project_action(self, menu, actions_mapping): + for attr_name in ("project_name", "get_project_names"): + if not hasattr(self.category_widget, attr_name): + return + + if self.entity.is_dynamic_item or self.entity.is_in_dynamic_item: + return + + submenu = QtWidgets.QMenu("Apply values from", menu) + + current_project_name = self.category_widget.project_name + for project_name in self.category_widget.get_project_names(): + if current_project_name == project_name: + continue + + if project_name is None: + project_name = "< Default >" + + action = QtWidgets.QAction(project_name) + submenu.addAction(action) + actions_mapping[action] = lambda: self._apply_values_from_project( + project_name + ) + menu.addMenu(submenu) + + def _apply_values_from_project(self, project_name): + with self.category_widget.working_state_context(): + path_keys = [ + item + for item in self.entity.path.split("/") + if item + ] + + settings = ProjectSettings(project_name) + entity = settings + for key in path_keys: + entity = entity[key] + self.entity.set(entity.value) + def show_actions_menu(self, event=None): if event and event.button() != QtCore.Qt.RightButton: return @@ -303,6 +345,7 @@ class BaseWidget(QtWidgets.QWidget): self._remove_from_studio_default_action(menu, actions_mapping) self._add_to_project_override_action(menu, actions_mapping) self._remove_from_project_override_action(menu, actions_mapping) + self._apply_values_from_project_action(menu, actions_mapping) ui_actions = [] ui_actions.extend(self._copy_value_actions(menu)) From c08b4673fd5fd935f41b5277c8977e80aaeb595b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 13:07:38 +0100 Subject: [PATCH 200/307] use defined constant for default project label --- openpype/tools/settings/settings/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index f03d0c6186..94687f4f35 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -7,6 +7,7 @@ from openpype.tools.settings import CHILD_OFFSET from .widgets import ExpandingWidget from .lib import create_deffered_value_change_timer +from .constants import DEFAULT_PROJECT_LABEL class BaseWidget(QtWidgets.QWidget): @@ -304,7 +305,7 @@ class BaseWidget(QtWidgets.QWidget): continue if project_name is None: - project_name = "< Default >" + project_name = DEFAULT_PROJECT_LABEL action = QtWidgets.QAction(project_name) submenu.addAction(action) From 8270f2fc402a7cb119fdc71ead0bd474f0d7c9d6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 13:07:57 +0100 Subject: [PATCH 201/307] catch exceptions happened during applying values from different project --- openpype/tools/settings/settings/base.py | 40 ++++++++++++++++++------ 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 94687f4f35..6776845125 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -1,4 +1,6 @@ +import sys import json +import traceback from Qt import QtWidgets, QtGui, QtCore @@ -316,17 +318,35 @@ class BaseWidget(QtWidgets.QWidget): def _apply_values_from_project(self, project_name): with self.category_widget.working_state_context(): - path_keys = [ - item - for item in self.entity.path.split("/") - if item - ] + try: + path_keys = [ + item + for item in self.entity.path.split("/") + if item + ] + entity = ProjectSettings(project_name) + for key in path_keys: + entity = entity[key] + self.entity.set(entity.value) - settings = ProjectSettings(project_name) - entity = settings - for key in path_keys: - entity = entity[key] - self.entity.set(entity.value) + except Exception: + if project_name is None: + project_name = DEFAULT_PROJECT_LABEL + + # TODO better message + title = "Applying values failed" + msg = "Using values from project \"{}\" failed.".format( + project_name + ) + detail_msg = "".join( + traceback.format_exception(*sys.exc_info()) + ) + dialog = QtWidgets.QMessageBox(self) + dialog.setWindowTitle(title) + dialog.setIcon(QtWidgets.QMessageBox.Warning) + dialog.setText(msg) + dialog.setDetailedText(detail_msg) + dialog.exec_() def show_actions_menu(self, event=None): if event and event.button() != QtCore.Qt.RightButton: From 8e71919145a1ad491511697486238e8ebc82221e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 13:25:22 +0100 Subject: [PATCH 202/307] skip action if does not have any projects which can be used as source for values --- openpype/tools/settings/settings/base.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 6776845125..9016e63970 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -299,13 +299,18 @@ class BaseWidget(QtWidgets.QWidget): if self.entity.is_dynamic_item or self.entity.is_in_dynamic_item: return + current_project_name = self.category_widget.project_name + project_names = [] + for project_name in self.category_widget.get_project_names(): + if project_name != current_project_name: + project_names.append(project_name) + + if not project_names: + return + submenu = QtWidgets.QMenu("Apply values from", menu) - current_project_name = self.category_widget.project_name - for project_name in self.category_widget.get_project_names(): - if current_project_name == project_name: - continue - + for project_name in project_names: if project_name is None: project_name = DEFAULT_PROJECT_LABEL From 5fb526721b7c5348274a14a7de494983738bfb4d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 8 Dec 2021 15:30:20 +0100 Subject: [PATCH 203/307] improving PR template --- .github/pull_request_template.md | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5f0a04cee3..c5789d798f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,19 +1,16 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: enhancement -assignees: '' ---- +## Brief description +First sentence is brief description. -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] +## Description +Next paragraf is more elaborate text with more info. This will be displayed for example in collapsed form under the first sentence in a changelog. -**Describe the solution you'd like** -A clear and concise description of what you want to happen. +## Additional info +The rest will be ignored in changelog and should contain any additional +technical information. -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. +## Documentation (add Documentation label) +[feature_documentation](future_url_after_it_will_be_merged) -**Additional context** -Add any other context or screenshots about the feature request here. +## Testing notes: +1. start with this step +2. follow this step \ No newline at end of file From 6e127d273cd0a598c37c9a8011200b557b81d2cd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 8 Dec 2021 15:33:00 +0100 Subject: [PATCH 204/307] better documentation category --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c5789d798f..20ae298f70 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,7 +8,7 @@ Next paragraf is more elaborate text with more info. This will be displayed for The rest will be ignored in changelog and should contain any additional technical information. -## Documentation (add Documentation label) +## Documentation (add _"type: documentation"_ label) [feature_documentation](future_url_after_it_will_be_merged) ## Testing notes: From 6ed4db4da11ae598c8e7ebf3fbf94dc434cfcd47 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 8 Dec 2021 17:11:45 +0100 Subject: [PATCH 205/307] installand copy xcb libs to pyside2 inside openpype --- Dockerfile.centos7 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile.centos7 b/Dockerfile.centos7 index f3b257e66b..ce60ea7fb1 100644 --- a/Dockerfile.centos7 +++ b/Dockerfile.centos7 @@ -41,6 +41,8 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n ncurses \ ncurses-devel \ qt5-qtbase-devel \ + xcb-util-wm \ + xcb-util-renderutil && yum clean all # we need to build our own patchelf @@ -92,7 +94,8 @@ RUN source $HOME/.bashrc \ RUN cp /usr/lib64/libffi* ./build/exe.linux-x86_64-3.7/lib \ && cp /usr/lib64/libssl* ./build/exe.linux-x86_64-3.7/lib \ && cp /usr/lib64/libcrypto* ./build/exe.linux-x86_64-3.7/lib \ - && cp /root/.pyenv/versions/${OPENPYPE_PYTHON_VERSION}/lib/libpython* ./build/exe.linux-x86_64-3.7/lib + && cp /root/.pyenv/versions/${OPENPYPE_PYTHON_VERSION}/lib/libpython* ./build/exe.linux-x86_64-3.7/lib \ + && cp /usr/lib64/libxcb* ./build/exe.linux-x86_64-3.7/vendor/python/PySide2/Qt/lib RUN cd /opt/openpype \ rm -rf ./vendor/bin From 9e2760c52e5e0a8964e2ed3f6c1bcb44976408eb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Dec 2021 18:10:46 +0100 Subject: [PATCH 206/307] OP-2042 - better handling of reusing deployed workfile --- .../hosts/nuke/test_publish_in_nuke.py | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/tests/integration/hosts/nuke/test_publish_in_nuke.py b/tests/integration/hosts/nuke/test_publish_in_nuke.py index d603b9f177..092fd7d1c6 100644 --- a/tests/integration/hosts/nuke/test_publish_in_nuke.py +++ b/tests/integration/hosts/nuke/test_publish_in_nuke.py @@ -1,6 +1,7 @@ import pytest import os import logging +import shutil from tests.lib.testing_classes import PublishTest @@ -23,6 +24,8 @@ class TestPublishInNuke(PublishTest): (in cmd with activated {OPENPYPE_ROOT}/.venv) {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/nuke # noqa: E501 + To check log/errors from launched app's publish process keep PERSIST + to True and check `test_openpype.logs` collection. """ PERSIST = True # True - keep test_db, test_openpype, outputted test files @@ -36,21 +39,32 @@ class TestPublishInNuke(PublishTest): TIMEOUT = 120 # publish timeout - TEST_DATA_FOLDER = None # provide existing folder with test data + TEST_DATA_FOLDER = "C:\\Users\\petrk\\AppData\\Local\\Temp\\tmpbfh976y6" # provide existing folder with test data @pytest.fixture(scope="module") def last_workfile_path(self, download_test_data): """Get last_workfile_path from source data. """ - log.info("log last_workfile_path") - src_path = os.path.join( - download_test_data, - "input", - "workfile", - "test_project_test_asset_CompositingInNuke_v001.nk") + source_file_name = "test_project_test_asset_CompositingInNuke_v001.nk" + src_path = os.path.join(download_test_data, + "input", + "workfile", + source_file_name) + dest_folder = os.path.join(download_test_data, + self.PROJECT, + self.ASSET, + "work", + self.TASK) + if not os.path.exists(dest_folder): + os.makedirs(dest_folder) - yield src_path + dest_path = os.path.join(dest_folder, + source_file_name) + + shutil.copy(src_path, dest_path) + + yield dest_path @pytest.fixture(scope="module") def startup_scripts(self, monkeypatch_session, download_test_data): @@ -68,9 +82,9 @@ class TestPublishInNuke(PublishTest): """Host and input data dependent expected results in DB.""" print("test_db_asserts") versions = dbcon.count_documents({"type": "version"}) - assert 5 == versions, \ + assert 2 == versions, \ "Not expected no of versions. "\ - "Expected 5, found {}".format(versions) + "Expected 2, found {}".format(versions) assert 0 == dbcon.count_documents({"type": "version", "name": {"$ne": 1}}), \ From a3638ef70ffc2c6d6bed9b8bf0126736f59b35fe Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Dec 2021 18:12:01 +0100 Subject: [PATCH 207/307] OP-2042 - injection of TEST_SOURCE_FOLDER It tells Nuke where it should locate test input data --- tests/lib/testing_classes.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 0a04dc59c2..25ad66cdfb 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -105,9 +105,6 @@ class ModuleUnitTest(BaseTest): value = value.format(**all_vars) print("Setting {}:{}".format(key, value)) monkeypatch_session.setenv(key, str(value)) - import openpype - - openpype_root = os.path.dirname(os.path.dirname(openpype.__file__)) #reset connection to openpype DB with new env var import openpype.settings.lib as sett_lib @@ -116,10 +113,16 @@ class ModuleUnitTest(BaseTest): sett_lib.create_settings_handler() sett_lib.create_local_settings_handler() + import openpype + openpype_root = os.path.dirname(os.path.dirname(openpype.__file__)) + # ?? why 2 of those monkeypatch_session.setenv("OPENPYPE_ROOT", openpype_root) monkeypatch_session.setenv("OPENPYPE_REPOS_ROOT", openpype_root) + # for remapping purposes (currently in Nuke) + monkeypatch_session.setenv("TEST_SOURCE_FOLDER", download_test_data) + @pytest.fixture(scope="module") def db_setup(self, download_test_data, env_var, monkeypatch_session): """Restore prepared MongoDB dumps into selected DB.""" @@ -271,7 +274,8 @@ class PublishTest(ModuleUnitTest): app_name = "{}/{}".format(self.APP, variant) - yield application_manager.launch(app_name, **data) + app_process = application_manager.launch(app_name, **data) + yield app_process @pytest.fixture(scope="module") def publish_finished(self, dbcon, launched_app, download_test_data): From e680a362b778943f9cfe80f18395c69c8c5ca8b2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Dec 2021 18:13:08 +0100 Subject: [PATCH 208/307] OP-2042 - additions to developer documentation --- tests/integration/README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/integration/README.md b/tests/integration/README.md index 8839e2e43f..0b6a1804ae 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -15,6 +15,10 @@ How to run OR can use built executables `openpype_console runtests {ABS_PATH}/tests/integration` +How to check logs/errors from app +-------------------------------- +Keep PERSIST to True in the class and check `test_openpype.logs` collection. + How to create test for publishing from host ------------------------------------------ - Extend PublishTest in `tests/lib/testing_classes.py` @@ -28,9 +32,21 @@ How to create test for publishing from host ``` import openpype from avalon import api, HOST + +from openpype.api import Logger + +log = Logger().get_logger(__name__) api.install(HOST) -pyblish.util.publish() +log_lines = [] +for result in pyblish.util.publish_iter(): + for record in result["records"]: # for logging to test_openpype DB + log_lines.append("{}: {}".format( + result["plugin"].label, record.msg)) + + if result["error"]: + err_fmt = "Failed {plugin.__name__}: {error} -- {error.traceback}" + log.error(err_fmt.format(**result)) EXIT_APP (command to exit host) ``` From 97404abf43adc4ebb38afa1e93403e7d16776b97 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Dec 2021 19:00:12 +0100 Subject: [PATCH 209/307] OP-2053 - added check of installed extension for PS --- .../publish/collect_extension_version.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 openpype/hosts/photoshop/plugins/publish/collect_extension_version.py diff --git a/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py b/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py new file mode 100644 index 0000000000..f07ff0b0ff --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py @@ -0,0 +1,57 @@ +import os +import re +import pyblish.api + +from avalon import photoshop + + +class CollectExtensionVersion(pyblish.api.ContextPlugin): + """ Pulls and compares version of installed extension. + + It is recommended to use same extension as in provided Openpype code. + + Please use Anastasiy’s Extension Manager or ZXPInstaller to update + extension in case of an error. + + You can locate extension.zxp in your installed Openpype code in + `repos/avalon-core/avalon/photoshop` + """ + # This technically should be a validator, but other collectors might be + # impacted with usage of obsolete extension, so collector that runs first + # was chosen + order = pyblish.api.CollectorOrder - 0.5 + label = "Collect extension version" + hosts = ["photoshop"] + + optional = True + active = True + + def process(self, context): + installed_version = photoshop.stub().get_extension_version() + + if not installed_version: + raise ValueError("Unknown version, probably old extension") + + manifest_url = os.path.join(os.path.dirname(photoshop.__file__), + "extension", "CSXS", "manifest.xml") + + if not os.path.exists(manifest_url): + self.log.debug("Unable to locate extension manifest, not checking") + return + + expected_version = None + with open(manifest_url) as fp: + content = fp.read() + + found = re.findall(r'(ExtensionBundleVersion=")([0-10\.]+)(")', + content) + if found: + expected_version = found[0][1] + + if expected_version != installed_version: + msg = "Expected version '{}' found '{}'\n".format( + expected_version, installed_version) + msg += "Please update your installed extension, it might not work " + msg += "properly." + + raise ValueError(msg) From 5dc2fd05885718cb6c90083f14f60de18cc2c828 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Dec 2021 19:00:44 +0100 Subject: [PATCH 210/307] OP-2053 - bump down order of collector for current file It depends on valid extension --- .../hosts/photoshop/plugins/publish/collect_current_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_current_file.py b/openpype/hosts/photoshop/plugins/publish/collect_current_file.py index 3cc3e3f636..4d4829555e 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_current_file.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_current_file.py @@ -8,7 +8,7 @@ from avalon import photoshop class CollectCurrentFile(pyblish.api.ContextPlugin): """Inject the current working file into context""" - order = pyblish.api.CollectorOrder - 0.5 + order = pyblish.api.CollectorOrder - 0.49 label = "Current File" hosts = ["photoshop"] From 2673b587731576c30b26f6ec045c3cbff91f0fa3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Dec 2021 14:01:28 +0100 Subject: [PATCH 211/307] OP-2053 - added possibility to check installed extension version --- .../plugins/publish/collect_current_file.py | 2 +- .../publish/collect_extension_version.py | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/aftereffects/plugins/publish/collect_extension_version.py diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_current_file.py b/openpype/hosts/aftereffects/plugins/publish/collect_current_file.py index b59ff41a0e..51f6f5c844 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_current_file.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_current_file.py @@ -8,7 +8,7 @@ from avalon import aftereffects class CollectCurrentFile(pyblish.api.ContextPlugin): """Inject the current working file into context""" - order = pyblish.api.CollectorOrder - 0.5 + order = pyblish.api.CollectorOrder - 0.49 label = "Current File" hosts = ["aftereffects"] diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_extension_version.py b/openpype/hosts/aftereffects/plugins/publish/collect_extension_version.py new file mode 100644 index 0000000000..3352fd21f0 --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/publish/collect_extension_version.py @@ -0,0 +1,56 @@ +import os +import re +import pyblish.api + +from avalon import aftereffects + + +class CollectExtensionVersion(pyblish.api.ContextPlugin): + """ Pulls and compares version of installed extension. + + It is recommended to use same extension as in provided Openpype code. + + Please use Anastasiy’s Extension Manager or ZXPInstaller to update + extension in case of an error. + + You can locate extension.zxp in your installed Openpype code in + `repos/avalon-core/avalon/aftereffects` + """ + # This technically should be a validator, but other collectors might be + # impacted with usage of obsolete extension, so collector that runs first + # was chosen + order = pyblish.api.CollectorOrder - 0.5 + label = "Collect extension version" + hosts = ["aftereffects"] + + optional = True + active = True + + def process(self, context): + installed_version = aftereffects.stub().get_extension_version() + + if not installed_version: + raise ValueError("Unknown version, probably old extension") + + manifest_url = os.path.join(os.path.dirname(aftereffects.__file__), + "extension", "CSXS", "manifest.xml") + + if not os.path.exists(manifest_url): + self.log.debug("Unable to locate extension manifest, not checking") + return + + expected_version = None + with open(manifest_url) as fp: + content = fp.read() + found = re.findall(r'(ExtensionBundleVersion=")([0-9\.]+)(")', + content) + if found: + expected_version = found[0][1] + + if expected_version != installed_version: + msg = "Expected version '{}' found '{}'\n".format( + expected_version, installed_version) + msg += "Please update your installed extension, it might not work " + msg += "properly." + + raise ValueError(msg) From a80ed0deecc0440fcbc779d4c855f5c6713db958 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Dec 2021 14:25:24 +0100 Subject: [PATCH 212/307] OP-2053 - Hound --- openpype/hosts/aftereffects/plugins/publish/closeAE.py | 2 -- .../hosts/aftereffects/test_publish_in_aftereffects.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/closeAE.py b/openpype/hosts/aftereffects/plugins/publish/closeAE.py index e6e9623474..21bedf0125 100644 --- a/openpype/hosts/aftereffects/plugins/publish/closeAE.py +++ b/openpype/hosts/aftereffects/plugins/publish/closeAE.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- """Close AE after publish. For Webpublishing only.""" -import os - import pyblish.api from avalon import aftereffects diff --git a/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py b/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py index 3d1fa8f804..e0f6b3e48e 100644 --- a/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py +++ b/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py @@ -92,7 +92,7 @@ class TestPublishInAfterEffects(PublishTest): "Not expected no of representations" assert 1 == dbcon.count_documents({"type": "representation", - "context.subset": "imageMainBackgroundcopy", #noqa E501 + "context.subset": "imageMainBackgroundcopy", # noqa E501 "context.ext": "png"}), \ "Not expected no of representations with ext 'png'" From 2f2116b4bda283fa9b8e20464a97fb8a3dd25fc7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Dec 2021 18:30:43 +0100 Subject: [PATCH 213/307] OP-2042 - added test_data_folder to command line --- openpype/cli.py | 8 ++++++-- openpype/pype_commands.py | 18 +++++++++--------- tests/integration/conftest.py | 12 ++++++++++++ tests/lib/testing_classes.py | 8 ++++---- 4 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 tests/integration/conftest.py diff --git a/openpype/cli.py b/openpype/cli.py index 4c4dc1a3c6..1f444006ca 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -356,9 +356,13 @@ def run(script): "--pyargs", help="Run tests from package", default=None) -def runtests(folder, mark, pyargs): +@click.option("-t", + "--test_data_folder", + help="Unzipped directory path of test file", + default=None) +def runtests(folder, mark, pyargs, test_data_folder): """Run all automatic tests after proper initialization via start.py""" - PypeCommands().run_tests(folder, mark, pyargs) + PypeCommands().run_tests(folder, mark, pyargs, test_data_folder) @main.command() diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index f01c6f0e42..7b3c799b3c 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -341,7 +341,7 @@ class PypeCommands: def validate_jsons(self): pass - def run_tests(self, folder, mark, pyargs): + def run_tests(self, folder, mark, pyargs, test_data_folder): """ Runs tests from 'folder' @@ -349,26 +349,26 @@ class PypeCommands: folder (str): relative path to folder with tests mark (str): label to run tests marked by it (slow etc) pyargs (str): package path to test + test_data_folder (str): url to unzipped folder of test data """ print("run_tests") - import subprocess - if folder: folder = " ".join(list(folder)) else: folder = "../tests" - mark_str = pyargs_str = '' + # disable warnings and show captured stdout even if success + args = ["--disable-pytest-warnings", "-rP", folder] + if mark: - mark_str = "-m {}".format(mark) + args.extend(["-m", mark]) if pyargs: - pyargs_str = "--pyargs {}".format(pyargs) + args.extend(["--pyargs", pyargs]) - # disable warnings and show captured stdout even if success - args = ["--disable-pytest-warnings", "-rP"] + if test_data_folder: + args.extend(["--test_data_folder", test_data_folder]) - args += [folder, mark_str, pyargs_str] print("run_tests args: {}".format(args)) import pytest pytest.main(args) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 0000000000..bc002e8f86 --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +import pytest + +def pytest_addoption(parser): + parser.addoption( + "--test_data_folder", action="store", default=None, + help="Provide url of a folder of unzipped test file" + ) + +@pytest.fixture(scope="module") +def test_data_folder(request): + return request.config.getoption("--test_data_folder") \ No newline at end of file diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 25ad66cdfb..541a92d15d 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -58,10 +58,10 @@ class ModuleUnitTest(BaseTest): m.undo() @pytest.fixture(scope="module") - def download_test_data(self): - if self.TEST_DATA_FOLDER: - print("Using existing folder {}".format(self.TEST_DATA_FOLDER)) - yield self.TEST_DATA_FOLDER + def download_test_data(self, test_data_folder): + if test_data_folder: + print("Using existing folder {}".format(test_data_folder)) + yield test_data_folder else: tmpdir = tempfile.mkdtemp() for test_file in self.TEST_FILES: From 53bbae2cbd1d19b25b6acbde3a0195aec58bfd2a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Dec 2021 18:34:40 +0100 Subject: [PATCH 214/307] OP-2042 - better error message in validator --- .../hosts/nuke/plugins/publish/validate_rendered_frames.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py index 29faf867d2..af5e8e9d27 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py +++ b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py @@ -67,7 +67,9 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): if not repre.get("files"): msg = ("no frames were collected, " - "you need to render them") + "you need to render them.\n" + "Check properties of write node (group) and" + "select 'Local' option in 'Publish' dropdown.") self.log.error(msg) raise ValidationException(msg) From 06a8f5014023ec1c792263fc66eb82c15159f908 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 10 Dec 2021 13:46:08 +0100 Subject: [PATCH 215/307] OP-2042 - evaluate paths in write nodes Paths in write nodes could contain python code for automatic testing. It needs to be evaluated to all os operations to work properly. --- .../nuke/plugins/publish/extract_render_local.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_render_local.py b/openpype/hosts/nuke/plugins/publish/extract_render_local.py index bc7b41c733..50a5d01483 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_render_local.py +++ b/openpype/hosts/nuke/plugins/publish/extract_render_local.py @@ -42,10 +42,14 @@ class NukeRenderLocal(openpype.api.Extractor): self.log.info("Start frame: {}".format(first_frame)) self.log.info("End frame: {}".format(last_frame)) + # write node url might contain nuke's ctl expressin + # as [python ...]/path... + path = node["file"].evaluate() + # Ensure output directory exists. - directory = os.path.dirname(node["file"].value()) - if not os.path.exists(directory): - os.makedirs(directory) + out_dir = os.path.dirname(path) + if not os.path.exists(out_dir): + os.makedirs(out_dir) # Render frames nuke.execute( @@ -58,15 +62,12 @@ class NukeRenderLocal(openpype.api.Extractor): if "slate" in families: first_frame += 1 - path = node['file'].value() - out_dir = os.path.dirname(path) ext = node["file_type"].value() if "representations" not in instance.data: instance.data["representations"] = [] collected_frames = os.listdir(out_dir) - if len(collected_frames) == 1: repre = { 'name': ext, From 00d9681d56e381169e47f6561a065ca9d2e4ea88 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 10 Dec 2021 13:47:55 +0100 Subject: [PATCH 216/307] OP-2042 - better cleanup of test DBs before start of test --- tests/lib/db_handler.py | 14 +++++++++++--- tests/lib/testing_classes.py | 6 ++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/lib/db_handler.py b/tests/lib/db_handler.py index 0aa4c69ca6..b181055012 100644 --- a/tests/lib/db_handler.py +++ b/tests/lib/db_handler.py @@ -112,9 +112,17 @@ class DBHandler: source 'db_name' """ db_name_out = db_name_out or db_name - if self._db_exists(db_name) and not overwrite: - raise RuntimeError("DB {} already exists".format(db_name_out) + - "Run with overwrite=True") + if self._db_exists(db_name_out): + if not overwrite: + raise RuntimeError("DB {} already exists".format(db_name_out) + + "Run with overwrite=True") + else: + if collection: + coll = self.client[db_name_out].get(collection) + if coll: + coll.drop() + else: + self.teardown(db_name_out) dir_path = os.path.join(dump_dir, db_name) if not os.path.exists(dir_path): diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 541a92d15d..96e7148bff 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -130,10 +130,12 @@ class ModuleUnitTest(BaseTest): uri = os.environ.get("OPENPYPE_MONGO") db_handler = DBHandler(uri) - db_handler.setup_from_dump(self.TEST_DB_NAME, backup_dir, True, + db_handler.setup_from_dump(self.TEST_DB_NAME, backup_dir, + overwrite=True, db_name_out=self.TEST_DB_NAME) - db_handler.setup_from_dump("openpype", backup_dir, True, + db_handler.setup_from_dump("openpype", backup_dir, + overwrite=True, db_name_out=self.TEST_OPENPYPE_NAME) yield db_handler From 6e9c9c087cd5047c5f4d7e32b6019ce6a782df3d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 10 Dec 2021 15:04:10 +0100 Subject: [PATCH 217/307] OP-2042 - adding persist, app_variant to cli --- openpype/cli.py | 13 +++++- openpype/pype_commands.py | 15 ++++++- tests/integration/conftest.py | 25 ++++++++++- .../hosts/nuke/test_publish_in_nuke.py | 15 ++++--- tests/lib/testing_classes.py | 45 ++++++++++++------- 5 files changed, 85 insertions(+), 28 deletions(-) diff --git a/openpype/cli.py b/openpype/cli.py index 1f444006ca..6b20fb5203 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -360,9 +360,18 @@ def run(script): "--test_data_folder", help="Unzipped directory path of test file", default=None) -def runtests(folder, mark, pyargs, test_data_folder): +@click.option("-s", + "--persist", + help="Persist test DB and published files after test end", + default=None) +@click.option("-a", + "--app_variant", + help="Provide specific app variant for test, empty for latest", + default=None) +def runtests(folder, mark, pyargs, test_data_folder, persist, app_variant): """Run all automatic tests after proper initialization via start.py""" - PypeCommands().run_tests(folder, mark, pyargs, test_data_folder) + PypeCommands().run_tests(folder, mark, pyargs, test_data_folder, + persist, app_variant) @main.command() diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 7b3c799b3c..a6330bae1f 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -341,7 +341,8 @@ class PypeCommands: def validate_jsons(self): pass - def run_tests(self, folder, mark, pyargs, test_data_folder): + def run_tests(self, folder, mark, pyargs, + test_data_folder, persist, app_variant): """ Runs tests from 'folder' @@ -350,6 +351,10 @@ class PypeCommands: mark (str): label to run tests marked by it (slow etc) pyargs (str): package path to test test_data_folder (str): url to unzipped folder of test data + persist (bool): True if keep test db and published after test + end + app_variant (str): variant (eg 2020 for AE), empty if use + latest installed version """ print("run_tests") if folder: @@ -366,9 +371,15 @@ class PypeCommands: if pyargs: args.extend(["--pyargs", pyargs]) - if test_data_folder: + if persist: args.extend(["--test_data_folder", test_data_folder]) + if persist: + args.extend(["--persist", persist]) + + if app_variant: + args.extend(["--app_variant", app_variant]) + print("run_tests args: {}".format(args)) import pytest pytest.main(args) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index bc002e8f86..400c0dcc2a 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,12 +1,35 @@ # -*- coding: utf-8 -*- +# adds command line arguments for 'runtests' as a fixtures import pytest + def pytest_addoption(parser): parser.addoption( "--test_data_folder", action="store", default=None, help="Provide url of a folder of unzipped test file" ) + parser.addoption( + "--persist", action="store", default=None, + help="True - keep test_db, test_openpype, outputted test files" + ) + + parser.addoption( + "--app_variant", action="store", default=None, + help="Keep empty to locate latest installed variant or explicit" + ) + + @pytest.fixture(scope="module") def test_data_folder(request): - return request.config.getoption("--test_data_folder") \ No newline at end of file + return request.config.getoption("--test_data_folder") + + +@pytest.fixture(scope="module") +def persist(request): + return request.config.getoption("--persist") + + +@pytest.fixture(scope="module") +def app_variant(request): + return request.config.getoption("--app_variant") diff --git a/tests/integration/hosts/nuke/test_publish_in_nuke.py b/tests/integration/hosts/nuke/test_publish_in_nuke.py index 092fd7d1c6..a5a09bdd04 100644 --- a/tests/integration/hosts/nuke/test_publish_in_nuke.py +++ b/tests/integration/hosts/nuke/test_publish_in_nuke.py @@ -27,22 +27,23 @@ class TestPublishInNuke(PublishTest): To check log/errors from launched app's publish process keep PERSIST to True and check `test_openpype.logs` collection. """ - PERSIST = True # True - keep test_db, test_openpype, outputted test files - + # https://drive.google.com/file/d/1SUurHj2aiQ21ZIMJfGVBI2KjR8kIjBGI/view?usp=sharing # noqa: E501 TEST_FILES = [ ("1SUurHj2aiQ21ZIMJfGVBI2KjR8kIjBGI", "test_Nuke_publish.zip", "") ] APP = "nuke" - # keep empty to locate latest installed variant or explicit - APP_VARIANT = "" TIMEOUT = 120 # publish timeout - TEST_DATA_FOLDER = "C:\\Users\\petrk\\AppData\\Local\\Temp\\tmpbfh976y6" # provide existing folder with test data + # could be overwritten by command line arguments + # keep empty to locate latest installed variant or explicit + APP_VARIANT = "" + PERSIST = True # True - keep test_db, test_openpype, outputted test files + TEST_DATA_FOLDER = None @pytest.fixture(scope="module") - def last_workfile_path(self, download_test_data): + def last_workfile_path(self, download_test_data, output_folder_url): """Get last_workfile_path from source data. """ @@ -99,7 +100,7 @@ class TestPublishInNuke(PublishTest): "name": "workfileTest_task"}), \ "workfileTest_task subset must be present" - assert 10 == dbcon.count_documents({"type": "representation"}), \ + assert 4 == dbcon.count_documents({"type": "representation"}), \ "Not expected no of representations" reprs = dbcon.count_documents({"type": "representation", diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 96e7148bff..40363e928f 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -7,6 +7,7 @@ import pytest import tempfile import shutil import glob +import platform from tests.lib.db_handler import DBHandler from tests.lib.file_handler import RemoteFileHandler @@ -58,7 +59,8 @@ class ModuleUnitTest(BaseTest): m.undo() @pytest.fixture(scope="module") - def download_test_data(self, test_data_folder): + def download_test_data(self, test_data_folder, persist=False): + test_data_folder = test_data_folder or self.TEST_DATA_FOLDER if test_data_folder: print("Using existing folder {}".format(test_data_folder)) yield test_data_folder @@ -78,7 +80,8 @@ class ModuleUnitTest(BaseTest): print("Temporary folder created:: {}".format(tmpdir)) yield tmpdir - if not self.PERSIST: + persist = persist or self.PERSIST + if not persist: print("Removing {}".format(tmpdir)) shutil.rmtree(tmpdir) @@ -188,14 +191,28 @@ class PublishTest(ModuleUnitTest): """ APP = "" - APP_VARIANT = "" # keep empty to locate latest installed variant TIMEOUT = 120 # publish timeout - @property - def app_name(self): - if self.APP_VARIANT: - return "{}/{}".format(self.APP, self.APP_VARIANT) + # could be overwritten by command line arguments + # command line value takes precedence + + # keep empty to locate latest installed variant or explicit + APP_VARIANT = "" + PERSIST = True # True - keep test_db, test_openpype, outputted test files + TEST_DATA_FOLDER = None # use specific folder of unzipped test file + + @pytest.fixture(scope="module") + def app_name(self, app_variant): + """Returns calculated value for ApplicationManager. Eg.(nuke/12-2)""" + from openpype.lib import ApplicationManager + app_variant = app_variant or self.APP_VARIANT + + application_manager = ApplicationManager() + if not app_variant: + app_variant = find_variant_key(application_manager, self.APP) + + yield "{}/{}".format(self.APP, app_variant) @pytest.fixture(scope="module") def last_workfile_path(self, download_test_data): @@ -203,6 +220,7 @@ class PublishTest(ModuleUnitTest): @pytest.fixture(scope="module") def startup_scripts(self, monkeypatch_session, download_test_data): + """"Adds init scripts (like userSetup) to expected location""" raise NotImplementedError @pytest.fixture(scope="module") @@ -270,12 +288,6 @@ class PublishTest(ModuleUnitTest): if app_args: data["app_args"] = app_args - variant = self.APP_VARIANT - if not variant: - variant = find_variant_key(application_manager, self.APP) - - app_name = "{}/{}".format(self.APP, variant) - app_process = application_manager.launch(app_name, **data) yield app_process @@ -295,13 +307,13 @@ class PublishTest(ModuleUnitTest): yield True def test_folder_structure_same(self, dbcon, publish_finished, - download_test_data): + download_test_data, output_folder_url): """Check if expected and published subfolders contain same files. Compares only presence, not size nor content! """ published_dir_base = download_test_data - published_dir = os.path.join(published_dir_base, + published_dir = os.path.join(output_folder_url, self.PROJECT, self.TASK, "**") @@ -311,7 +323,8 @@ class PublishTest(ModuleUnitTest): self.PROJECT, self.TASK, "**") - + print("Comparing published:'{}' : expected:'{}'".format(published_dir, + expected_dir)) published = set(f.replace(published_dir_base, '') for f in glob.glob(published_dir, recursive=True) if f != published_dir_base and os.path.exists(f)) From 9a0d55e2f3c6e07d1ccfffd3cd1f5f04669b15b7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 10 Dec 2021 15:08:13 +0100 Subject: [PATCH 218/307] OP-2042 - added new fixture output_folder_url Explicitly sets directory of published files. (Purges them if exist!) --- .../hosts/maya/test_publish_in_maya.py | 4 ++-- .../hosts/nuke/test_publish_in_nuke.py | 2 +- .../photoshop/test_publish_in_photoshop.py | 4 ++-- tests/lib/testing_classes.py | 19 +++++++++++++++---- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/tests/integration/hosts/maya/test_publish_in_maya.py b/tests/integration/hosts/maya/test_publish_in_maya.py index 687e6fbc6e..5f3a550c6a 100644 --- a/tests/integration/hosts/maya/test_publish_in_maya.py +++ b/tests/integration/hosts/maya/test_publish_in_maya.py @@ -39,7 +39,7 @@ class TestPublishInMaya(PublishTest): TIMEOUT = 120 # publish timeout @pytest.fixture(scope="module") - def last_workfile_path(self, download_test_data): + def last_workfile_path(self, download_test_data, output_folder_url): """Get last_workfile_path from source data. Maya expects workfile in proper folder, so copy is done first. @@ -48,7 +48,7 @@ class TestPublishInMaya(PublishTest): "input", "workfile", "test_project_test_asset_TestTask_v001.mb") - dest_folder = os.path.join(download_test_data, + dest_folder = os.path.join(output_folder_url, self.PROJECT, self.ASSET, "work", diff --git a/tests/integration/hosts/nuke/test_publish_in_nuke.py b/tests/integration/hosts/nuke/test_publish_in_nuke.py index a5a09bdd04..797bc0a9d3 100644 --- a/tests/integration/hosts/nuke/test_publish_in_nuke.py +++ b/tests/integration/hosts/nuke/test_publish_in_nuke.py @@ -52,7 +52,7 @@ class TestPublishInNuke(PublishTest): "input", "workfile", source_file_name) - dest_folder = os.path.join(download_test_data, + dest_folder = os.path.join(output_folder_url, self.PROJECT, self.ASSET, "work", diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py index c7f2399494..541552fedf 100644 --- a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py @@ -37,7 +37,7 @@ class TestPublishInPhotoshop(PublishTest): TIMEOUT = 120 # publish timeout @pytest.fixture(scope="module") - def last_workfile_path(self, download_test_data): + def last_workfile_path(self, download_test_data, output_folder_url): """Get last_workfile_path from source data. Maya expects workfile in proper folder, so copy is done first. @@ -46,7 +46,7 @@ class TestPublishInPhotoshop(PublishTest): "input", "workfile", "test_project_test_asset_TestTask_v001.psd") - dest_folder = os.path.join(download_test_data, + dest_folder = os.path.join(output_folder_url, self.PROJECT, self.ASSET, "work", diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 40363e928f..ad637e6974 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -215,7 +215,17 @@ class PublishTest(ModuleUnitTest): yield "{}/{}".format(self.APP, app_variant) @pytest.fixture(scope="module") - def last_workfile_path(self, download_test_data): + def output_folder_url(self, download_test_data): + """Returns location of published data, cleans it first if exists.""" + path = os.path.join(download_test_data, "output") + if os.path.exists(path): + print("Purging {}".format(path)) + shutil.rmtree(path) + yield path + + @pytest.fixture(scope="module") + def last_workfile_path(self, download_test_data, output_folder_url): + """Returns url of workfile""" raise NotImplementedError @pytest.fixture(scope="module") @@ -251,15 +261,16 @@ class PublishTest(ModuleUnitTest): @pytest.fixture(scope="module") def launched_app(self, dbcon, download_test_data, last_workfile_path, - startup_scripts, app_args): + startup_scripts, app_args, app_name, output_folder_url): """Launch host app""" # set publishing folders - root_key = "config.roots.work.{}".format("windows") # TEMP + platform_str = platform.system().lower() + root_key = "config.roots.work.{}".format(platform_str) dbcon.update_one( {"type": "project"}, {"$set": { - root_key: download_test_data + root_key: output_folder_url }} ) From 09bbebae18bca1da746229963bc45cf2f48f8d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20LORRAIN?= Date: Fri, 10 Dec 2021 15:14:03 +0100 Subject: [PATCH 219/307] Add settings to repair --- .../publish/validate_render_image_rule.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py index a912431b55..642ca9e25d 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -23,11 +23,7 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): def process(self, instance): - default_render_file = instance.context.data.get('project_settings')\ - .get('maya') \ - .get('create') \ - .get('CreateRender') \ - .get('default_render_image_folder') + default_render_file = self.get_default_render_image_folder(instance) assert get_file_rule("images") == default_render_file, ( "Workspace's `images` file rule must be set to: {}".format( @@ -37,5 +33,14 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): - pm.workspace.fileRules["images"] = "renders" + default = cls.get_default_render_image_folder(instance) + pm.workspace.fileRules["images"] = default pm.system.Workspace.save() + + @staticmethod + def get_default_render_image_folder(instance): + return instance.context.data.get('project_settings')\ + .get('maya') \ + .get('create') \ + .get('CreateRender') \ + .get('default_render_image_folder') From c754521d0aa5ba0bed5baab997e3278224d7a602 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 10 Dec 2021 16:40:23 +0100 Subject: [PATCH 220/307] OP-2042 - added new handling of asserts Added DBAssert class which wraps standard use cases for asserts --- .../hosts/nuke/test_publish_in_nuke.py | 43 +++++++++--------- tests/lib/assert_classes.py | 45 +++++++++++++++++++ 2 files changed, 66 insertions(+), 22 deletions(-) create mode 100644 tests/lib/assert_classes.py diff --git a/tests/integration/hosts/nuke/test_publish_in_nuke.py b/tests/integration/hosts/nuke/test_publish_in_nuke.py index 797bc0a9d3..6b1088206a 100644 --- a/tests/integration/hosts/nuke/test_publish_in_nuke.py +++ b/tests/integration/hosts/nuke/test_publish_in_nuke.py @@ -4,6 +4,7 @@ import logging import shutil from tests.lib.testing_classes import PublishTest +from tests.lib.assert_classes import DBAssert log = logging.getLogger("test_publish_in_nuke") @@ -82,33 +83,31 @@ class TestPublishInNuke(PublishTest): def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" print("test_db_asserts") - versions = dbcon.count_documents({"type": "version"}) - assert 2 == versions, \ - "Not expected no of versions. "\ - "Expected 2, found {}".format(versions) + failures = [] - assert 0 == dbcon.count_documents({"type": "version", - "name": {"$ne": 1}}), \ - "Only versions with 1 expected" + failures.append(DBAssert.count_of_types(dbcon, "version", 2)) - assert 1 == dbcon.count_documents({"type": "subset", - "name": "renderCompositingInNukeMain"} # noqa: E501 - ), \ - "renderCompositingInNukeMain subset must be present" + failures.append( + DBAssert.count_of_types(dbcon, "version", 0, name={"$ne": 1})) - assert 1 == dbcon.count_documents({"type": "subset", - "name": "workfileTest_task"}), \ - "workfileTest_task subset must be present" + failures.append( + DBAssert.count_of_types(dbcon, "subset", 1, + name="renderCompositingInNukeMain")) - assert 4 == dbcon.count_documents({"type": "representation"}), \ - "Not expected no of representations" + failures.append( + DBAssert.count_of_types(dbcon, "subset", 1, + name="workfileTest_task")) - reprs = dbcon.count_documents({"type": "representation", - "context.subset": "renderCompositingInNukeMain", # noqa: E501 - "context.ext": "exr"}) - assert 1 == reprs, \ - "Not expected no of representations with ext 'exr'."\ - "Expected 1, found {}".format(reprs) + failures.append( + DBAssert.count_of_types(dbcon, "representation", 4)) + + additional_args = {"context.subset": "renderCompositingInNukeMain", + "context.ext": "exr"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + assert not any(failures) if __name__ == "__main__": diff --git a/tests/lib/assert_classes.py b/tests/lib/assert_classes.py new file mode 100644 index 0000000000..7298853b67 --- /dev/null +++ b/tests/lib/assert_classes.py @@ -0,0 +1,45 @@ +"""Classed and methods for comparing expected and published items in DBs""" + +class DBAssert: + + @classmethod + def count_of_types(cls, dbcon, queried_type, expected, **kwargs): + """Queries 'dbcon' and counts documents of type 'queried_type' + + Args: + dbcon (AvalonMongoDB) + queried_type (str): type of document ("asset", "version"...) + expected (int): number of documents found + any number of additional keyword arguments + + special handling of argument additional_args (dict) + with additional args like + {"context.subset": "XXX"} + """ + args = {"type": queried_type} + for key, val in kwargs.items(): + if key == "additional_args": + args.update(val) + else: + args[key] = val + + msg = None + no_of_docs = dbcon.count_documents(args) + if expected != no_of_docs: + msg = "Not expected no of versions. "\ + "Expected {}, found {}".format(expected, no_of_docs) + + args.pop("type") + detail_str = " " + if args: + detail_str = " with {}".format(args) + + status = "successful" + if msg: + status = "failed" + + print("Comparing count of {}{} {}".format(queried_type, + detail_str, + status)) + + return msg From ec15b482dbfd38a0a4c18831f38009c900156632 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 10 Dec 2021 17:25:37 +0100 Subject: [PATCH 221/307] OP-2042 - added additional class wrapper per host --- tests/integration/hosts/maya/lib.py | 41 +++++++++++++++++ .../hosts/maya/test_publish_in_maya.py | 42 +----------------- tests/integration/hosts/nuke/lib.py | 44 +++++++++++++++++++ .../hosts/nuke/test_publish_in_nuke.py | 44 +------------------ tests/integration/hosts/photoshop/lib.py | 34 ++++++++++++++ .../photoshop/test_publish_in_photoshop.py | 35 +-------------- tests/lib/testing_classes.py | 23 +++++----- 7 files changed, 138 insertions(+), 125 deletions(-) create mode 100644 tests/integration/hosts/maya/lib.py create mode 100644 tests/integration/hosts/nuke/lib.py create mode 100644 tests/integration/hosts/photoshop/lib.py diff --git a/tests/integration/hosts/maya/lib.py b/tests/integration/hosts/maya/lib.py new file mode 100644 index 0000000000..f3a438c065 --- /dev/null +++ b/tests/integration/hosts/maya/lib.py @@ -0,0 +1,41 @@ +import os +import pytest +import shutil + +from tests.lib.testing_classes import HostFixtures + + +class MayaTestClass(HostFixtures): + @pytest.fixture(scope="module") + def last_workfile_path(self, download_test_data, output_folder_url): + """Get last_workfile_path from source data. + + Maya expects workfile in proper folder, so copy is done first. + """ + src_path = os.path.join(download_test_data, + "input", + "workfile", + "test_project_test_asset_TestTask_v001.mb") + dest_folder = os.path.join(output_folder_url, + self.PROJECT, + self.ASSET, + "work", + self.TASK) + os.makedirs(dest_folder) + dest_path = os.path.join(dest_folder, + "test_project_test_asset_TestTask_v001.mb") + shutil.copy(src_path, dest_path) + + yield dest_path + + @pytest.fixture(scope="module") + def startup_scripts(self, monkeypatch_session, download_test_data): + """Points Maya to userSetup file from input data""" + startup_path = os.path.join(download_test_data, + "input", + "startup") + original_pythonpath = os.environ.get("PYTHONPATH") + monkeypatch_session.setenv("PYTHONPATH", + "{}{}{}".format(startup_path, + os.pathsep, + original_pythonpath)) diff --git a/tests/integration/hosts/maya/test_publish_in_maya.py b/tests/integration/hosts/maya/test_publish_in_maya.py index 5f3a550c6a..68b0564428 100644 --- a/tests/integration/hosts/maya/test_publish_in_maya.py +++ b/tests/integration/hosts/maya/test_publish_in_maya.py @@ -1,11 +1,7 @@ -import pytest -import os -import shutil - -from tests.lib.testing_classes import PublishTest +from tests.integration.hosts.maya.lib import MayaTestClass -class TestPublishInMaya(PublishTest): +class TestPublishInMaya(MayaTestClass): """Basic test case for publishing in Maya Shouldnt be running standalone only via 'runtests' pype command! (??) @@ -38,40 +34,6 @@ class TestPublishInMaya(PublishTest): TIMEOUT = 120 # publish timeout - @pytest.fixture(scope="module") - def last_workfile_path(self, download_test_data, output_folder_url): - """Get last_workfile_path from source data. - - Maya expects workfile in proper folder, so copy is done first. - """ - src_path = os.path.join(download_test_data, - "input", - "workfile", - "test_project_test_asset_TestTask_v001.mb") - dest_folder = os.path.join(output_folder_url, - self.PROJECT, - self.ASSET, - "work", - self.TASK) - os.makedirs(dest_folder) - dest_path = os.path.join(dest_folder, - "test_project_test_asset_TestTask_v001.mb") - shutil.copy(src_path, dest_path) - - yield dest_path - - @pytest.fixture(scope="module") - def startup_scripts(self, monkeypatch_session, download_test_data): - """Points Maya to userSetup file from input data""" - startup_path = os.path.join(download_test_data, - "input", - "startup") - original_pythonpath = os.environ.get("PYTHONPATH") - monkeypatch_session.setenv("PYTHONPATH", - "{}{}{}".format(startup_path, - os.pathsep, - original_pythonpath)) - def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" print("test_db_asserts") diff --git a/tests/integration/hosts/nuke/lib.py b/tests/integration/hosts/nuke/lib.py new file mode 100644 index 0000000000..d3c3d7ba81 --- /dev/null +++ b/tests/integration/hosts/nuke/lib.py @@ -0,0 +1,44 @@ +import os +import pytest +import shutil + +from tests.lib.testing_classes import HostFixtures + + +class NukeTestClass(HostFixtures): + @pytest.fixture(scope="module") + def last_workfile_path(self, download_test_data, output_folder_url): + """Get last_workfile_path from source data. + + """ + source_file_name = "test_project_test_asset_CompositingInNuke_v001.nk" + src_path = os.path.join(download_test_data, + "input", + "workfile", + source_file_name) + dest_folder = os.path.join(output_folder_url, + self.PROJECT, + self.ASSET, + "work", + self.TASK) + if not os.path.exists(dest_folder): + os.makedirs(dest_folder) + + dest_path = os.path.join(dest_folder, + source_file_name) + + shutil.copy(src_path, dest_path) + + yield dest_path + + @pytest.fixture(scope="module") + def startup_scripts(self, monkeypatch_session, download_test_data): + """Points Nuke to userSetup file from input data""" + startup_path = os.path.join(download_test_data, + "input", + "startup") + original_nuke_path = os.environ.get("NUKE_PATH", "") + monkeypatch_session.setenv("NUKE_PATH", + "{}{}{}".format(startup_path, + os.pathsep, + original_nuke_path)) \ No newline at end of file diff --git a/tests/integration/hosts/nuke/test_publish_in_nuke.py b/tests/integration/hosts/nuke/test_publish_in_nuke.py index 6b1088206a..884160e0b5 100644 --- a/tests/integration/hosts/nuke/test_publish_in_nuke.py +++ b/tests/integration/hosts/nuke/test_publish_in_nuke.py @@ -1,15 +1,12 @@ -import pytest -import os import logging -import shutil -from tests.lib.testing_classes import PublishTest from tests.lib.assert_classes import DBAssert +from tests.integration.hosts.nuke.lib import NukeTestClass log = logging.getLogger("test_publish_in_nuke") -class TestPublishInNuke(PublishTest): +class TestPublishInNuke(NukeTestClass): """Basic test case for publishing in Nuke Uses generic TestCase to prepare fixtures for test data, testing DBs, @@ -43,43 +40,6 @@ class TestPublishInNuke(PublishTest): PERSIST = True # True - keep test_db, test_openpype, outputted test files TEST_DATA_FOLDER = None - @pytest.fixture(scope="module") - def last_workfile_path(self, download_test_data, output_folder_url): - """Get last_workfile_path from source data. - - """ - source_file_name = "test_project_test_asset_CompositingInNuke_v001.nk" - src_path = os.path.join(download_test_data, - "input", - "workfile", - source_file_name) - dest_folder = os.path.join(output_folder_url, - self.PROJECT, - self.ASSET, - "work", - self.TASK) - if not os.path.exists(dest_folder): - os.makedirs(dest_folder) - - dest_path = os.path.join(dest_folder, - source_file_name) - - shutil.copy(src_path, dest_path) - - yield dest_path - - @pytest.fixture(scope="module") - def startup_scripts(self, monkeypatch_session, download_test_data): - """Points Nuke to userSetup file from input data""" - startup_path = os.path.join(download_test_data, - "input", - "startup") - original_nuke_path = os.environ.get("NUKE_PATH", "") - monkeypatch_session.setenv("NUKE_PATH", - "{}{}{}".format(startup_path, - os.pathsep, - original_nuke_path)) - def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" print("test_db_asserts") diff --git a/tests/integration/hosts/photoshop/lib.py b/tests/integration/hosts/photoshop/lib.py new file mode 100644 index 0000000000..16ef2d3ae6 --- /dev/null +++ b/tests/integration/hosts/photoshop/lib.py @@ -0,0 +1,34 @@ +import os +import pytest +import shutil + +from tests.lib.testing_classes import HostFixtures + + +class PhotoshopTestClass(HostFixtures): + @pytest.fixture(scope="module") + def last_workfile_path(self, download_test_data, output_folder_url): + """Get last_workfile_path from source data. + + Maya expects workfile in proper folder, so copy is done first. + """ + src_path = os.path.join(download_test_data, + "input", + "workfile", + "test_project_test_asset_TestTask_v001.psd") + dest_folder = os.path.join(output_folder_url, + self.PROJECT, + self.ASSET, + "work", + self.TASK) + os.makedirs(dest_folder) + dest_path = os.path.join(dest_folder, + "test_project_test_asset_TestTask_v001.psd") + shutil.copy(src_path, dest_path) + + yield dest_path + + @pytest.fixture(scope="module") + def startup_scripts(self, monkeypatch_session, download_test_data): + """Points Maya to userSetup file from input data""" + pass diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py index 541552fedf..ab07577b4a 100644 --- a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py @@ -1,11 +1,7 @@ -import pytest -import os -import shutil - -from tests.lib.testing_classes import PublishTest +from tests.integration.hosts.photoshop.lib import PhotoshopTestClass -class TestPublishInPhotoshop(PublishTest): +class TestPublishInPhotoshop(PhotoshopTestClass): """Basic test case for publishing in Photoshop Uses generic TestCase to prepare fixtures for test data, testing DBs, @@ -36,33 +32,6 @@ class TestPublishInPhotoshop(PublishTest): TIMEOUT = 120 # publish timeout - @pytest.fixture(scope="module") - def last_workfile_path(self, download_test_data, output_folder_url): - """Get last_workfile_path from source data. - - Maya expects workfile in proper folder, so copy is done first. - """ - src_path = os.path.join(download_test_data, - "input", - "workfile", - "test_project_test_asset_TestTask_v001.psd") - dest_folder = os.path.join(output_folder_url, - self.PROJECT, - self.ASSET, - "work", - self.TASK) - os.makedirs(dest_folder) - dest_path = os.path.join(dest_folder, - "test_project_test_asset_TestTask_v001.psd") - shutil.copy(src_path, dest_path) - - yield dest_path - - @pytest.fixture(scope="module") - def startup_scripts(self, monkeypatch_session, download_test_data): - """Points Maya to userSetup file from input data""" - pass - def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" print("test_db_asserts") diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index ad637e6974..06922abc01 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -223,16 +223,6 @@ class PublishTest(ModuleUnitTest): shutil.rmtree(path) yield path - @pytest.fixture(scope="module") - def last_workfile_path(self, download_test_data, output_folder_url): - """Returns url of workfile""" - raise NotImplementedError - - @pytest.fixture(scope="module") - def startup_scripts(self, monkeypatch_session, download_test_data): - """"Adds init scripts (like userSetup) to expected location""" - raise NotImplementedError - @pytest.fixture(scope="module") def app_args(self, download_test_data): """Returns additional application arguments from a test file. @@ -345,3 +335,16 @@ class PublishTest(ModuleUnitTest): not_matched = expected.difference(published) assert not not_matched, "Missing {} files".format(not_matched) + + +class HostFixtures(PublishTest): + """Host specific fixtures. Should be implemented once per host.""" + @pytest.fixture(scope="module") + def last_workfile_path(self, download_test_data, output_folder_url): + """Returns url of workfile""" + raise NotImplementedError + + @pytest.fixture(scope="module") + def startup_scripts(self, monkeypatch_session, download_test_data): + """"Adds init scripts (like userSetup) to expected location""" + raise NotImplementedError \ No newline at end of file From ea034447ed88285f52e1d7466bdfa4d26b82bed3 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 11 Dec 2021 03:41:14 +0000 Subject: [PATCH 222/307] [Automated] Bump version --- CHANGELOG.md | 6 +++--- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a38bbf7af..860e26c59c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.7.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.7.0-nightly.7](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.4...HEAD) @@ -30,6 +30,7 @@ - General: Reduce vendor imports [\#2305](https://github.com/pypeclub/OpenPype/pull/2305) - Tools: Cleanup of unused classes [\#2304](https://github.com/pypeclub/OpenPype/pull/2304) - Project Manager: Added ability to delete project [\#2298](https://github.com/pypeclub/OpenPype/pull/2298) +- Ftrack: Synchronize input links [\#2287](https://github.com/pypeclub/OpenPype/pull/2287) - Nuke: extract baked review videos presets [\#2248](https://github.com/pypeclub/OpenPype/pull/2248) **🐛 Bug fixes** @@ -93,7 +94,6 @@ - Tools: Assets widget [\#2265](https://github.com/pypeclub/OpenPype/pull/2265) - SceneInventory: Choose loader in asset switcher [\#2262](https://github.com/pypeclub/OpenPype/pull/2262) - Style: New fonts in OpenPype style [\#2256](https://github.com/pypeclub/OpenPype/pull/2256) -- Tools: SceneInventory in OpenPype [\#2255](https://github.com/pypeclub/OpenPype/pull/2255) - Tools: Tasks widget [\#2251](https://github.com/pypeclub/OpenPype/pull/2251) - Tools: Creator in OpenPype [\#2244](https://github.com/pypeclub/OpenPype/pull/2244) @@ -121,8 +121,8 @@ **🚀 Enhancements** +- Tools: SceneInventory in OpenPype [\#2255](https://github.com/pypeclub/OpenPype/pull/2255) - Tools: Subset manager in OpenPype [\#2243](https://github.com/pypeclub/OpenPype/pull/2243) -- General: Skip module directories without init file [\#2239](https://github.com/pypeclub/OpenPype/pull/2239) **🐛 Bug fixes** diff --git a/openpype/version.py b/openpype/version.py index 8909c5edac..95cd7a285f 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.7.0-nightly.6" +__version__ = "3.7.0-nightly.7" diff --git a/pyproject.toml b/pyproject.toml index 0b2176d277..d994569fa7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.7.0-nightly.6" # OpenPype +version = "3.7.0-nightly.7" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From c78d4d89a4a36b7987b0ba7d7fc175a282cf0593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20LORRAIN?= Date: Mon, 13 Dec 2021 10:45:30 +0100 Subject: [PATCH 223/307] Handle message type attribute --- openpype/hosts/maya/plugins/publish/collect_look.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 20a9d4ca12..0ab278772e 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -489,6 +489,8 @@ class CollectLook(pyblish.api.InstancePlugin): if not cmds.attributeQuery(attr, node=node, exists=True): continue attribute = "{}.{}".format(node, attr) + if cmds.getAttr(attribute, type=True) == "message": + continue node_attributes[attr] = cmds.getAttr(attribute) attributes.append({"name": node, From daa8eb532a2428d93376cf67a591c89319f9ac9d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 13 Dec 2021 11:49:19 +0100 Subject: [PATCH 224/307] OP-2042 - fix tested output path --- tests/lib/testing_classes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 06922abc01..fa467acf9c 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -316,12 +316,14 @@ class PublishTest(ModuleUnitTest): published_dir_base = download_test_data published_dir = os.path.join(output_folder_url, self.PROJECT, + self.ASSET, self.TASK, "**") expected_dir_base = os.path.join(published_dir_base, "expected") expected_dir = os.path.join(expected_dir_base, self.PROJECT, + self.ASSET, self.TASK, "**") print("Comparing published:'{}' : expected:'{}'".format(published_dir, From 165b5b0b0bb335511d70d68e8db4f2b6b8d4679b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20LORRAIN?= Date: Mon, 13 Dec 2021 11:59:43 +0100 Subject: [PATCH 225/307] Add zero padding to otio burnins --- openpype/scripts/otio_burnin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index 68f4728bc7..156550aab1 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -342,7 +342,8 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): if frame_start is None: replacement_final = replacement_size = str(MISSING_KEY_VALUE) else: - replacement_final = "%{eif:n+" + str(frame_start) + ":d}" + replacement_final = "%{eif:n+" + str(frame_start) + ":d:" + \ + str(len(str(frame_end))) + "}" replacement_size = str(frame_end) final_text = final_text.replace( From fed3013703b944142bc5f084b23b0705fbd0aaa0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Dec 2021 12:37:33 +0100 Subject: [PATCH 226/307] fix changed state of wrapper label --- openpype/style/style.css | 57 +++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 19245cdc40..81aa70ea62 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1044,16 +1044,45 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { color: {color:settings:label-fg}; } #SettingsLabel:hover {color: {color:settings:label-fg-hover};} -#SettingsLabel[state="studio"] {color: {color:settings:studio-light};} -#SettingsLabel[state="studio"]:hover {color: {color:settings:studio-label-hover};} -#SettingsLabel[state="modified"] {color: {color:settings:modified-mid};} -#SettingsLabel[state="modified"]:hover {color: {color:settings:modified-light};} -#SettingsLabel[state="overriden-modified"] {color: {color:settings:modified-mid};} -#SettingsLabel[state="overriden-modified"]:hover {color: {color:settings:modified-light};} -#SettingsLabel[state="overriden"] {color: {color:settings:project-mid};} -#SettingsLabel[state="overriden"]:hover {color: {color:settings:project-light};} -#SettingsLabel[state="invalid"] {color:{color:settings:invalid-dark};} -#SettingsLabel[state="invalid"]:hover {color: {color:settings:invalid-dark};} + +#ExpandLabel { + font-weight: bold; + color: {color:settings:label-fg}; +} +#ExpandLabel:hover { + color: {color:settings:label-fg-hover}; +} + +#ExpandLabel[state="studio"], #SettingsLabel[state="studio"] { + color: {color:settings:studio-light}; +} +#ExpandLabel[state="studio"]:hover, #SettingsLabel[state="studio"]:hover { + color: {color:settings:studio-label-hover}; +} +#ExpandLabel[state="modified"], #SettingsLabel[state="modified"] { + color: {color:settings:modified-mid}; +} +#ExpandLabel[state="modified"]:hover, #SettingsLabel[state="modified"]:hover { + color: {color:settings:modified-light}; +} +#ExpandLabel[state="overriden-modified"], #SettingsLabel[state="overriden-modified"] { + color: {color:settings:modified-mid}; +} +#ExpandLabel[state="overriden-modified"]:hover, #SettingsLabel[state="overriden-modified"]:hover { + color: {color:settings:modified-light}; +} +#ExpandLabel[state="overriden"], #SettingsLabel[state="overriden"] {color: { + color:settings:project-mid}; +} +#ExpandLabel[state="overriden"]:hover, #SettingsLabel[state="overriden"]:hover { + color: {color:settings:project-light}; +} +#ExpandLabel[state="invalid"], #SettingsLabel[state="invalid"] { + color:{color:settings:invalid-dark}; +} +#ExpandLabel[state="invalid"]:hover, #SettingsLabel[state="invalid"]:hover { + color: {color:settings:invalid-dark}; +} /* TODO Replace these with explicit widget types if possible */ #SettingsMainWidget QWidget[input-state="modified"] { @@ -1085,14 +1114,6 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { #DictKey[state="modified"] {border-color: {color:settings:modified-mid};} #DictKey[state="invalid"] {border-color: {color:settings:invalid-dark};} -#ExpandLabel { - font-weight: bold; - color: {color:settings:label-fg}; -} -#ExpandLabel:hover { - color: {color:settings:label-fg-hover}; -} - #ContentWidget { background-color: transparent; } From 590f19e96f0b6a903cb760157a50ed55f0a782c3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Dec 2021 12:44:37 +0100 Subject: [PATCH 227/307] fixed typo --- openpype/style/style.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 81aa70ea62..4159fe1676 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1071,8 +1071,8 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { #ExpandLabel[state="overriden-modified"]:hover, #SettingsLabel[state="overriden-modified"]:hover { color: {color:settings:modified-light}; } -#ExpandLabel[state="overriden"], #SettingsLabel[state="overriden"] {color: { - color:settings:project-mid}; +#ExpandLabel[state="overriden"], #SettingsLabel[state="overriden"] { + color: {color:settings:project-mid}; } #ExpandLabel[state="overriden"]:hover, #SettingsLabel[state="overriden"]:hover { color: {color:settings:project-light}; From c4106bf95a9e604c760c1c449ef456c884b27f40 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Dec 2021 14:01:28 +0100 Subject: [PATCH 228/307] set placeholder font color on initialization instead of after show --- openpype/tools/utils/widgets.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index 009c1dc506..3bfa092a21 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -12,22 +12,17 @@ class PlaceholderLineEdit(QtWidgets.QLineEdit): """Set placeholder color of QLineEdit in Qt 5.12 and higher.""" def __init__(self, *args, **kwargs): super(PlaceholderLineEdit, self).__init__(*args, **kwargs) - self._first_show = True - - def showEvent(self, event): - super(PlaceholderLineEdit, self).showEvent(event) - if self._first_show: - self._first_show = False + # Change placeholder palette color + if hasattr(QtGui.QPalette, "PlaceholderText"): filter_palette = self.palette() - if hasattr(filter_palette, "PlaceholderText"): - color_obj = get_objected_colors()["font"] - color = color_obj.get_qcolor() - color.setAlpha(67) - filter_palette.setColor( - filter_palette.PlaceholderText, - color - ) - self.setPalette(filter_palette) + color_obj = get_objected_colors()["font"] + color = color_obj.get_qcolor() + color.setAlpha(67) + filter_palette.setColor( + QtGui.QPalette.PlaceholderText, + color + ) + self.setPalette(filter_palette) class ImageButton(QtWidgets.QPushButton): From 0b6e2f337aa94e0aa35beb17937a2d7a49fd889b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 6 Dec 2021 12:24:02 +0100 Subject: [PATCH 229/307] modified version regex for installation of PySide2 into blender --- openpype/hosts/blender/hooks/pre_pyside_install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/hooks/pre_pyside_install.py b/openpype/hosts/blender/hooks/pre_pyside_install.py index 6d253300d9..e2a419c8ef 100644 --- a/openpype/hosts/blender/hooks/pre_pyside_install.py +++ b/openpype/hosts/blender/hooks/pre_pyside_install.py @@ -32,7 +32,7 @@ class InstallPySideToBlender(PreLaunchHook): def inner_execute(self): # Get blender's python directory - version_regex = re.compile(r"^2\.[0-9]{2}$") + version_regex = re.compile(r"^[2-3]\.[0-9]+$") executable = self.launch_context.executable.executable_path if os.path.basename(executable).lower() != "blender.exe": From 818740c359b336925fe4b8e662ca610c75fbfe75 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Dec 2021 19:23:59 +0100 Subject: [PATCH 230/307] use default components in log mongo --- openpype/lib/log.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/openpype/lib/log.py b/openpype/lib/log.py index 85cbc733ba..a34cb898e3 100644 --- a/openpype/lib/log.py +++ b/openpype/lib/log.py @@ -27,7 +27,7 @@ import copy from . import Terminal from .mongo import ( MongoEnvNotSet, - decompose_url, + get_default_components, OpenPypeMongoConnection ) try: @@ -202,10 +202,6 @@ class PypeLogger: use_mongo_logging = None mongo_process_id = None - # Information about mongo url - log_mongo_url = None - log_mongo_url_components = None - # Database name in Mongo log_database_name = os.environ["OPENPYPE_DATABASE_NAME"] # Collection name under database in Mongo @@ -282,9 +278,9 @@ class PypeLogger: if not cls.use_mongo_logging: return - components = cls.log_mongo_url_components + components = get_default_components() kwargs = { - "host": cls.log_mongo_url, + "host": components["host"], "database_name": cls.log_database_name, "collection": cls.log_collection_name, "username": components["username"], @@ -354,14 +350,8 @@ class PypeLogger: # Define if is in OPENPYPE_DEBUG mode cls.pype_debug = int(os.getenv("OPENPYPE_DEBUG") or "0") - # Mongo URL where logs will be stored - cls.log_mongo_url = os.environ.get("OPENPYPE_MONGO") - - if not cls.log_mongo_url: + if not os.environ.get("OPENPYPE_MONGO"): cls.use_mongo_logging = False - else: - # Decompose url - cls.log_mongo_url_components = decompose_url(cls.log_mongo_url) # Mark as initialized cls.initialized = True @@ -474,7 +464,7 @@ class PypeLogger: if not cls.initialized: cls.initialize() - return OpenPypeMongoConnection.get_mongo_client(cls.log_mongo_url) + return OpenPypeMongoConnection.get_mongo_client() def timeit(method): From f712b207f20456621511052da6c6a2d7bd706102 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Dec 2021 19:25:08 +0100 Subject: [PATCH 231/307] removed compose_url and decompose_url from api and lib functions --- openpype/api.py | 4 ---- openpype/lib/__init__.py | 4 ---- 2 files changed, 8 deletions(-) diff --git a/openpype/api.py b/openpype/api.py index a6529202ff..51854492ab 100644 --- a/openpype/api.py +++ b/openpype/api.py @@ -31,8 +31,6 @@ from .lib import ( ) from .lib.mongo import ( - decompose_url, - compose_url, get_default_components ) @@ -84,8 +82,6 @@ __all__ = [ "Anatomy", "config", "execute", - "decompose_url", - "compose_url", "get_default_components", "ApplicationManager", "BuildWorkfile", diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index efd2cddf7e..c99e3bc28d 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -32,8 +32,6 @@ from .execute import ( ) from .log import PypeLogger, timeit from .mongo import ( - decompose_url, - compose_url, get_default_components, validate_mongo_connection, OpenPypeMongoConnection @@ -276,8 +274,6 @@ __all__ = [ "get_datetime_data", "PypeLogger", - "decompose_url", - "compose_url", "get_default_components", "validate_mongo_connection", "OpenPypeMongoConnection", From 975f0a1c6857fb950e176fc70560bd7929feb8cb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Dec 2021 19:25:17 +0100 Subject: [PATCH 232/307] removed unused compose_url --- openpype/lib/mongo.py | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py index 0fd4517b5b..9f2463eb89 100644 --- a/openpype/lib/mongo.py +++ b/openpype/lib/mongo.py @@ -48,35 +48,6 @@ def decompose_url(url): return components -def compose_url(scheme=None, - host=None, - username=None, - password=None, - port=None, - auth_db=None): - - url = "{scheme}://" - - if username and password: - url += "{username}:{password}@" - - url += "{host}" - if port: - url += ":{port}" - - if auth_db: - url += "?authSource={auth_db}" - - return url.format(**{ - "scheme": scheme, - "host": host, - "username": username, - "password": password, - "port": port, - "auth_db": auth_db - }) - - def get_default_components(): mongo_url = os.environ.get("OPENPYPE_MONGO") if mongo_url is None: From 432c9f8f15377f506b5895d7f4433c2946c6ca9f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Dec 2021 19:27:31 +0100 Subject: [PATCH 233/307] decompose_url is now private function _decompose_url --- openpype/lib/mongo.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py index 9f2463eb89..fb4eb76f28 100644 --- a/openpype/lib/mongo.py +++ b/openpype/lib/mongo.py @@ -15,7 +15,14 @@ class MongoEnvNotSet(Exception): pass -def decompose_url(url): +def _decompose_url(url): + """Decompose mongo url to basic components. + + Used for creation of MongoHandler which expect mongo url components as + separated kwargs. Components are at the end not used as we're setting + connection directly this is just a dumb components for MongoHandler + validation pass. + """ components = { "scheme": None, "host": None, @@ -54,7 +61,7 @@ def get_default_components(): raise MongoEnvNotSet( "URL for Mongo logging connection is not set." ) - return decompose_url(mongo_url) + return _decompose_url(mongo_url) def should_add_certificate_path_to_mongo_url(mongo_url): From d83989c9b31be293d52de4e8a210d6e5f09a6c6d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Dec 2021 19:28:14 +0100 Subject: [PATCH 234/307] use first part of url for replica set urls --- openpype/lib/mongo.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py index fb4eb76f28..7e0bd4f796 100644 --- a/openpype/lib/mongo.py +++ b/openpype/lib/mongo.py @@ -23,6 +23,11 @@ def _decompose_url(url): connection directly this is just a dumb components for MongoHandler validation pass. """ + # Use first url from passed url + # - this is beacuse it is possible to pass multiple urls for multiple + # replica sets which would crash on urlparse otherwise + # - please don't use comma in username of password + url = url.split(",")[0] components = { "scheme": None, "host": None, From f52d4700ae75b26ded49adbcadc1ad5818555b39 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Dec 2021 20:10:36 +0100 Subject: [PATCH 235/307] added new plugin that will cleanup paths defined in context --- openpype/plugins/publish/cleanup_explicit.py | 152 +++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 openpype/plugins/publish/cleanup_explicit.py diff --git a/openpype/plugins/publish/cleanup_explicit.py b/openpype/plugins/publish/cleanup_explicit.py new file mode 100644 index 0000000000..88bba34532 --- /dev/null +++ b/openpype/plugins/publish/cleanup_explicit.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +"""Cleanup files when publishing is done.""" +import os +import shutil +import pyblish.api + + +class ExplicitCleanUp(pyblish.api.ContextPlugin): + """Cleans up the files and folder defined to be deleted. + + plugin is looking for 2 keys into context data: + - `cleanupFullPaths` - full paths that should be removed not matter if + is path to file or to directory + - `cleanupEmptyDirs` - full paths to directories that should be removed + only if do not contain any file in it but will be removed if contain + sub-folders + """ + + order = pyblish.api.IntegratorOrder + 10 + label = "Explicit Clean Up" + optional = True + active = True + + def process(self, context): + cleanup_full_paths = context.data.get("cleanupFullPaths") + cleanup_empty_dirs = context.data.get("cleanupEmptyDirs") + + self._remove_full_paths(cleanup_full_paths) + self._remove_empty_dirs(cleanup_empty_dirs) + + def _remove_full_paths(self, full_paths): + """Remove files and folders from disc. + + Folders are removed with whole content. + """ + if not full_paths: + self.log.debug("No full paths to cleanup were collected.") + return + + # Separate paths into files and directories + filepaths = set() + dirpaths = set() + for path in full_paths: + # Skip empty items + if not path: + continue + # Normalize path + normalized = os.path.normpath(path) + # Check if path exists + if not os.path.exists(normalized): + continue + + if os.path.isfile(normalized): + filepaths.add(normalized) + else: + dirpaths.add(normalized) + + # Store failed paths with exception + failed = [] + # Store removed filepaths for logging + succeded_files = set() + # Remove file by file + for filepath in filepaths: + try: + os.remove(filepath) + succeded_files.add(filepath) + except Exception as exc: + failed.append((filepath, exc)) + + if succeded_files: + self.log.info( + "Removed files:\n{}".format("\n".join(succeded_files)) + ) + + # Delete folders with it's content + succeded_dirs = set() + for dirpath in dirpaths: + # Check if directory still exists + # - it is possible that directory was already deleted with + # different dirpath to delete + if os.path.exists(dirpath): + try: + shutil.rmtree(dirpath) + succeded_dirs.add(dirpath) + except Exception: + failed.append(dirpath) + + if succeded_dirs: + self.log.info( + "Removed direcoties:\n{}".format("\n".join(succeded_dirs)) + ) + + # Prepare lines for report of failed removements + lines = [] + for filepath, exc in failed: + lines.append("{}: {}".format(filepath, str(exc))) + + if lines: + self.log.warning( + "Failed to remove filepaths:\n{}".format("\n".join(lines)) + ) + + def _remove_empty_dirs(self, empty_dirpaths): + """Remove directories if do not contain any files.""" + if not empty_dirpaths: + self.log.debug("No empty dirs to cleanup were collected.") + return + + # First filtering of directories and making sure those are + # existing directories + filtered_dirpaths = set() + for path in empty_dirpaths: + if ( + path + and os.path.exists(path) + and os.path.isdir(path) + ): + filtered_dirpaths.add(os.path.normpath(path)) + + to_delete_dirpaths = set() + to_skip_dirpaths = set() + # Check if contain any files (or it's subfolders contain files) + for dirpath in filtered_dirpaths: + valid = True + for _, _, filenames in os.walk(dirpath): + if filenames: + valid = False + break + + if valid: + to_delete_dirpaths.add(dirpath) + else: + to_skip_dirpaths.add(dirpath) + + if to_skip_dirpaths: + self.log.debug( + "Skipped directories because contain files:\n{}".format( + "\n".join(to_skip_dirpaths) + ) + ) + + # Remove empty directies + for dirpath in to_delete_dirpaths: + if os.path.exists(dirpath): + shutil.rmtree(dirpath) + + if to_delete_dirpaths: + self.log.debug( + "Deleted empty directories:\n{}".format( + "\n".join(to_delete_dirpaths) + ) + ) From 6e64e385544bb439e81eb9a8712275f094620d7b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Dec 2021 20:16:48 +0100 Subject: [PATCH 236/307] added hosts filter for old cleanup plugin --- openpype/plugins/publish/cleanup.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/plugins/publish/cleanup.py b/openpype/plugins/publish/cleanup.py index b8104078d9..f29e6ccd4e 100644 --- a/openpype/plugins/publish/cleanup.py +++ b/openpype/plugins/publish/cleanup.py @@ -15,6 +15,25 @@ class CleanUp(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder + 10 label = "Clean Up" + hosts = [ + "aftereffects", + "blender", + "celaction", + "flame", + "fusion", + "harmony", + "hiero", + "houdini", + "maya", + "nuke", + "photoshop", + "resolve", + "tvpaint", + "unreal", + "standalonepublisher", + "webpublisher", + "shell" + ] exclude_families = ["clip"] optional = True active = True From 148bb47b7d2c2ad366b2271a1c6916016651fdb2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 14 Dec 2021 10:54:10 +0100 Subject: [PATCH 237/307] OP-2053 - allow injection of AVALON_DB env var as a db --- start.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/start.py b/start.py index 0f7e82071d..10b81bc725 100644 --- a/start.py +++ b/start.py @@ -339,13 +339,14 @@ def set_avalon_environments(): os.environ.get("AVALON_MONGO") or os.environ["OPENPYPE_MONGO"] ) + avalon_db = os.environ.get("AVALON_DB") or "avalon" # for tests os.environ.update({ # Mongo url (use same as OpenPype has) "AVALON_MONGO": avalon_mongo_url, "AVALON_SCHEMA": schema_path, # Mongo DB name where avalon docs are stored - "AVALON_DB": "avalon", + "AVALON_DB": avalon_db, # Name of config "AVALON_CONFIG": "openpype", "AVALON_LABEL": "OpenPype" From a6a7348a73ac5249cbbcd143b8e0f9678c4b0b6e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Dec 2021 11:12:06 +0100 Subject: [PATCH 238/307] fix access to default settings in get_general_environments --- openpype/settings/lib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index ff75562413..43489aecfd 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -933,8 +933,10 @@ def get_general_environments(): # - prevent to use `get_system_settings` where `get_default_settings` # is used default_values = load_openpype_default_settings() + system_settings = default_values["system_settings"] studio_overrides = get_studio_system_settings_overrides() - result = apply_overrides(default_values, studio_overrides) + + result = apply_overrides(system_settings, studio_overrides) environments = result["general"]["environment"] clear_metadata_from_settings(environments) From 8739dd9f05e7f70dddd388da2de0b7efd4456832 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 14 Dec 2021 11:13:02 +0100 Subject: [PATCH 239/307] OP-2053 - fix counts in db_asserts --- .../hosts/aftereffects/test_publish_in_aftereffects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py b/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py index e0f6b3e48e..ec0280e7c6 100644 --- a/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py +++ b/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py @@ -68,7 +68,7 @@ class TestPublishInAfterEffects(PublishTest): def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" print("test_db_asserts") - assert 3 == dbcon.count_documents({"type": "version"}), \ + assert 2 == dbcon.count_documents({"type": "version"}), \ "Not expected no of versions" assert 0 == dbcon.count_documents({"type": "version", @@ -88,7 +88,7 @@ class TestPublishInAfterEffects(PublishTest): "name": "reviewTesttask"}), \ "reviewTesttask subset must be present" - assert 6 == dbcon.count_documents({"type": "representation"}), \ + assert 4 == dbcon.count_documents({"type": "representation"}), \ "Not expected no of representations" assert 1 == dbcon.count_documents({"type": "representation", From 02717fac4eaaf5dbd17ddcf86e59bc58b136a8ff Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 14 Dec 2021 11:15:51 +0100 Subject: [PATCH 240/307] Update error msg format Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../plugins/publish/collect_extension_version.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_extension_version.py b/openpype/hosts/aftereffects/plugins/publish/collect_extension_version.py index 3352fd21f0..4e74252043 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_extension_version.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_extension_version.py @@ -48,9 +48,9 @@ class CollectExtensionVersion(pyblish.api.ContextPlugin): expected_version = found[0][1] if expected_version != installed_version: - msg = "Expected version '{}' found '{}'\n".format( - expected_version, installed_version) - msg += "Please update your installed extension, it might not work " - msg += "properly." + msg = ( + "Expected version '{}' found '{}'\n Please update" + " your installed extension, it might not work properly." + ).format(expected_version, installed_version) raise ValueError(msg) From aa232b43268cd38a0162eb6af6d50a8d4b1d998c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 14 Dec 2021 11:13:02 +0100 Subject: [PATCH 241/307] OP-2053 - fix counts in db_asserts --- .../hosts/aftereffects/test_publish_in_aftereffects.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py b/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py index e0f6b3e48e..c3ca9aa9d2 100644 --- a/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py +++ b/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py @@ -68,7 +68,7 @@ class TestPublishInAfterEffects(PublishTest): def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" print("test_db_asserts") - assert 3 == dbcon.count_documents({"type": "version"}), \ + assert 2 == dbcon.count_documents({"type": "version"}), \ "Not expected no of versions" assert 0 == dbcon.count_documents({"type": "version", @@ -81,18 +81,18 @@ class TestPublishInAfterEffects(PublishTest): "modelMain subset must be present" assert 1 == dbcon.count_documents({"type": "subset", - "name": "workfileTesttask"}), \ + "name": "workfileTest_task"}), \ "workfileTesttask subset must be present" assert 1 == dbcon.count_documents({"type": "subset", "name": "reviewTesttask"}), \ "reviewTesttask subset must be present" - assert 6 == dbcon.count_documents({"type": "representation"}), \ + assert 4 == dbcon.count_documents({"type": "representation"}), \ "Not expected no of representations" assert 1 == dbcon.count_documents({"type": "representation", - "context.subset": "imageMainBackgroundcopy", # noqa E501 + "context.subset": "renderTestTaskDefault", # noqa E501 "context.ext": "png"}), \ "Not expected no of representations with ext 'png'" From 0ff12fdaeeb6629e74a26cd56c06fa4d459ddd8d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Dec 2021 11:48:23 +0100 Subject: [PATCH 242/307] cache object types --- .../default_modules/ftrack/lib/avalon_sync.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/lib/avalon_sync.py b/openpype/modules/default_modules/ftrack/lib/avalon_sync.py index 3ba874281a..392b64eb57 100644 --- a/openpype/modules/default_modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/default_modules/ftrack/lib/avalon_sync.py @@ -360,6 +360,8 @@ class SyncEntitiesFactory: self._subsets_by_parent_id = None self._changeability_by_mongo_id = None + self._object_types_by_name = None + self.all_filtered_entities = {} self.filtered_ids = [] self.not_selected_ids = [] @@ -651,6 +653,18 @@ class SyncEntitiesFactory: self._bubble_changeability(list(self.subsets_by_parent_id.keys())) return self._changeability_by_mongo_id + @property + def object_types_by_name(self): + if self._object_types_by_name is None: + object_types_by_name = self.session.query( + "select id, name from ObjectType" + ).all() + self._object_types_by_name = { + object_type["name"]: object_type + for object_type in object_types_by_name + } + return self._object_types_by_name + @property def all_ftrack_names(self): """ @@ -880,10 +894,7 @@ class SyncEntitiesFactory: custom_attrs, hier_attrs = get_openpype_attr( self.session, query_keys=self.cust_attr_query_keys ) - ent_types = self.session.query("select id, name from ObjectType").all() - ent_types_by_name = { - ent_type["name"]: ent_type["id"] for ent_type in ent_types - } + ent_types_by_name = self.object_types_by_name # Custom attribute types cust_attr_types = self.session.query( "select id, name from CustomAttributeType" From 00b143066ea99ffabd32ea7bf32b711d4f07a849 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Dec 2021 11:48:55 +0100 Subject: [PATCH 243/307] check existence of entity type on recreation and use Folder if not found --- .../modules/default_modules/ftrack/lib/avalon_sync.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/ftrack/lib/avalon_sync.py b/openpype/modules/default_modules/ftrack/lib/avalon_sync.py index 392b64eb57..f58eb91485 100644 --- a/openpype/modules/default_modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/default_modules/ftrack/lib/avalon_sync.py @@ -2502,7 +2502,13 @@ class SyncEntitiesFactory: parent_entity = self.entities_dict[parent_id]["entity"] _name = av_entity["name"] - _type = av_entity["data"].get("entityType", "folder") + _type = av_entity["data"].get("entityType") + # Check existence of object type + if _type and _type not in self.object_types_by_name: + _type = None + + if not _type: + _type = "Folder" self.log.debug(( "Re-ceating deleted entity {} <{}>" From f1ab759a6179efbd96f4db17957351f60984fb04 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 14 Dec 2021 13:28:00 +0100 Subject: [PATCH 244/307] OP-2151 - replaced PATH usage with oiio path for maketx utility --- openpype/hosts/maya/plugins/publish/extract_look.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 2407617b6f..92587c2910 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -55,8 +55,11 @@ def maketx(source, destination, *args): str: Output of `maketx` command. """ + from openpype.lib import get_oiio_tools_path + + maketx_path = get_oiio_tools_path("maketx") cmd = [ - "maketx", + maketx_path, "-v", # verbose "-u", # update mode # unpremultiply before conversion (recommended when alpha present) From 209dbea1a5b83aa61047b072923565fe238c56ff Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 14 Dec 2021 13:44:21 +0100 Subject: [PATCH 245/307] OP-2151 - added logging and exception if oiio utilities not found --- openpype/hosts/maya/plugins/publish/extract_look.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 92587c2910..953539f65c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -58,6 +58,11 @@ def maketx(source, destination, *args): from openpype.lib import get_oiio_tools_path maketx_path = get_oiio_tools_path("maketx") + if not os.path.exists(maketx_path): + print( + "OIIO tool not found in {}".format(maketx_path)) + raise AssertionError("OIIO tool not found") + cmd = [ maketx_path, "-v", # verbose From ebbd43bd5fd6d587f6e88558df5512d8c9e971d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Tue, 14 Dec 2021 13:46:34 +0100 Subject: [PATCH 246/307] Update Dockerfile.centos7 Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- Dockerfile.centos7 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.centos7 b/Dockerfile.centos7 index ce60ea7fb1..736a42663c 100644 --- a/Dockerfile.centos7 +++ b/Dockerfile.centos7 @@ -42,7 +42,7 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n ncurses-devel \ qt5-qtbase-devel \ xcb-util-wm \ - xcb-util-renderutil + xcb-util-renderutil \ && yum clean all # we need to build our own patchelf From 12282ca19cb931ea5499b0665a74e6e830df6eb4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 14 Dec 2021 14:54:23 +0100 Subject: [PATCH 247/307] nuke: baking representations was not additive --- .../nuke/plugins/publish/extract_review_data_mov.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py index 261fca6583..32962b57a6 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py @@ -42,6 +42,7 @@ class ExtractReviewDataMov(openpype.api.Extractor): # generate data with anlib.maintained_selection(): + generated_repres = [] for o_name, o_data in self.outputs.items(): f_families = o_data["filter"]["families"] f_task_types = o_data["filter"]["task_types"] @@ -112,11 +113,13 @@ class ExtractReviewDataMov(openpype.api.Extractor): }) else: data = exporter.generate_mov(**o_data) + generated_repres.extend(data["representations"]) - self.log.info(data["representations"]) + self.log.info(generated_repres) - # assign to representations - instance.data["representations"] += data["representations"] + if generated_repres: + # assign to representations + instance.data["representations"] += generated_repres self.log.debug( "_ representations: {}".format( From b57a09b430a8dd1e09db2fa97cf37685b0ca9aa8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 14 Dec 2021 15:16:48 +0100 Subject: [PATCH 248/307] OP-2019 - revert unwanted commit --- openpype/hooks/pre_foundry_apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hooks/pre_foundry_apps.py b/openpype/hooks/pre_foundry_apps.py index 70554cbedb..85f68c6b60 100644 --- a/openpype/hooks/pre_foundry_apps.py +++ b/openpype/hooks/pre_foundry_apps.py @@ -13,7 +13,7 @@ class LaunchFoundryAppsWindows(PreLaunchHook): # Should be as last hook because must change launch arguments to string order = 1000 - app_groups = ["nuke", "nukex", "hiero", "nukestudio", "aftereffects"] + app_groups = ["nuke", "nukex", "hiero", "nukestudio"] platforms = ["windows"] def execute(self): From 450dbf3fd25225b4d94a6f92f6404ccb5a0c80c3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 14 Dec 2021 15:29:42 +0100 Subject: [PATCH 249/307] OP-2053 - fix PS after merge --- .../photoshop/test_publish_in_photoshop.py | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py index b52af48009..f7bedf6069 100644 --- a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py @@ -44,32 +44,6 @@ class TestPublishInPhotoshop(PhotoshopTestClass): TIMEOUT = 120 # publish timeout - @pytest.fixture(scope="module") - def last_workfile_path(self, download_test_data): - """Get last_workfile_path from source data. - - Maya expects workfile in proper folder, so copy is done first. - """ - src_path = os.path.join(download_test_data, - "input", - "workfile", - "test_project_test_asset_TestTask_v001.psd") - dest_folder = os.path.join(download_test_data, - self.PROJECT, - self.ASSET, - "work", - self.TASK) - os.makedirs(dest_folder) - dest_path = os.path.join(dest_folder, - "test_project_test_asset_TestTask_v001.psd") - shutil.copy(src_path, dest_path) - - yield dest_path - - @pytest.fixture(scope="module") - def startup_scripts(self, monkeypatch_session, download_test_data): - """Points Maya to userSetup file from input data""" - pass def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" From 2c66a1eab2527a34029fc639fc475efa2dc77896 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 14 Dec 2021 15:31:03 +0100 Subject: [PATCH 250/307] OP-2053 - fix PS after merge --- tests/integration/hosts/photoshop/test_publish_in_photoshop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py index f7bedf6069..32053cd9d4 100644 --- a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py @@ -37,7 +37,7 @@ class TestPublishInPhotoshop(PhotoshopTestClass): ] APP = "photoshop" - # keep empty to locate latest installed variant or explicit + # keep empty to locate latest installed variant or explicit APP_VARIANT = "" APP_NAME = "{}/{}".format(APP, APP_VARIANT) From 68b99391acecec1a208468df98026506db7ac95a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Dec 2021 15:35:39 +0100 Subject: [PATCH 251/307] modified message --- openpype/tools/settings/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 9016e63970..b10c958880 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -340,7 +340,7 @@ class BaseWidget(QtWidgets.QWidget): # TODO better message title = "Applying values failed" - msg = "Using values from project \"{}\" failed.".format( + msg = "Applying values from project \"{}\" failed.".format( project_name ) detail_msg = "".join( From 512bf2e3f66c2523eedd52f2434ea2b84b5772be Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 15 Dec 2021 03:41:30 +0000 Subject: [PATCH 252/307] [Automated] Bump version --- CHANGELOG.md | 54 ++++++++++++++------------------------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 18 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 860e26c59c..dde8138629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,12 @@ # Changelog -## [3.7.0-nightly.7](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.7.0-nightly.8](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.4...HEAD) -### 📖 Documentation +**Deprecated:** -- docs\[website\]: Add Ellipse Studio \(logo\) as an OpenPype contributor [\#2324](https://github.com/pypeclub/OpenPype/pull/2324) +- General: Default modules hierarchy n2 [\#2368](https://github.com/pypeclub/OpenPype/pull/2368) **🆕 New features** @@ -14,27 +14,31 @@ **🚀 Enhancements** +- Ftrack: Check existence of object type on recreation [\#2404](https://github.com/pypeclub/OpenPype/pull/2404) +- Flame: moving `utility\_scripts` to api folder also with `scripts` [\#2385](https://github.com/pypeclub/OpenPype/pull/2385) +- Centos 7 dependency compatibility [\#2384](https://github.com/pypeclub/OpenPype/pull/2384) +- Enhancement: Settings: Use project settings values from another project [\#2382](https://github.com/pypeclub/OpenPype/pull/2382) +- Blender 3: Support auto install for new blender version [\#2377](https://github.com/pypeclub/OpenPype/pull/2377) +- Maya add render image path to settings [\#2375](https://github.com/pypeclub/OpenPype/pull/2375) - Settings: Webpublisher in hosts enum [\#2367](https://github.com/pypeclub/OpenPype/pull/2367) - Hiero: python3 compatibility [\#2365](https://github.com/pypeclub/OpenPype/pull/2365) - Burnins: Be able recognize mxf OPAtom format [\#2361](https://github.com/pypeclub/OpenPype/pull/2361) +- Maya: Add is\_static\_image\_plane and is\_in\_all\_views option in imagePlaneLoader [\#2356](https://github.com/pypeclub/OpenPype/pull/2356) - Local settings: Copyable studio paths [\#2349](https://github.com/pypeclub/OpenPype/pull/2349) - Assets Widget: Clear model on project change [\#2345](https://github.com/pypeclub/OpenPype/pull/2345) - General: OpenPype default modules hierarchy [\#2338](https://github.com/pypeclub/OpenPype/pull/2338) - General: FFprobe error exception contain original error message [\#2328](https://github.com/pypeclub/OpenPype/pull/2328) - Resolve: Add experimental button to menu [\#2325](https://github.com/pypeclub/OpenPype/pull/2325) -- Hiero: Add experimental tools action [\#2323](https://github.com/pypeclub/OpenPype/pull/2323) - Input links: Cleanup and unification of differences [\#2322](https://github.com/pypeclub/OpenPype/pull/2322) - General: Don't validate vendor bin with executing them [\#2317](https://github.com/pypeclub/OpenPype/pull/2317) - General: Multilayer EXRs support [\#2315](https://github.com/pypeclub/OpenPype/pull/2315) -- General: Run process log stderr as info log level [\#2309](https://github.com/pypeclub/OpenPype/pull/2309) - General: Reduce vendor imports [\#2305](https://github.com/pypeclub/OpenPype/pull/2305) -- Tools: Cleanup of unused classes [\#2304](https://github.com/pypeclub/OpenPype/pull/2304) -- Project Manager: Added ability to delete project [\#2298](https://github.com/pypeclub/OpenPype/pull/2298) -- Ftrack: Synchronize input links [\#2287](https://github.com/pypeclub/OpenPype/pull/2287) -- Nuke: extract baked review videos presets [\#2248](https://github.com/pypeclub/OpenPype/pull/2248) **🐛 Bug fixes** +- General: Fix access to environments from default settings [\#2403](https://github.com/pypeclub/OpenPype/pull/2403) +- Fix: Placeholder Input color set fix [\#2399](https://github.com/pypeclub/OpenPype/pull/2399) +- Settings: Fix state change of wrapper label [\#2396](https://github.com/pypeclub/OpenPype/pull/2396) - Flame: fix ftrack publisher [\#2381](https://github.com/pypeclub/OpenPype/pull/2381) - hiero: solve custom ocio path [\#2379](https://github.com/pypeclub/OpenPype/pull/2379) - hiero: fix workio and flatten [\#2378](https://github.com/pypeclub/OpenPype/pull/2378) @@ -53,21 +57,16 @@ - Timers Manager: Disable auto stop timer on linux platform [\#2334](https://github.com/pypeclub/OpenPype/pull/2334) - nuke: bake preset single input exception [\#2331](https://github.com/pypeclub/OpenPype/pull/2331) - Hiero: fixing multiple templates at a hierarchy parent [\#2330](https://github.com/pypeclub/OpenPype/pull/2330) -- Fix - provider icons are pulled from a folder [\#2326](https://github.com/pypeclub/OpenPype/pull/2326) -- InputLinks: Typo in "inputLinks" key [\#2314](https://github.com/pypeclub/OpenPype/pull/2314) -- Deadline timeout and logging [\#2312](https://github.com/pypeclub/OpenPype/pull/2312) -- nuke: do not multiply representation on class method [\#2311](https://github.com/pypeclub/OpenPype/pull/2311) -- Workfiles tool: Fix task formatting [\#2306](https://github.com/pypeclub/OpenPype/pull/2306) -- Delivery: Fix delivery paths created on windows [\#2302](https://github.com/pypeclub/OpenPype/pull/2302) -- Maya: Deadline - fix limit groups [\#2295](https://github.com/pypeclub/OpenPype/pull/2295) - Royal Render: Fix plugin order and OpenPype auto-detection [\#2291](https://github.com/pypeclub/OpenPype/pull/2291) -- Alternate site for site sync doesnt work for sequences [\#2284](https://github.com/pypeclub/OpenPype/pull/2284) **Merged pull requests:** +- \[Fix\]\[MAYA\] Handle message type attribute within CollectLook [\#2394](https://github.com/pypeclub/OpenPype/pull/2394) +- Add validator to check correct version of extension for PS and AE [\#2387](https://github.com/pypeclub/OpenPype/pull/2387) - Linux : flip updating submodules logic [\#2357](https://github.com/pypeclub/OpenPype/pull/2357) - Update of avalon-core [\#2346](https://github.com/pypeclub/OpenPype/pull/2346) - Maya: configurable model top level validation [\#2321](https://github.com/pypeclub/OpenPype/pull/2321) +- Create test publish class for After Effects [\#2270](https://github.com/pypeclub/OpenPype/pull/2270) ## [3.6.4](https://github.com/pypeclub/OpenPype/tree/3.6.4) (2021-11-23) @@ -93,42 +92,21 @@ - Tools: Assets widget [\#2265](https://github.com/pypeclub/OpenPype/pull/2265) - SceneInventory: Choose loader in asset switcher [\#2262](https://github.com/pypeclub/OpenPype/pull/2262) -- Style: New fonts in OpenPype style [\#2256](https://github.com/pypeclub/OpenPype/pull/2256) -- Tools: Tasks widget [\#2251](https://github.com/pypeclub/OpenPype/pull/2251) -- Tools: Creator in OpenPype [\#2244](https://github.com/pypeclub/OpenPype/pull/2244) **🐛 Bug fixes** - Tools: Parenting of tools in Nuke and Hiero [\#2266](https://github.com/pypeclub/OpenPype/pull/2266) - limiting validator to specific editorial hosts [\#2264](https://github.com/pypeclub/OpenPype/pull/2264) - Tools: Select Context dialog attribute fix [\#2261](https://github.com/pypeclub/OpenPype/pull/2261) -- Maya: Render publishing fails on linux [\#2260](https://github.com/pypeclub/OpenPype/pull/2260) -- LookAssigner: Fix tool reopen [\#2259](https://github.com/pypeclub/OpenPype/pull/2259) -- Standalone: editorial not publishing thumbnails on all subsets [\#2258](https://github.com/pypeclub/OpenPype/pull/2258) -- Burnins: Support mxf metadata [\#2247](https://github.com/pypeclub/OpenPype/pull/2247) ## [3.6.1](https://github.com/pypeclub/OpenPype/tree/3.6.1) (2021-11-16) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.1-nightly.1...3.6.1) -**🐛 Bug fixes** - -- Loader doesn't allow changing of version before loading [\#2254](https://github.com/pypeclub/OpenPype/pull/2254) - ## [3.6.0](https://github.com/pypeclub/OpenPype/tree/3.6.0) (2021-11-15) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.0-nightly.6...3.6.0) -**🚀 Enhancements** - -- Tools: SceneInventory in OpenPype [\#2255](https://github.com/pypeclub/OpenPype/pull/2255) -- Tools: Subset manager in OpenPype [\#2243](https://github.com/pypeclub/OpenPype/pull/2243) - -**🐛 Bug fixes** - -- Ftrack: Sync project ftrack id cache issue [\#2250](https://github.com/pypeclub/OpenPype/pull/2250) -- Ftrack: Session creation and Prepare project [\#2245](https://github.com/pypeclub/OpenPype/pull/2245) - ## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.5.0-nightly.8...3.5.0) diff --git a/openpype/version.py b/openpype/version.py index 95cd7a285f..06bc20ae43 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.7.0-nightly.7" +__version__ = "3.7.0-nightly.8" diff --git a/pyproject.toml b/pyproject.toml index d994569fa7..e5d552bb3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.7.0-nightly.7" # OpenPype +version = "3.7.0-nightly.8" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From bda4a3c934b6947bf3a15cc7adfd2f5c7cd7a783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20LORRAIN?= Date: Wed, 15 Dec 2021 15:47:33 +0100 Subject: [PATCH 253/307] Fix build regex and improve filter for create_zip cache cleaning --- tools/create_zip.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/create_zip.ps1 b/tools/create_zip.ps1 index c27857b480..99e4b39550 100644 --- a/tools/create_zip.ps1 +++ b/tools/create_zip.ps1 @@ -96,9 +96,9 @@ if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Cleaning cache files ... " -NoNewline -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 +Get-ChildItem $openpype_root -Filter "__pycache__" -Force -Recurse| Where-Object {( $_.FullName -inotmatch '\\build\\' ) -and ( $_.FullName -inotmatch '\\.venv' )} | Remove-Item -Force -Recurse +Get-ChildItem $openpype_root -Filter "*.pyc" -Force -Recurse | Where-Object {( $_.FullName -inotmatch '\\build\\' ) -and ( $_.FullName -inotmatch '\.venv' )} | Remove-Item -Force +Get-ChildItem $openpype_root -Filter "*.pyo" -Force -Recurse | Where-Object {( $_.FullName -inotmatch '\\build\\' ) -and ( $_.FullName -inotmatch '\.venv' )} | Remove-Item -Force Write-Host "OK" -ForegroundColor green Write-Host ">>> " -NoNewline -ForegroundColor green From 654aedbca1cdff4ac39a4c33e2cfbdc4f47753ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20LORRAIN?= Date: Wed, 15 Dec 2021 16:13:26 +0100 Subject: [PATCH 254/307] fix regex typo --- tools/create_zip.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/create_zip.ps1 b/tools/create_zip.ps1 index 99e4b39550..e33445d1fa 100644 --- a/tools/create_zip.ps1 +++ b/tools/create_zip.ps1 @@ -97,8 +97,8 @@ if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Cleaning cache files ... " -NoNewline Get-ChildItem $openpype_root -Filter "__pycache__" -Force -Recurse| Where-Object {( $_.FullName -inotmatch '\\build\\' ) -and ( $_.FullName -inotmatch '\\.venv' )} | Remove-Item -Force -Recurse -Get-ChildItem $openpype_root -Filter "*.pyc" -Force -Recurse | Where-Object {( $_.FullName -inotmatch '\\build\\' ) -and ( $_.FullName -inotmatch '\.venv' )} | Remove-Item -Force -Get-ChildItem $openpype_root -Filter "*.pyo" -Force -Recurse | Where-Object {( $_.FullName -inotmatch '\\build\\' ) -and ( $_.FullName -inotmatch '\.venv' )} | Remove-Item -Force +Get-ChildItem $openpype_root -Filter "*.pyc" -Force -Recurse | Where-Object {( $_.FullName -inotmatch '\\build\\' ) -and ( $_.FullName -inotmatch '\\.venv' )} | Remove-Item -Force +Get-ChildItem $openpype_root -Filter "*.pyo" -Force -Recurse | Where-Object {( $_.FullName -inotmatch '\\build\\' ) -and ( $_.FullName -inotmatch '\\.venv' )} | Remove-Item -Force Write-Host "OK" -ForegroundColor green Write-Host ">>> " -NoNewline -ForegroundColor green From 8b2edb368bf38c7465a648e868bf331767efb4f4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 12:23:36 +0100 Subject: [PATCH 255/307] added 2022 variant to aftereffects settings schemas --- .../system_schema/host_settings/schema_aftereffects.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_aftereffects.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_aftereffects.json index 6c36a9bb8a..334c9aa235 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_aftereffects.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_aftereffects.json @@ -36,6 +36,11 @@ "app_variant_label": "2021", "app_variant": "2021", "variant_skip_paths": ["use_python_2"] + }, + { + "app_variant_label": "2022", + "app_variant": "2022", + "variant_skip_paths": ["use_python_2"] } ] } From 8f622281cf7300ea3bc92912ea133cbcfbef51e2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 13:45:31 +0100 Subject: [PATCH 256/307] change override state on calling pop --- openpype/settings/entities/dict_mutable_keys_entity.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/settings/entities/dict_mutable_keys_entity.py b/openpype/settings/entities/dict_mutable_keys_entity.py index cff346e9ea..be78018ebb 100644 --- a/openpype/settings/entities/dict_mutable_keys_entity.py +++ b/openpype/settings/entities/dict_mutable_keys_entity.py @@ -60,6 +60,12 @@ class DictMutableKeysEntity(EndpointEntity): def pop(self, key, *args, **kwargs): if key in self.required_keys: raise RequiredKeyModified(self.path, key) + + if self._override_state is OverrideState.STUDIO: + self._has_studio_override = True + elif self._override_state is OverrideState.PROJECT: + self._has_project_override = True + result = self.children_by_key.pop(key, *args, **kwargs) self.on_change() return result From 16ae2913863602b003c548eb7e126c2e741a3ac8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 15:46:45 +0100 Subject: [PATCH 257/307] adde abstract method to be able know if entity has children by a path key --- openpype/settings/entities/base_entity.py | 5 +++++ .../settings/entities/dict_conditional.py | 3 +++ .../entities/dict_immutable_keys_entity.py | 3 +++ .../entities/dict_mutable_keys_entity.py | 3 +++ openpype/settings/entities/input_entities.py | 3 +++ openpype/settings/entities/item_entities.py | 21 +++++++++++++++++++ openpype/settings/entities/list_entity.py | 16 ++++++++++++++ openpype/settings/entities/root_entities.py | 3 +++ 8 files changed, 57 insertions(+) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 341968bd75..cbc042d29d 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -235,6 +235,11 @@ class BaseItemEntity(BaseEntity): """Return system settings entity.""" pass + @abstractmethod + def has_child_with_key(self, key): + """Entity contains key as children.""" + pass + def schema_validations(self): """Validate schema of entity and it's hierachy. diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 5f1c172f31..92512a6668 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -107,6 +107,9 @@ class DictConditionalEntity(ItemEntity): for _key, _value in new_value.items(): self.non_gui_children[self.current_enum][_key].set(_value) + def has_child_with_key(self, key): + return key in self.keys() + def _item_initialization(self): self._default_metadata = NOT_SET self._studio_override_metadata = NOT_SET diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index 6131fa2ac7..c477a0eb0f 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -205,6 +205,9 @@ class DictImmutableKeysEntity(ItemEntity): ) self.show_borders = self.schema_data.get("show_borders", True) + def has_child_with_key(self, key): + return key in self.non_gui_children + def collect_static_entities_by_path(self): output = {} if self.is_dynamic_item or self.is_in_dynamic_item: diff --git a/openpype/settings/entities/dict_mutable_keys_entity.py b/openpype/settings/entities/dict_mutable_keys_entity.py index cff346e9ea..97af9e5c81 100644 --- a/openpype/settings/entities/dict_mutable_keys_entity.py +++ b/openpype/settings/entities/dict_mutable_keys_entity.py @@ -191,6 +191,9 @@ class DictMutableKeysEntity(EndpointEntity): child_entity = self.children_by_key[key] self.set_child_label(child_entity, label) + def has_child_with_key(self, key): + return key in self.children_by_key + def _item_initialization(self): self._default_metadata = {} self._studio_override_metadata = {} diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index a0598d405e..16893747a6 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -118,6 +118,9 @@ class InputEntity(EndpointEntity): return self.value == other.value return self.value == other + def has_child_with_key(self, key): + return False + def get_child_path(self, child_obj): raise TypeError("{} can't have children".format( self.__class__.__name__ diff --git a/openpype/settings/entities/item_entities.py b/openpype/settings/entities/item_entities.py index ff0a982900..9c6f428b97 100644 --- a/openpype/settings/entities/item_entities.py +++ b/openpype/settings/entities/item_entities.py @@ -1,3 +1,7 @@ +import re + +import six + from .lib import ( NOT_SET, STRING_TYPE, @@ -48,6 +52,9 @@ class PathEntity(ItemEntity): raise AttributeError(self.attribute_error_msg.format("items")) return self.child_obj.items() + def has_child_with_key(self, key): + return self.child_obj.has_child_with_key(key) + def _item_initialization(self): if self.group_item is None and not self.is_group: self.is_group = True @@ -197,6 +204,7 @@ class PathEntity(ItemEntity): class ListStrictEntity(ItemEntity): schema_types = ["list-strict"] + _key_regex = re.compile(r"[0-9]+") def __getitem__(self, idx): if not isinstance(idx, int): @@ -216,6 +224,19 @@ class ListStrictEntity(ItemEntity): return self.children[idx] return default + def has_child_with_key(self, key): + if ( + key + and isinstance(key, six.string_types) + and self._key_regex.match(key) + ): + key = int(key) + + if not isinstance(key, int): + return False + + return 0 <= key < len(self.children) + def _item_initialization(self): self.valid_value_types = (list, ) self.require_key = True diff --git a/openpype/settings/entities/list_entity.py b/openpype/settings/entities/list_entity.py index 5d89a81351..0268c208bb 100644 --- a/openpype/settings/entities/list_entity.py +++ b/openpype/settings/entities/list_entity.py @@ -1,4 +1,6 @@ import copy +import six +import re from . import ( BaseEntity, EndpointEntity @@ -21,6 +23,7 @@ class ListEntity(EndpointEntity): "collapsible": True, "collapsed": False } + _key_regex = re.compile(r"[0-9]+") def __iter__(self): for item in self.children: @@ -144,6 +147,19 @@ class ListEntity(EndpointEntity): ) self.on_change() + def has_child_with_key(self, key): + if ( + key + and isinstance(key, six.string_types) + and self._key_regex.match(key) + ): + key = int(key) + + if not isinstance(key, int): + return False + + return 0 <= key < len(self.children) + def _convert_to_valid_type(self, value): if isinstance(value, (set, tuple)): return list(value) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index b8baed8a93..687784a359 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -127,6 +127,9 @@ class RootEntity(BaseItemEntity): for _key, _value in new_value.items(): self.non_gui_children[_key].set(_value) + def has_child_with_key(self, key): + return key in self.non_gui_children + def keys(self): return self.non_gui_children.keys() From 06fed885ad5e9a5626baa50dd981856ea98eb183 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 15:47:29 +0100 Subject: [PATCH 258/307] breadcrumbs are not brute focing access to child entities by accessing with __getitem__ but checking if 'has_child_with_key' --- .../settings/settings/breadcrumbs_widget.py | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/openpype/tools/settings/settings/breadcrumbs_widget.py b/openpype/tools/settings/settings/breadcrumbs_widget.py index d25cbdc8cb..7524bc61f0 100644 --- a/openpype/tools/settings/settings/breadcrumbs_widget.py +++ b/openpype/tools/settings/settings/breadcrumbs_widget.py @@ -71,17 +71,35 @@ class SettingsBreadcrumbs(BreadcrumbsModel): return True return False + def get_valid_path(self, path): + if not path: + return "" + + path_items = path.split("/") + new_path_items = [] + entity = self.entity + for item in path_items: + if not entity.has_child_with_key(item): + break + + new_path_items.append(item) + entity = entity[item] + + return "/".join(new_path_items) + def is_valid_path(self, path): if not path: return True path_items = path.split("/") - try: - entity = self.entity - for item in path_items: - entity = entity[item] - except Exception: - return False + + entity = self.entity + for item in path_items: + if not entity.has_child_with_key(item): + return False + + entity = entity[item] + return True @@ -436,6 +454,7 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): self.change_path(path) def change_path(self, path): + path = self._model.get_valid_path(path) if self._model and not self._model.is_valid_path(path): self._show_address_field() else: From d392885b35de566faaecab0e8e0a1a3a029eeac2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 16 Dec 2021 15:58:16 +0100 Subject: [PATCH 259/307] OP-2038 - introduced settings for invalid characters to use in ValidateNaming plugin --- .../plugins/publish/validate_naming.py | 44 ++++++++++++++----- .../defaults/project_settings/photoshop.json | 14 +++--- .../schema_project_photoshop.json | 42 +++++++++++++----- 3 files changed, 75 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/validate_naming.py b/openpype/hosts/photoshop/plugins/publish/validate_naming.py index 0fd6794313..077f7cf132 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_naming.py @@ -1,3 +1,5 @@ +import re + import pyblish.api import openpype.api from avalon import photoshop @@ -19,20 +21,32 @@ class ValidateNamingRepair(pyblish.api.Action): and result["instance"] not in failed): failed.append(result["instance"]) + invalid_chars, replace_char = plugin.get_replace_chars() + # Apply pyblish.logic to get the instances for the plug-in instances = pyblish.api.instances_by_plugin(failed, plugin) stub = photoshop.stub() for instance in instances: self.log.info("validate_naming instance {}".format(instance)) - name = instance.data["name"].replace(" ", "_") - name = name.replace(instance.data["family"], '') - instance[0].Name = name - data = stub.read(instance[0]) - data["subset"] = "image" + name - stub.imprint(instance[0], data) + metadata = stub.read(instance[0]) + self.log.info("metadata instance {}".format(metadata)) + layer_name = None + if metadata.get("uuid"): + layer_data = stub.get_layer(metadata["uuid"]) + self.log.info("layer_data {}".format(layer_data)) + if layer_data: + layer_name = re.sub(invalid_chars, + replace_char, + layer_name) - name = stub.PUBLISH_ICON + name - stub.rename_layer(instance.data["uuid"], name) + stub.rename_layer(instance.data["uuid"], layer_name) + + subset_name = re.sub(invalid_chars, replace_char, + instance.data["name"]) + + instance[0].Name = layer_name or subset_name + metadata["subset"] = subset_name + stub.imprint(instance[0], metadata) return True @@ -49,12 +63,22 @@ class ValidateNaming(pyblish.api.InstancePlugin): families = ["image"] actions = [ValidateNamingRepair] + # configured by Settings + invalid_chars = '' + replace_char = '' + def process(self, instance): help_msg = ' Use Repair action (A) in Pyblish to fix it.' msg = "Name \"{}\" is not allowed.{}".format(instance.data["name"], help_msg) - assert " " not in instance.data["name"], msg + assert not re.search(self.invalid_chars, instance.data["name"]), msg msg = "Subset \"{}\" is not allowed.{}".format(instance.data["subset"], help_msg) - assert " " not in instance.data["subset"], msg + assert not re.search(self.invalid_chars, instance.data["subset"]), msg + + + @classmethod + def get_replace_chars(cls): + """Pass values configured in Settings for Repair.""" + return cls.invalid_chars, cls.replace_char \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index 0c24c943ec..db9bf87268 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -7,11 +7,6 @@ } }, "publish": { - "ValidateContainers": { - "enabled": true, - "optional": true, - "active": true - }, "CollectRemoteInstances": { "color_code_mapping": [ { @@ -22,6 +17,15 @@ } ] }, + "ValidateContainers": { + "enabled": true, + "optional": true, + "active": true + }, + "ValidateNaming": { + "invalid_chars": "[ \\\\/+\\*\\?\\(\\)\\[\\]\\{\\}:,]", + "replace_char": "_" + }, "ExtractImage": { "formats": [ "png", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index ca388de60c..51ea5b3fe7 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -33,16 +33,6 @@ "key": "publish", "label": "Publish plugins", "children": [ - { - "type": "schema_template", - "name": "template_publish_plugin", - "template_data": [ - { - "key": "ValidateContainers", - "label": "ValidateContainers" - } - ] - }, { "type": "dict", "collapsible": true, @@ -108,6 +98,38 @@ } ] }, + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateContainers", + "label": "ValidateContainers" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ValidateNaming", + "label": "Validate naming of subsets and layers", + "children": [ + { + "type": "label", + "label": "Subset cannot contain invalid characters or extract to file would fail" + }, + { + "type": "text", + "key": "invalid_chars", + "label": "Regex pattern of invalid characters" + }, + { + "type": "text", + "key": "replace_char", + "label": "Replacement character" + } + ] + }, { "type": "dict", "collapsible": true, From 9ec197acdaf7d064842214bd4540e8dec261ddc0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 16:53:35 +0100 Subject: [PATCH 260/307] added method get_timer_data_for_context to be able get timers related data for passed context --- .../timers_manager/timers_manager.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 0f165ff0ac..6541a9197c 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -151,6 +151,44 @@ class TimersManager(OpenPypeModule, ITrayService): self._idle_manager.stop() self._idle_manager.wait() + def get_timer_data_for_context(self, project_name, asset_name, task_name): + """Prepare data for timer related callbacks.""" + dbconn = AvalonMongoDB() + dbconn.install() + dbconn.Session["AVALON_PROJECT"] = project_name + + asset_doc = dbconn.find_one( + { + "type": "asset", + "name": asset_name + }, + { + "data.tasks": True, + "data.parents": True + } + ) + if not asset_doc: + raise ValueError("Uknown asset {}".format(asset_name)) + + asset_data = asset_doc.get("data") or {} + task_type = "" + try: + task_type = asset_data["tasks"][task_name]["type"] + except KeyError: + self.log.warning( + "Couldn't find task_type for {}".format(task_name) + ) + + hierarchy_items = asset_data.get("parents") or [] + hierarchy_items.append(asset_name) + + return { + "project_name": project_name, + "task_name": task_name, + "task_type": task_type, + "hierarchy": hierarchy_items + } + def start_timer(self, project_name, asset_name, task_name, hierarchy): """ Start timer for 'project_name', 'asset_name' and 'task_name' From 246bf8f1403cb9bc40cab5498eddb85f771cabe6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 16:54:16 +0100 Subject: [PATCH 261/307] use get_timer_data_for_context in start_timer and don't expect hierarchy --- .../timers_manager/rest_api.py | 15 +++--- .../timers_manager/timers_manager.py | 47 ++++--------------- 2 files changed, 18 insertions(+), 44 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/rest_api.py b/openpype/modules/default_modules/timers_manager/rest_api.py index 19b72d688b..4296610c23 100644 --- a/openpype/modules/default_modules/timers_manager/rest_api.py +++ b/openpype/modules/default_modules/timers_manager/rest_api.py @@ -39,17 +39,18 @@ class TimersManagerModuleRestApi: async def start_timer(self, request): data = await request.json() try: - project_name = data['project_name'] - asset_name = data['asset_name'] - task_name = data['task_name'] - hierarchy = data['hierarchy'] + project_name = data["project_name"] + asset_name = data["asset_name"] + task_name = data["task_name"] except KeyError: - log.error("Payload must contain fields 'project_name, " + - "'asset_name', 'task_name', 'hierarchy'") + log.error(( + "Payload must contain fields 'project_name," + " 'asset_name' and 'task_name'" + )) return Response(status=400) self.module.stop_timers() - self.module.start_timer(project_name, asset_name, task_name, hierarchy) + self.module.start_timer(project_name, asset_name, task_name) return Response(status=200) async def stop_timer(self, request): diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 6541a9197c..fc9897b022 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -189,44 +189,17 @@ class TimersManager(OpenPypeModule, ITrayService): "hierarchy": hierarchy_items } - def start_timer(self, project_name, asset_name, task_name, hierarchy): + def start_timer(self, project_name, asset_name, task_name): + """Start timer for passed context. + + Args: + project_name (str): Project name + asset_name (str): Asset name + task_name (str): Task name """ - Start timer for 'project_name', 'asset_name' and 'task_name' - - Called from REST api by hosts. - - Args: - project_name (string) - asset_name (string) - task_name (string) - hierarchy (string) - """ - dbconn = AvalonMongoDB() - dbconn.install() - dbconn.Session["AVALON_PROJECT"] = project_name - - asset_doc = dbconn.find_one({ - "type": "asset", "name": asset_name - }) - if not asset_doc: - raise ValueError("Uknown asset {}".format(asset_name)) - - task_type = '' - try: - task_type = asset_doc["data"]["tasks"][task_name]["type"] - except KeyError: - self.log.warning("Couldn't find task_type for {}". - format(task_name)) - - hierarchy = hierarchy.split("\\") - hierarchy.append(asset_name) - - data = { - "project_name": project_name, - "task_name": task_name, - "task_type": task_type, - "hierarchy": hierarchy - } + data = self.get_timer_data_for_context( + project_name, asset_name, task_name + ) self.timer_started(None, data) def get_task_time(self, project_name, asset_name, task_name): From 83887b918a507d7a7ffce528c39f6c0ac7227415 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 16:55:05 +0100 Subject: [PATCH 262/307] changed 'change_timer_from_host' to static method 'start_timer_with_webserver' --- .../timers_manager/timers_manager.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index fc9897b022..8aa8e18902 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -306,18 +306,29 @@ class TimersManager(OpenPypeModule, ITrayService): self, server_manager ) - def change_timer_from_host(self, project_name, asset_name, task_name): + @staticmethod + def start_timer_with_webserver( + project_name, asset_name, task_name, logger=None + ): """Prepared method for calling change timers on REST api""" webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") if not webserver_url: - self.log.warning("Couldn't find webserver url") + msg = "Couldn't find webserver url" + if logger is not None: + logger.warning(msg) + else: + print(msg) return rest_api_url = "{}/timers_manager/start_timer".format(webserver_url) try: import requests except Exception: - self.log.warning("Couldn't start timer") + msg = "Couldn't start timer ('requests' is not available)" + if logger is not None: + logger.warning(msg) + else: + print(msg) return data = { "project_name": project_name, From fb096d0f68b3dc3171b6f9f0802d51d542b89bb2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 16:56:09 +0100 Subject: [PATCH 263/307] don't pass hierarchy in lib function --- openpype/lib/avalon_context.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index e3bceff275..cb5bca133d 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -1433,7 +1433,11 @@ def get_creator_by_name(creator_name, case_sensitive=False): @with_avalon def change_timer_to_current_context(): - """Called after context change to change timers""" + """Called after context change to change timers. + + TODO: + - use TimersManager's static method instead of reimplementing it here + """ webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") if not webserver_url: log.warning("Couldn't find webserver url") @@ -1448,8 +1452,7 @@ def change_timer_to_current_context(): data = { "project_name": avalon.io.Session["AVALON_PROJECT"], "asset_name": avalon.io.Session["AVALON_ASSET"], - "task_name": avalon.io.Session["AVALON_TASK"], - "hierarchy": get_hierarchy() + "task_name": avalon.io.Session["AVALON_TASK"] } requests.post(rest_api_url, json=data) From c777bc8afcfebac4009ee2d9ab77e9558c8f3483 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 16:56:31 +0100 Subject: [PATCH 264/307] application launch context nad launch hooks have access to modules manager --- openpype/lib/applications.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 6eb44a9694..184a57ea89 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -640,6 +640,10 @@ class LaunchHook: def app_name(self): return getattr(self.application, "full_name", None) + @property + def modules_manager(self): + return getattr(self.launch_context, "modules_manager", None) + def validate(self): """Optional validation of launch hook on initialization. @@ -702,9 +706,13 @@ class ApplicationLaunchContext: """ def __init__(self, application, executable, **data): + from openpype.modules import ModulesManager + # Application object self.application = application + self.modules_manager = ModulesManager() + # Logger logger_name = "{}-{}".format(self.__class__.__name__, self.app_name) self.log = PypeLogger.get_logger(logger_name) @@ -812,10 +820,7 @@ class ApplicationLaunchContext: paths.append(path) # Load modules paths - from openpype.modules import ModulesManager - - manager = ModulesManager() - paths.extend(manager.collect_launch_hook_paths()) + paths.extend(self.modules_manager.collect_launch_hook_paths()) return paths From 272f4fcc67f5f6ea9d3dffb4eb97b57848389bd5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 16:56:59 +0100 Subject: [PATCH 265/307] added new post launch hook that will trigger start timer using timers manager --- openpype/hooks/post_start_timer.py | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 openpype/hooks/post_start_timer.py diff --git a/openpype/hooks/post_start_timer.py b/openpype/hooks/post_start_timer.py new file mode 100644 index 0000000000..be2cec82b7 --- /dev/null +++ b/openpype/hooks/post_start_timer.py @@ -0,0 +1,48 @@ +import os + +from openpype.api import get_project_settings +from openpype.lib import PostLaunchHook + + +class PostStartTimerHook(PostLaunchHook): + """Start timer with TimersManager module. + + This module requires enabled TimerManager module. + """ + order = None + + def execute(self): + project_name = self.data.get("project_name") + asset_name = self.data.get("asset_name") + task_name = self.data.get("task_name") + + missing_context_keys = set() + if not project_name: + missing_context_keys.add("project_name") + if not asset_name: + missing_context_keys.add("asset_name") + if not task_name: + missing_context_keys.add("task_name") + + if missing_context_keys: + missing_keys_str = ", ".join([ + "\"{}\"".format(key) for key in missing_context_keys + ]) + self.log.debug("Hook {} skipped. Missing data keys: {}".format( + self.__class__.__name__, missing_keys_str + )) + return + + timers_manager = self.modules_manager.modules_by_name.get( + "timers_manager" + ) + if not timers_manager or not timers_manager.enabled: + self.log.info(( + "Skipping starting timer because" + " TimersManager is not available." + )) + return + + timers_manager.start_timer_with_webserver( + project_name, asset_name, task_name, logger=self.log + ) From def385836b88a3626e09fb338121ed5223695572 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 16:57:13 +0100 Subject: [PATCH 266/307] removed start timer in ftrack's post launch hook --- .../launch_hooks/post_ftrack_changes.py | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/launch_hooks/post_ftrack_changes.py b/openpype/modules/default_modules/ftrack/launch_hooks/post_ftrack_changes.py index df16cde2b8..d5a95fad91 100644 --- a/openpype/modules/default_modules/ftrack/launch_hooks/post_ftrack_changes.py +++ b/openpype/modules/default_modules/ftrack/launch_hooks/post_ftrack_changes.py @@ -52,7 +52,7 @@ class PostFtrackHook(PostLaunchHook): ) if entity: self.ftrack_status_change(session, entity, project_name) - self.start_timer(session, entity, ftrack_api) + except Exception: self.log.warning( "Couldn't finish Ftrack procedure.", exc_info=True @@ -160,26 +160,3 @@ class PostFtrackHook(PostLaunchHook): " on Ftrack entity type \"{}\"" ).format(next_status_name, entity.entity_type) self.log.warning(msg) - - def start_timer(self, session, entity, _ftrack_api): - """Start Ftrack timer on task from context.""" - self.log.debug("Triggering timer start.") - - user_entity = session.query("User where username is \"{}\"".format( - os.environ["FTRACK_API_USER"] - )).first() - if not user_entity: - self.log.warning( - "Couldn't find user with username \"{}\" in Ftrack".format( - os.environ["FTRACK_API_USER"] - ) - ) - return - - try: - user_entity.start_timer(entity, force=True) - session.commit() - self.log.debug("Timer start triggered successfully.") - - except Exception: - self.log.warning("Couldn't trigger Ftrack timer.", exc_info=True) From 5528f064130663dda83bce12cc60a906f9c54733 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 17:05:19 +0100 Subject: [PATCH 267/307] added few docstrings --- .../timers_manager/timers_manager.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 8aa8e18902..5fffb24a5e 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -152,7 +152,11 @@ class TimersManager(OpenPypeModule, ITrayService): self._idle_manager.wait() def get_timer_data_for_context(self, project_name, asset_name, task_name): - """Prepare data for timer related callbacks.""" + """Prepare data for timer related callbacks. + + TODO: + - return predefined object that has access to asset document etc. + """ dbconn = AvalonMongoDB() dbconn.install() dbconn.Session["AVALON_PROJECT"] = project_name @@ -203,6 +207,11 @@ class TimersManager(OpenPypeModule, ITrayService): self.timer_started(None, data) def get_task_time(self, project_name, asset_name, task_name): + """Get total time for passed context. + + TODO: + - convert context to timer data + """ times = {} for module_id, connector in self._connectors_by_module_id.items(): if hasattr(connector, "get_task_time"): @@ -213,6 +222,10 @@ class TimersManager(OpenPypeModule, ITrayService): return times def timer_started(self, source_id, data): + """Connector triggered that timer has started. + + New timer has started for context in data. + """ for module_id, connector in self._connectors_by_module_id.items(): if module_id == source_id: continue @@ -230,6 +243,14 @@ class TimersManager(OpenPypeModule, ITrayService): self.is_running = True def timer_stopped(self, source_id): + """Connector triggered that hist timer has stopped. + + Should stop all other timers. + + TODO: + - pass context for which timer has stopped to validate if timers are + same and valid + """ for module_id, connector in self._connectors_by_module_id.items(): if module_id == source_id: continue @@ -248,6 +269,7 @@ class TimersManager(OpenPypeModule, ITrayService): self.timer_started(None, self.last_task) def stop_timers(self): + """Stop all timers.""" if self.is_running is False: return From 1984f9d8b484f1f37beb4ae9abf1b62448d36df8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 17:05:42 +0100 Subject: [PATCH 268/307] added helper method for future use --- .../timers_manager/timers_manager.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 5fffb24a5e..7c8ad25529 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -151,6 +151,22 @@ class TimersManager(OpenPypeModule, ITrayService): self._idle_manager.stop() self._idle_manager.wait() + def get_timer_data_for_path(self, task_path): + """Convert string path to a timer data. + + It is expected that first item is project name, last item is task name + and parent asset name is before task name. + """ + path_items = task_path.split("/") + if len(path_items) < 3: + raise ValueError("Invalid path") + task_name = path_items.pop(-1) + asset_name = path_items.pop(-1) + project_name = path_items.pop(0) + return self.get_timer_data_for_context( + project_name, asset_name, task_name + ) + def get_timer_data_for_context(self, project_name, asset_name, task_name): """Prepare data for timer related callbacks. From f4aa2d17b25b748ba9523c246e2fe86989af77bd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 17:12:04 +0100 Subject: [PATCH 269/307] added few lines of docstring --- .../timers_manager/timers_manager.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 7c8ad25529..0ae0af4cdb 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -348,7 +348,18 @@ class TimersManager(OpenPypeModule, ITrayService): def start_timer_with_webserver( project_name, asset_name, task_name, logger=None ): - """Prepared method for calling change timers on REST api""" + """Prepared method for calling change timers on REST api. + + Webserver must be active. At the moment is Webserver running only when + OpenPype Tray is used. + + Args: + project_name (str): Project name. + asset_name (str): Asset name. + task_name (str): Task name. + logger (logging.Logger): Logger object. Using 'print' if not + passed. + """ webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") if not webserver_url: msg = "Couldn't find webserver url" From 63b7b56547cd395fa5336a7a8abe36ea9888edb2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 16 Dec 2021 17:12:12 +0100 Subject: [PATCH 270/307] OP-2038 - fix use correct value --- openpype/hosts/photoshop/plugins/publish/validate_naming.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/publish/validate_naming.py b/openpype/hosts/photoshop/plugins/publish/validate_naming.py index 077f7cf132..7c9ad1e923 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_naming.py @@ -22,6 +22,7 @@ class ValidateNamingRepair(pyblish.api.Action): failed.append(result["instance"]) invalid_chars, replace_char = plugin.get_replace_chars() + self.log.info("{} --- {}".format(invalid_chars, replace_char)) # Apply pyblish.logic to get the instances for the plug-in instances = pyblish.api.instances_by_plugin(failed, plugin) @@ -37,7 +38,7 @@ class ValidateNamingRepair(pyblish.api.Action): if layer_data: layer_name = re.sub(invalid_chars, replace_char, - layer_name) + layer_data.name) stub.rename_layer(instance.data["uuid"], layer_name) From 27dc4552eb2adb3dbdbab72d2b06558cec94fbb2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 16 Dec 2021 17:14:34 +0100 Subject: [PATCH 271/307] OP-2038 - Hound --- openpype/hosts/photoshop/plugins/publish/validate_naming.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/validate_naming.py b/openpype/hosts/photoshop/plugins/publish/validate_naming.py index 7c9ad1e923..1635096f4b 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_naming.py @@ -78,8 +78,7 @@ class ValidateNaming(pyblish.api.InstancePlugin): help_msg) assert not re.search(self.invalid_chars, instance.data["subset"]), msg - @classmethod def get_replace_chars(cls): """Pass values configured in Settings for Repair.""" - return cls.invalid_chars, cls.replace_char \ No newline at end of file + return cls.invalid_chars, cls.replace_char From d4112eb3fafbaf2c817d7771c12e40b36701f2a0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 17:15:27 +0100 Subject: [PATCH 272/307] moved the launch hook into timers manager --- .../launch_hooks}/post_start_timer.py | 3 --- .../timers_manager/timers_manager.py | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) rename openpype/{hooks => modules/default_modules/timers_manager/launch_hooks}/post_start_timer.py (96%) diff --git a/openpype/hooks/post_start_timer.py b/openpype/modules/default_modules/timers_manager/launch_hooks/post_start_timer.py similarity index 96% rename from openpype/hooks/post_start_timer.py rename to openpype/modules/default_modules/timers_manager/launch_hooks/post_start_timer.py index be2cec82b7..d6ae013403 100644 --- a/openpype/hooks/post_start_timer.py +++ b/openpype/modules/default_modules/timers_manager/launch_hooks/post_start_timer.py @@ -1,6 +1,3 @@ -import os - -from openpype.api import get_project_settings from openpype.lib import PostLaunchHook diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 0ae0af4cdb..964e6d9a58 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -1,7 +1,10 @@ import os import platform from openpype.modules import OpenPypeModule -from openpype_interfaces import ITrayService +from openpype_interfaces import ( + ITrayService, + ILaunchHookPaths +) from avalon.api import AvalonMongoDB @@ -64,7 +67,7 @@ class ExampleTimersManagerConnector: self._timers_manager_module.timer_stopped(self._module.id) -class TimersManager(OpenPypeModule, ITrayService): +class TimersManager(OpenPypeModule, ITrayService, ILaunchHookPaths): """ Handles about Timers. Should be able to start/stop all timers at once. @@ -167,6 +170,13 @@ class TimersManager(OpenPypeModule, ITrayService): project_name, asset_name, task_name ) + def get_launch_hook_paths(self): + """Implementation of `ILaunchHookPaths`.""" + return os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "launch_hooks" + ) + def get_timer_data_for_context(self, project_name, asset_name, task_name): """Prepare data for timer related callbacks. From f1b21fedf17aa95aeea56c80f20579a8d517c9c8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 17:21:36 +0100 Subject: [PATCH 273/307] moved job queue module one hierarchy higher from default_modules --- openpype/modules/{default_modules => }/job_queue/__init__.py | 0 .../{default_modules => }/job_queue/job_server/__init__.py | 0 .../{default_modules => }/job_queue/job_server/job_queue_route.py | 0 .../modules/{default_modules => }/job_queue/job_server/jobs.py | 0 .../modules/{default_modules => }/job_queue/job_server/server.py | 0 .../modules/{default_modules => }/job_queue/job_server/utils.py | 0 .../modules/{default_modules => }/job_queue/job_server/workers.py | 0 .../job_queue/job_server/workers_rpc_route.py | 0 .../{default_modules => }/job_queue/job_workers/__init__.py | 0 .../{default_modules => }/job_queue/job_workers/base_worker.py | 0 openpype/modules/{default_modules => }/job_queue/module.py | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename openpype/modules/{default_modules => }/job_queue/__init__.py (100%) rename openpype/modules/{default_modules => }/job_queue/job_server/__init__.py (100%) rename openpype/modules/{default_modules => }/job_queue/job_server/job_queue_route.py (100%) rename openpype/modules/{default_modules => }/job_queue/job_server/jobs.py (100%) rename openpype/modules/{default_modules => }/job_queue/job_server/server.py (100%) rename openpype/modules/{default_modules => }/job_queue/job_server/utils.py (100%) rename openpype/modules/{default_modules => }/job_queue/job_server/workers.py (100%) rename openpype/modules/{default_modules => }/job_queue/job_server/workers_rpc_route.py (100%) rename openpype/modules/{default_modules => }/job_queue/job_workers/__init__.py (100%) rename openpype/modules/{default_modules => }/job_queue/job_workers/base_worker.py (100%) rename openpype/modules/{default_modules => }/job_queue/module.py (100%) diff --git a/openpype/modules/default_modules/job_queue/__init__.py b/openpype/modules/job_queue/__init__.py similarity index 100% rename from openpype/modules/default_modules/job_queue/__init__.py rename to openpype/modules/job_queue/__init__.py diff --git a/openpype/modules/default_modules/job_queue/job_server/__init__.py b/openpype/modules/job_queue/job_server/__init__.py similarity index 100% rename from openpype/modules/default_modules/job_queue/job_server/__init__.py rename to openpype/modules/job_queue/job_server/__init__.py diff --git a/openpype/modules/default_modules/job_queue/job_server/job_queue_route.py b/openpype/modules/job_queue/job_server/job_queue_route.py similarity index 100% rename from openpype/modules/default_modules/job_queue/job_server/job_queue_route.py rename to openpype/modules/job_queue/job_server/job_queue_route.py diff --git a/openpype/modules/default_modules/job_queue/job_server/jobs.py b/openpype/modules/job_queue/job_server/jobs.py similarity index 100% rename from openpype/modules/default_modules/job_queue/job_server/jobs.py rename to openpype/modules/job_queue/job_server/jobs.py diff --git a/openpype/modules/default_modules/job_queue/job_server/server.py b/openpype/modules/job_queue/job_server/server.py similarity index 100% rename from openpype/modules/default_modules/job_queue/job_server/server.py rename to openpype/modules/job_queue/job_server/server.py diff --git a/openpype/modules/default_modules/job_queue/job_server/utils.py b/openpype/modules/job_queue/job_server/utils.py similarity index 100% rename from openpype/modules/default_modules/job_queue/job_server/utils.py rename to openpype/modules/job_queue/job_server/utils.py diff --git a/openpype/modules/default_modules/job_queue/job_server/workers.py b/openpype/modules/job_queue/job_server/workers.py similarity index 100% rename from openpype/modules/default_modules/job_queue/job_server/workers.py rename to openpype/modules/job_queue/job_server/workers.py diff --git a/openpype/modules/default_modules/job_queue/job_server/workers_rpc_route.py b/openpype/modules/job_queue/job_server/workers_rpc_route.py similarity index 100% rename from openpype/modules/default_modules/job_queue/job_server/workers_rpc_route.py rename to openpype/modules/job_queue/job_server/workers_rpc_route.py diff --git a/openpype/modules/default_modules/job_queue/job_workers/__init__.py b/openpype/modules/job_queue/job_workers/__init__.py similarity index 100% rename from openpype/modules/default_modules/job_queue/job_workers/__init__.py rename to openpype/modules/job_queue/job_workers/__init__.py diff --git a/openpype/modules/default_modules/job_queue/job_workers/base_worker.py b/openpype/modules/job_queue/job_workers/base_worker.py similarity index 100% rename from openpype/modules/default_modules/job_queue/job_workers/base_worker.py rename to openpype/modules/job_queue/job_workers/base_worker.py diff --git a/openpype/modules/default_modules/job_queue/module.py b/openpype/modules/job_queue/module.py similarity index 100% rename from openpype/modules/default_modules/job_queue/module.py rename to openpype/modules/job_queue/module.py From fd7ed342337aa4272e499a38ee2dbde58491b626 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 17:21:49 +0100 Subject: [PATCH 274/307] added 'job_queue' module to default modules list --- openpype/modules/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index a1df3cfd14..b5c491a1c0 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -41,6 +41,7 @@ DEFAULT_OPENPYPE_MODULES = ( "project_manager_action", "settings_action", "standalonepublish_action", + "job_queue", ) From 449487031c37b8e881d30d8e642b4f4f0ed5a71d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Dec 2021 11:37:54 +0100 Subject: [PATCH 275/307] type label can contain links that can lead to settings or open standard urls --- openpype/tools/settings/settings/base.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index b10c958880..8a420c2447 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -554,7 +554,9 @@ class GUIWidget(BaseWidget): def _create_label_ui(self): label = self.entity["label"] label_widget = QtWidgets.QLabel(label, self) + label_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) label_widget.setObjectName("SettingsLabel") + label_widget.linkActivated.connect(self._on_link_activate) layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 5, 0, 5) @@ -570,6 +572,14 @@ class GUIWidget(BaseWidget): layout.setContentsMargins(5, 5, 5, 5) layout.addWidget(splitter_item) + def _on_link_activate(self, url): + if not url.startswith("settings://"): + QtGui.QDesktopServices.openUrl(url) + return + + path = url.replace("settings://", "") + self.category_widget.go_to_fullpath(path) + def set_entity_value(self): pass From c647649de0ee578217b1afde4141fac2789ef145 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Dec 2021 11:38:18 +0100 Subject: [PATCH 276/307] category widget can trigger change of full path --- openpype/tools/settings/settings/categories.py | 1 + openpype/tools/settings/settings/window.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 029619849e..724399c443 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -82,6 +82,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): state_changed = QtCore.Signal() saved = QtCore.Signal(QtWidgets.QWidget) restart_required_trigger = QtCore.Signal() + full_path_requested = QtCore.Signal(str, str) def __init__(self, user_role, parent=None): super(SettingsCategoryWidget, self).__init__(parent) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index fd0cd1d7cd..c376e5e91e 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -63,7 +63,9 @@ class MainWidget(QtWidgets.QWidget): tab_widget.restart_required_trigger.connect( self._on_restart_required ) + tab_widget.full_path_requested.connect(self._on_full_path_request) + self._header_tab_widget = header_tab_widget self.tab_widgets = tab_widgets def _on_tab_save(self, source_widget): @@ -90,6 +92,14 @@ class MainWidget(QtWidgets.QWidget): if app: app.processEvents() + def _on_full_path_request(self, category, path): + for tab_widget in self.tab_widgets: + if tab_widget.contain_category_key(category): + idx = self._header_tab_widget.indexOf(tab_widget) + self._header_tab_widget.setCurrentIndex(idx) + tab_widget.set_category_path(category, path) + break + def showEvent(self, event): super(MainWidget, self).showEvent(event) if self._reset_on_show: From a5082df4634f35010ed3194b8fc3147ef36aab5a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Dec 2021 11:41:19 +0100 Subject: [PATCH 277/307] added new methods for category widget that can change full path (lead to different category) --- .../tools/settings/settings/categories.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 724399c443..d7650b8663 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -269,6 +269,37 @@ class SettingsCategoryWidget(QtWidgets.QWidget): # Scroll to widget self.scroll_widget.ensureWidgetVisible(widget) + def go_to_fullpath(self, full_path): + """Full path of settings entity which can lead to different category. + + Args: + full_path (str): Full path to settings entity. It is expected that + path starts with category name ("system_setting" etc.). + """ + if not full_path: + return + items = full_path.split("/") + category = items[0] + path = "" + if len(items) > 1: + path = "/".join(items[1:]) + self.full_path_requested.emit(category, path) + + def contain_category_key(self, category): + """Parent widget ask if category of full path lead to this widget. + + Args: + category (str): The category name. + + Returns: + bool: Passed category lead to this widget. + """ + return False + + def set_category_path(self, category, path): + """Change path of widget based on category full path.""" + pass + def set_path(self, path): self.breadcrumbs_widget.set_path(path) From 61dff2c51cb0b6253affa27604c81af9cabac7ee Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Dec 2021 11:41:38 +0100 Subject: [PATCH 278/307] override required methods for both current categories --- .../tools/settings/settings/categories.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index d7650b8663..b046085975 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -587,6 +587,14 @@ class SettingsCategoryWidget(QtWidgets.QWidget): class SystemWidget(SettingsCategoryWidget): + def contain_category_key(self, category): + if category == "system_settings": + return True + return False + + def set_category_path(self, category, path): + self.breadcrumbs_widget.change_path(path) + def _create_root_entity(self): self.entity = SystemSettings(set_studio_state=False) self.entity.on_change_callbacks.append(self._on_entity_change) @@ -623,6 +631,21 @@ class SystemWidget(SettingsCategoryWidget): class ProjectWidget(SettingsCategoryWidget): + def contain_category_key(self, category): + if category in ("project_settings", "project_anatomy"): + return True + return False + + def set_category_path(self, category, path): + if path: + path_items = path.split("/") + if path_items[0] not in ("project_settings", "project_anatomy"): + path = "/".join([category, path]) + else: + path = category + + self.breadcrumbs_widget.change_path(path) + def initialize_attributes(self): self.project_name = None From b79b5369fb3b297016b89242dc69b059f15139e7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 17 Dec 2021 12:03:53 +0100 Subject: [PATCH 279/307] Merge current avalon-core main --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index 85c656fcf9..4f10fb1255 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit 85c656fcf9beb06ab92d3d6ce47f6472cf88df54 +Subproject commit 4f10fb1255beb156f23afa1bb8362dfc53d0c6f8 From f8e84de363bd3c24cb8970a1bb2f5eacab7197b2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Dec 2021 14:22:15 +0100 Subject: [PATCH 280/307] implemented tool buttons widgets for unreal --- openpype/hosts/unreal/api/tools_ui.py | 158 ++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 openpype/hosts/unreal/api/tools_ui.py diff --git a/openpype/hosts/unreal/api/tools_ui.py b/openpype/hosts/unreal/api/tools_ui.py new file mode 100644 index 0000000000..bcf8dd2c50 --- /dev/null +++ b/openpype/hosts/unreal/api/tools_ui.py @@ -0,0 +1,158 @@ +import sys +from Qt import QtWidgets, QtCore, QtGui + +from openpype import ( + resources, + style +) +from openpype.tools.utils import host_tools +from openpype.tools.utils.lib import qt_app_context + + +class ToolsBtnsWidget(QtWidgets.QWidget): + """Widget containing buttons which are clickable.""" + tool_required = QtCore.Signal(str) + + def __init__(self, parent=None): + super(ToolsBtnsWidget, self).__init__(parent) + + create_btn = QtWidgets.QPushButton("Create...", self) + load_btn = QtWidgets.QPushButton("Load...", self) + publish_btn = QtWidgets.QPushButton("Publish...", self) + manage_btn = QtWidgets.QPushButton("Manage...", self) + experimental_tools_btn = QtWidgets.QPushButton( + "Experimental tools...", self + ) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(create_btn, 0) + layout.addWidget(load_btn, 0) + layout.addWidget(publish_btn, 0) + layout.addWidget(manage_btn, 0) + layout.addWidget(experimental_tools_btn, 0) + layout.addStretch(1) + + create_btn.clicked.connect(self._on_create) + load_btn.clicked.connect(self._on_load) + publish_btn.clicked.connect(self._on_publish) + manage_btn.clicked.connect(self._on_manage) + experimental_tools_btn.clicked.connect(self._on_experimental) + + def _on_create(self): + self.tool_required.emit("creator") + + def _on_load(self): + self.tool_required.emit("loader") + + def _on_publish(self): + self.tool_required.emit("publish") + + def _on_manage(self): + self.tool_required.emit("sceneinventory") + + def _on_experimental(self): + self.tool_required.emit("experimental_tools") + + +class ToolsDialog(QtWidgets.QDialog): + """Dialog with tool buttons that will stay opened until user close it.""" + def __init__(self, *args, **kwargs): + super(ToolsDialog, self).__init__(*args, **kwargs) + + self.setWindowTitle("OpenPype tools") + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) + self.setWindowIcon(icon) + + self.setWindowFlags( + QtCore.Qt.Window + | QtCore.Qt.WindowStaysOnTopHint + ) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + tools_widget = ToolsBtnsWidget(self) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(tools_widget) + + tools_widget.tool_required.connect(self._on_tool_require) + self._tools_widget = tools_widget + + self._first_show = True + + def sizeHint(self): + result = super(ToolsDialog, self).sizeHint() + result.setWidth(result.width() * 2) + return result + + def showEvent(self, event): + super(ToolsDialog, self).showEvent(event) + if self._first_show: + self.setStyleSheet(style.load_stylesheet()) + self._first_show = False + + def _on_tool_require(self, tool_name): + host_tools.show_tool_by_name(tool_name, parent=self) + + +class ToolsPopup(ToolsDialog): + """Popup with tool buttons that will close when loose focus.""" + def __init__(self, *args, **kwargs): + super(ToolsPopup, self).__init__(*args, **kwargs) + + self.setWindowFlags( + QtCore.Qt.FramelessWindowHint + | QtCore.Qt.Popup + ) + + def showEvent(self, event): + super(ToolsPopup, self).showEvent(event) + app = QtWidgets.QApplication.instance() + app.processEvents() + pos = QtGui.QCursor.pos() + self.move(pos) + + +class WindowCache: + """Cached objects and methods to be used in global scope.""" + _dialog = None + _popup = None + _first_show = True + + @classmethod + def _before_show(cls): + """Create QApplication if does not exists yet."""" + if not cls._first_show: + return + + cls._first_show = False + if not QtWidgets.QApplication.instance(): + QtWidgets.QApplication(sys.argv) + + @classmethod + def show_popup(cls): + cls._before_show() + with qt_app_context(): + if cls._popup is None: + cls._popup = ToolsPopup() + + cls._popup.show() + + @classmethod + def show_dialog(cls): + cls._before_show() + with qt_app_context(): + if cls._dialog is None: + cls._dialog = ToolsDialog() + + cls._dialog.show() + cls._dialog.raise_() + cls._dialog.activateWindow() + + +def show_tools_popup(): + WindowCache.show_popup() + + +def show_tools_dialog(): + WindowCache.show_dialog() From 96c88cec25db6feb01aa7a9894582f5f93156fc6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Dec 2021 14:30:30 +0100 Subject: [PATCH 281/307] fix comment --- openpype/hosts/unreal/api/tools_ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/api/tools_ui.py b/openpype/hosts/unreal/api/tools_ui.py index bcf8dd2c50..93361c3574 100644 --- a/openpype/hosts/unreal/api/tools_ui.py +++ b/openpype/hosts/unreal/api/tools_ui.py @@ -121,7 +121,7 @@ class WindowCache: @classmethod def _before_show(cls): - """Create QApplication if does not exists yet."""" + """Create QApplication if does not exists yet.""" if not cls._first_show: return From de5a4ca0a184115efcbccbef5f5d3b64a4b964c1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Dec 2021 17:48:25 +0100 Subject: [PATCH 282/307] added functin to parse environment data based on passed information --- openpype/lib/applications.py | 94 ++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 6eb44a9694..bbbd335a73 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -41,6 +41,100 @@ from .python_module_tools import ( _logger = None +PLATFORM_NAMES = {"windows", "linux", "darwin"} +DEFAULT_ENV_SUBGROUP = "standard" + + +def parse_environments(env_data, env_group=None, platform_name=None): + """Parse environment values from settings byt group and platfrom. + + Data may contain up to 2 hierarchical levels of dictionaries. At the end + of the last level must be string or list. List is joined using platform + specific joiner (';' for windows and ':' for linux and mac). + + Hierarchical levels can contain keys for subgroups and platform name. + Platform specific values must be always last level of dictionary. Platform + names are "windows" (MS Windows), "linux" (any linux distribution) and + "darwin" (any MacOS distribution). + + Subgroups are helpers added mainly for standard and on farm usage. Farm + may require different environments for e.g. licence related values or + plugins. Default subgroup is "standard". + + Examples: + ``` + { + # Unchanged value + "ENV_KEY1": "value", + # Empty values are kept (unset environment variable) + "ENV_KEY2": "", + + # Join list values with ':' or ';' + "ENV_KEY3": ["value1", "value2"], + + # Environment groups + "ENV_KEY4": { + "standard": "DEMO_SERVER_URL", + "farm": "LICENCE_SERVER_URL" + }, + + # Platform specific (and only for windows and mac) + "ENV_KEY5": { + "windows": "windows value", + "darwin": ["value 1", "value 2"] + }, + + # Environment groups and platform combination + "ENV_KEY6": { + "farm": "FARM_VALUE", + "standard": { + "windows": ["value1", "value2"], + "linux": "value1", + "darwin": "" + } + } + } + ``` + + Args: + + """ + output = {} + if not env_data: + return output + + if not env_group: + env_group = DEFAULT_ENV_SUBGROUP + + if not platform_name: + platform_name = platform.system().lower() + + for key, value in env_data.items(): + if isinstance(value, dict): + # Look if any key is platform key + # - expect that represents environment group if does not contain + # platform keys + if not PLATFORM_NAMES.intersection(set(value.keys())): + # Skip the key if group is not available + if env_group not in value: + continue + value = value[env_group] + + # Check again if value is dictionary + # - this time there should be only platform keys + if isinstance(value, dict): + value = value.get(platform_name) + + # Check if value is list and join it's values + # QUESTION Should empty values be skipped? + if isinstance(value, (list, tuple)): + value = os.pathsep.join(value) + + # Set key to output if value is string + if isinstance(value, six.string_types): + output[key] = value + return output + def get_logger(): """Global lib.applications logger getter.""" From e3532074c50e9cf1e6f1342fa896f8201dff568a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Dec 2021 17:49:51 +0100 Subject: [PATCH 283/307] environment group is part of all environment related functions and application launch context --- openpype/hooks/pre_global_host_data.py | 2 +- openpype/lib/applications.py | 28 +++++++++++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/openpype/hooks/pre_global_host_data.py b/openpype/hooks/pre_global_host_data.py index b32fb5e44a..6b08cdb444 100644 --- a/openpype/hooks/pre_global_host_data.py +++ b/openpype/hooks/pre_global_host_data.py @@ -48,7 +48,7 @@ class GlobalHostDataHook(PreLaunchHook): "log": self.log }) - prepare_host_environments(temp_data) + prepare_host_environments(temp_data, self.launch_context.env_group) prepare_context_environments(temp_data) temp_data.pop("log") diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index bbbd335a73..2e301adf03 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -795,7 +795,7 @@ class ApplicationLaunchContext: preparation to store objects usable in multiple places. """ - def __init__(self, application, executable, **data): + def __init__(self, application, executable, env_group=None, **data): # Application object self.application = application @@ -805,6 +805,11 @@ class ApplicationLaunchContext: self.executable = executable + if env_group is None: + env_group = DEFAULT_ENV_SUBGROUP + + self.env_group = env_group + self.data = dict(data) # subprocess.Popen launch arguments (first argument in constructor) @@ -1141,7 +1146,7 @@ class EnvironmentPrepData(dict): def get_app_environments_for_context( - project_name, asset_name, task_name, app_name, env=None + project_name, asset_name, task_name, app_name, env_group=None, env=None ): """Prepare environment variables by context. Args: @@ -1193,8 +1198,8 @@ def get_app_environments_for_context( "env": env }) - prepare_host_environments(data) - prepare_context_environments(data) + prepare_host_environments(data, env_group) + prepare_context_environments(data, env_group) # Discard avalon connection dbcon.uninstall() @@ -1214,7 +1219,7 @@ def _merge_env(env, current_env): return result -def prepare_host_environments(data, implementation_envs=True): +def prepare_host_environments(data, env_group=None, implementation_envs=True): """Modify launch environments based on launched app and context. Args: @@ -1268,7 +1273,7 @@ def prepare_host_environments(data, implementation_envs=True): continue # Choose right platform - tool_env = acre.parse(_env_values) + tool_env = parse_environments(_env_values, env_group) # Merge dictionaries env_values = _merge_env(tool_env, env_values) @@ -1300,7 +1305,9 @@ def prepare_host_environments(data, implementation_envs=True): data["env"].pop(key, None) -def apply_project_environments_value(project_name, env, project_settings=None): +def apply_project_environments_value( + project_name, env, project_settings=None, env_group=None +): """Apply project specific environments on passed environments. The enviornments are applied on passed `env` argument value so it is not @@ -1328,14 +1335,15 @@ def apply_project_environments_value(project_name, env, project_settings=None): env_value = project_settings["global"]["project_environments"] if env_value: + parsed_value = parse_environments(env_value, env_group) env.update(acre.compute( - _merge_env(acre.parse(env_value), env), + _merge_env(parsed_value, env), cleanup=False )) return env -def prepare_context_environments(data): +def prepare_context_environments(data, env_group=None): """Modify launch environemnts with context data for launched host. Args: @@ -1365,7 +1373,7 @@ def prepare_context_environments(data): data["project_settings"] = project_settings # Apply project specific environments on current env value apply_project_environments_value( - project_name, data["env"], project_settings + project_name, data["env"], project_settings, env_group ) app = data["app"] From 4de80689ed7666cfd87866d591872e2052accf1e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Dec 2021 17:54:02 +0100 Subject: [PATCH 284/307] added environment group to extractenvironments command line argument --- openpype/cli.py | 7 +++++-- openpype/lib/applications.py | 3 --- openpype/pype_commands.py | 9 ++++++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/openpype/cli.py b/openpype/cli.py index 6b20fb5203..6e9c237b0e 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -138,7 +138,10 @@ def webpublisherwebserver(debug, executable, upload_dir, host=None, port=None): @click.option("--asset", help="Asset name", default=None) @click.option("--task", help="Task name", default=None) @click.option("--app", help="Application name", default=None) -def extractenvironments(output_json_path, project, asset, task, app): +@click.option( + "--envgroup", help="Environment group (e.g. \"farm\")", default=None +) +def extractenvironments(output_json_path, project, asset, task, app, envgroup): """Extract environment variables for entered context to a json file. Entered output filepath will be created if does not exists. @@ -149,7 +152,7 @@ def extractenvironments(output_json_path, project, asset, task, app): Context options are "project", "asset", "task", "app" """ PypeCommands.extractenvironments( - output_json_path, project, asset, task, app + output_json_path, project, asset, task, app, envgroup ) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 2e301adf03..bf52daba7c 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -95,9 +95,6 @@ def parse_environments(env_data, env_group=None, platform_name=None): } } ``` - - Args: - """ output = {} if not env_data: diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index a6330bae1f..e25b56744e 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -305,13 +305,16 @@ class PypeCommands: log.info("Publish finished.") @staticmethod - def extractenvironments(output_json_path, project, asset, task, app): - env = os.environ.copy() + def extractenvironments( + output_json_path, project, asset, task, app, env_group + ): if all((project, asset, task, app)): from openpype.api import get_app_environments_for_context env = get_app_environments_for_context( - project, asset, task, app, env + project, asset, task, app, env_group ) + else: + env = os.environ.copy() output_dir = os.path.dirname(output_json_path) if not os.path.exists(output_dir): From eb465b4d266c444193dcfe83f09693ba7e817abf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Dec 2021 17:55:39 +0100 Subject: [PATCH 285/307] deadline global plugin is using new argument "envgroup" --- vendor/deadline/custom/plugins/GlobalJobPreLoad.py | 1 + 1 file changed, 1 insertion(+) diff --git a/vendor/deadline/custom/plugins/GlobalJobPreLoad.py b/vendor/deadline/custom/plugins/GlobalJobPreLoad.py index 0aa5adaa20..ba1e5f6c6a 100644 --- a/vendor/deadline/custom/plugins/GlobalJobPreLoad.py +++ b/vendor/deadline/custom/plugins/GlobalJobPreLoad.py @@ -48,6 +48,7 @@ def inject_openpype_environment(deadlinePlugin): add_args['asset'] = job.GetJobEnvironmentKeyValue('AVALON_ASSET') add_args['task'] = job.GetJobEnvironmentKeyValue('AVALON_TASK') add_args['app'] = job.GetJobEnvironmentKeyValue('AVALON_APP_NAME') + add_args["envgroup"] = "farm" if all(add_args.values()): for key, value in add_args.items(): From db4dc04d8f5f8052c9e98c2c4212748ad52b4aed Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 18 Dec 2021 03:42:07 +0000 Subject: [PATCH 286/307] [Automated] Bump version --- CHANGELOG.md | 26 ++++++++------------------ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dde8138629..1eb8455a09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.7.0-nightly.8](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.7.0-nightly.9](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.4...HEAD) @@ -14,6 +14,8 @@ **🚀 Enhancements** +- Settings UI: Hyperlinks to settings [\#2420](https://github.com/pypeclub/OpenPype/pull/2420) +- Modules: JobQueue module moved one hierarchy level higher [\#2419](https://github.com/pypeclub/OpenPype/pull/2419) - Ftrack: Check existence of object type on recreation [\#2404](https://github.com/pypeclub/OpenPype/pull/2404) - Flame: moving `utility\_scripts` to api folder also with `scripts` [\#2385](https://github.com/pypeclub/OpenPype/pull/2385) - Centos 7 dependency compatibility [\#2384](https://github.com/pypeclub/OpenPype/pull/2384) @@ -29,13 +31,14 @@ - General: OpenPype default modules hierarchy [\#2338](https://github.com/pypeclub/OpenPype/pull/2338) - General: FFprobe error exception contain original error message [\#2328](https://github.com/pypeclub/OpenPype/pull/2328) - Resolve: Add experimental button to menu [\#2325](https://github.com/pypeclub/OpenPype/pull/2325) -- Input links: Cleanup and unification of differences [\#2322](https://github.com/pypeclub/OpenPype/pull/2322) -- General: Don't validate vendor bin with executing them [\#2317](https://github.com/pypeclub/OpenPype/pull/2317) -- General: Multilayer EXRs support [\#2315](https://github.com/pypeclub/OpenPype/pull/2315) - General: Reduce vendor imports [\#2305](https://github.com/pypeclub/OpenPype/pull/2305) +- Ftrack: Synchronize input links [\#2287](https://github.com/pypeclub/OpenPype/pull/2287) **🐛 Bug fixes** +- PS: Introduced settings for invalid characters to use in ValidateNaming plugin [\#2417](https://github.com/pypeclub/OpenPype/pull/2417) +- Settings UI: Breadcrumbs path does not create new entities [\#2416](https://github.com/pypeclub/OpenPype/pull/2416) +- AfterEffects: Variant 2022 is in defaults but missing in schemas [\#2412](https://github.com/pypeclub/OpenPype/pull/2412) - General: Fix access to environments from default settings [\#2403](https://github.com/pypeclub/OpenPype/pull/2403) - Fix: Placeholder Input color set fix [\#2399](https://github.com/pypeclub/OpenPype/pull/2399) - Settings: Fix state change of wrapper label [\#2396](https://github.com/pypeclub/OpenPype/pull/2396) @@ -55,8 +58,7 @@ - Tools: Use Qt context on tools show [\#2340](https://github.com/pypeclub/OpenPype/pull/2340) - Flame: Fix default argument value in custom dictionary [\#2339](https://github.com/pypeclub/OpenPype/pull/2339) - Timers Manager: Disable auto stop timer on linux platform [\#2334](https://github.com/pypeclub/OpenPype/pull/2334) -- nuke: bake preset single input exception [\#2331](https://github.com/pypeclub/OpenPype/pull/2331) -- Hiero: fixing multiple templates at a hierarchy parent [\#2330](https://github.com/pypeclub/OpenPype/pull/2330) +- Fix - provider icons are pulled from a folder [\#2326](https://github.com/pypeclub/OpenPype/pull/2326) - Royal Render: Fix plugin order and OpenPype auto-detection [\#2291](https://github.com/pypeclub/OpenPype/pull/2291) **Merged pull requests:** @@ -66,7 +68,6 @@ - Linux : flip updating submodules logic [\#2357](https://github.com/pypeclub/OpenPype/pull/2357) - Update of avalon-core [\#2346](https://github.com/pypeclub/OpenPype/pull/2346) - Maya: configurable model top level validation [\#2321](https://github.com/pypeclub/OpenPype/pull/2321) -- Create test publish class for After Effects [\#2270](https://github.com/pypeclub/OpenPype/pull/2270) ## [3.6.4](https://github.com/pypeclub/OpenPype/tree/3.6.4) (2021-11-23) @@ -88,17 +89,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.2-nightly.2...3.6.2) -**🚀 Enhancements** - -- Tools: Assets widget [\#2265](https://github.com/pypeclub/OpenPype/pull/2265) -- SceneInventory: Choose loader in asset switcher [\#2262](https://github.com/pypeclub/OpenPype/pull/2262) - -**🐛 Bug fixes** - -- Tools: Parenting of tools in Nuke and Hiero [\#2266](https://github.com/pypeclub/OpenPype/pull/2266) -- limiting validator to specific editorial hosts [\#2264](https://github.com/pypeclub/OpenPype/pull/2264) -- Tools: Select Context dialog attribute fix [\#2261](https://github.com/pypeclub/OpenPype/pull/2261) - ## [3.6.1](https://github.com/pypeclub/OpenPype/tree/3.6.1) (2021-11-16) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.1-nightly.1...3.6.1) diff --git a/openpype/version.py b/openpype/version.py index 06bc20ae43..544160d41c 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.7.0-nightly.8" +__version__ = "3.7.0-nightly.9" diff --git a/pyproject.toml b/pyproject.toml index e5d552bb3b..07a9ac8e43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.7.0-nightly.8" # OpenPype +version = "3.7.0-nightly.9" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 5a194263fde0b9821d29479a26d92c0d5c3fe510 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Dec 2021 13:10:17 +0100 Subject: [PATCH 287/307] get_timer_data_for_context is static method --- .../timers_manager/timers_manager.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 964e6d9a58..11e1821912 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -167,7 +167,7 @@ class TimersManager(OpenPypeModule, ITrayService, ILaunchHookPaths): asset_name = path_items.pop(-1) project_name = path_items.pop(0) return self.get_timer_data_for_context( - project_name, asset_name, task_name + project_name, asset_name, task_name, self.log ) def get_launch_hook_paths(self): @@ -177,7 +177,10 @@ class TimersManager(OpenPypeModule, ITrayService, ILaunchHookPaths): "launch_hooks" ) - def get_timer_data_for_context(self, project_name, asset_name, task_name): + @staticmethod + def get_timer_data_for_context( + project_name, asset_name, task_name, logger=None + ): """Prepare data for timer related callbacks. TODO: @@ -205,9 +208,11 @@ class TimersManager(OpenPypeModule, ITrayService, ILaunchHookPaths): try: task_type = asset_data["tasks"][task_name]["type"] except KeyError: - self.log.warning( - "Couldn't find task_type for {}".format(task_name) - ) + msg = "Couldn't find task_type for {}".format(task_name) + if logger is not None: + logger.warning(msg) + else: + print(msg) hierarchy_items = asset_data.get("parents") or [] hierarchy_items.append(asset_name) @@ -228,7 +233,7 @@ class TimersManager(OpenPypeModule, ITrayService, ILaunchHookPaths): task_name (str): Task name """ data = self.get_timer_data_for_context( - project_name, asset_name, task_name + project_name, asset_name, task_name, self.log ) self.timer_started(None, data) From 03ba413b282e1ce8b4c2d63a2371617d16c0592a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Dec 2021 13:13:10 +0100 Subject: [PATCH 288/307] adde new exception --- openpype/modules/default_modules/timers_manager/exceptions.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 openpype/modules/default_modules/timers_manager/exceptions.py diff --git a/openpype/modules/default_modules/timers_manager/exceptions.py b/openpype/modules/default_modules/timers_manager/exceptions.py new file mode 100644 index 0000000000..5a9e00765d --- /dev/null +++ b/openpype/modules/default_modules/timers_manager/exceptions.py @@ -0,0 +1,3 @@ +class InvalidContextError(ValueError): + """Context for which the timer should be started is invalid.""" + pass From 3bb3211d4a63d6a93666e6c854f3f2d068fbe5e2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Dec 2021 13:13:42 +0100 Subject: [PATCH 289/307] raise InvalidContextError if context is not valid --- .../timers_manager/timers_manager.py | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 11e1821912..051b2d7c80 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -1,11 +1,14 @@ import os import platform + +from avalon.api import AvalonMongoDB + from openpype.modules import OpenPypeModule from openpype_interfaces import ( ITrayService, ILaunchHookPaths ) -from avalon.api import AvalonMongoDB +from .exceptions import InvalidContextError class ExampleTimersManagerConnector: @@ -162,7 +165,7 @@ class TimersManager(OpenPypeModule, ITrayService, ILaunchHookPaths): """ path_items = task_path.split("/") if len(path_items) < 3: - raise ValueError("Invalid path") + raise InvalidContextError("Invalid path \"{}\"".format(task_path)) task_name = path_items.pop(-1) asset_name = path_items.pop(-1) project_name = path_items.pop(0) @@ -186,6 +189,12 @@ class TimersManager(OpenPypeModule, ITrayService, ILaunchHookPaths): TODO: - return predefined object that has access to asset document etc. """ + if not project_name or not asset_name or not task_name: + raise InvalidContextError(( + "Missing context information got" + " Project: \"{}\" Asset: \"{}\" Task: \"{}\"" + ).format(str(project_name), str(asset_name), str(task_name))) + dbconn = AvalonMongoDB() dbconn.install() dbconn.Session["AVALON_PROJECT"] = project_name @@ -201,12 +210,22 @@ class TimersManager(OpenPypeModule, ITrayService, ILaunchHookPaths): } ) if not asset_doc: - raise ValueError("Uknown asset {}".format(asset_name)) + dbconn.uninstall() + raise InvalidContextError(( + "Asset \"{}\" not found in project \"{}\"" + ).format(asset_name, project_name)) asset_data = asset_doc.get("data") or {} + asset_tasks = asset_data.get("tasks") or {} + if task_name not in asset_tasks: + dbconn.uninstall() + raise InvalidContextError(( + "Task \"{}\" not found on asset \"{}\" in project \"{}\"" + ).format(task_name, asset_name, project_name)) + task_type = "" try: - task_type = asset_data["tasks"][task_name]["type"] + task_type = asset_tasks[task_name]["type"] except KeyError: msg = "Couldn't find task_type for {}".format(task_name) if logger is not None: @@ -217,6 +236,7 @@ class TimersManager(OpenPypeModule, ITrayService, ILaunchHookPaths): hierarchy_items = asset_data.get("parents") or [] hierarchy_items.append(asset_name) + dbconn.uninstall() return { "project_name": project_name, "task_name": task_name, From ddcdff611e55ed6dadfccf598ca882a70c56cdf2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Dec 2021 13:14:18 +0100 Subject: [PATCH 290/307] return error message in response --- .../default_modules/timers_manager/rest_api.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/rest_api.py b/openpype/modules/default_modules/timers_manager/rest_api.py index 4296610c23..f16cb316c3 100644 --- a/openpype/modules/default_modules/timers_manager/rest_api.py +++ b/openpype/modules/default_modules/timers_manager/rest_api.py @@ -43,14 +43,19 @@ class TimersManagerModuleRestApi: asset_name = data["asset_name"] task_name = data["task_name"] except KeyError: - log.error(( + msg = ( "Payload must contain fields 'project_name," " 'asset_name' and 'task_name'" - )) - return Response(status=400) + ) + log.error(msg) + return Response(status=400, message=msg) self.module.stop_timers() - self.module.start_timer(project_name, asset_name, task_name) + try: + self.module.start_timer(project_name, asset_name, task_name) + except Exception as exc: + return Response(status=404, message=str(exc)) + return Response(status=200) async def stop_timer(self, request): From 9605abfb13f64356aa382ef3e4ec0182345a1cae Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Dec 2021 13:14:54 +0100 Subject: [PATCH 291/307] return response from posted request --- .../modules/default_modules/timers_manager/timers_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 051b2d7c80..47d020104b 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -420,4 +420,4 @@ class TimersManager(OpenPypeModule, ITrayService, ILaunchHookPaths): "task_name": task_name } - requests.post(rest_api_url, json=data) + return requests.post(rest_api_url, json=data) From c814e04691546032b1a6fe2937b2a4a8382396e3 Mon Sep 17 00:00:00 2001 From: 2-REC Date: Tue, 21 Dec 2021 17:35:29 +0700 Subject: [PATCH 292/307] Check min length of plugin path --- openpype/lib/plugin_tools.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 2a859da7cb..8de5f641eb 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -227,20 +227,27 @@ def filter_pyblish_plugins(plugins): # iterate over plugins for plugin in plugins[:]: - file = os.path.normpath(inspect.getsourcefile(plugin)) - file = os.path.normpath(file) - - # host determined from path - host_from_file = file.split(os.path.sep)[-4:-3][0] - plugin_kind = file.split(os.path.sep)[-2:-1][0] - - # TODO: change after all plugins are moved one level up - if host_from_file == "openpype": - host_from_file = "global" - try: config_data = presets[host]["publish"][plugin.__name__] except KeyError: + # host determined from path + file = os.path.normpath(inspect.getsourcefile(plugin)) + file = os.path.normpath(file) + + split_path = file.split(os.path.sep) + if len(split_path) < 4: + log.warning( + 'plugin path too short to extract host {}'.format(file) + ) + continue + + host_from_file = split_path[-4:-3][0] + plugin_kind = split_path[-2:-1][0] + + # TODO: change after all plugins are moved one level up + if host_from_file == "openpype": + host_from_file = "global" + try: config_data = presets[host_from_file][plugin_kind][plugin.__name__] # noqa: E501 except KeyError: From 6d15256840d39d07b8e4c3d1e4a20f8b715027c5 Mon Sep 17 00:00:00 2001 From: 2-REC Date: Tue, 21 Dec 2021 17:38:39 +0700 Subject: [PATCH 293/307] Simplified indexing --- openpype/lib/plugin_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 8de5f641eb..7c66f9760d 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -241,8 +241,8 @@ def filter_pyblish_plugins(plugins): ) continue - host_from_file = split_path[-4:-3][0] - plugin_kind = split_path[-2:-1][0] + host_from_file = split_path[-4] + plugin_kind = split_path[-2] # TODO: change after all plugins are moved one level up if host_from_file == "openpype": From 9b0e2c3e8beccc86fdbfa317080008cfa8e2d9b3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Dec 2021 13:21:31 +0100 Subject: [PATCH 294/307] added new exception PublishXmlValidationError --- openpype/pipeline/publish/__init__.py | 8 ++- openpype/pipeline/publish/lib.py | 57 ++++++++++++++++++++ openpype/pipeline/publish/publish_plugins.py | 25 ++++++++- 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index ca958816fe..d106f28617 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -6,7 +6,9 @@ from .publish_plugins import ( from .lib import ( DiscoverResult, - publish_plugins_discover + publish_plugins_discover, + load_help_content_from_plugin, + load_help_content_from_filepath ) @@ -16,5 +18,7 @@ __all__ = ( "OpenPypePyblishPluginMixin", "DiscoverResult", - "publish_plugins_discover" + "publish_plugins_discover", + "load_help_content_from_plugin", + "load_help_content_from_filepath" ) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 0fa712a301..aa30ac22c9 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -1,6 +1,8 @@ import os import sys import types +import inspect +import xml.etree.ElementTree import six import pyblish.plugin @@ -28,6 +30,61 @@ class DiscoverResult: self.plugins[item] = value +class HelpContent: + def __init__(self, title, description, detail=None): + self.title = title + self.description = description + self.detail = detail + + +def load_help_content_from_filepath(filepath): + """Load help content from xml file. + + Xml file may containt errors and warnings. + """ + errors = {} + warnings = {} + output = { + "errors": errors, + "warnings": warnings + } + if not os.path.exists(filepath): + return output + tree = xml.etree.ElementTree.parse(filepath) + root = tree.getroot() + for child in root: + child_id = child.attrib.get("id") + if child_id is None: + continue + + # Make sure ID is string + child_id = str(child_id) + + title = child.find("title").text + description = child.find("description").text + detail_node = child.find("detail") + detail = None + if detail_node: + detail = detail_node.text + if child.tag == "error": + errors[child_id] = HelpContent(title, description, detail) + elif child.tag == "warning": + warnings[child_id] = HelpContent(title, description, detail) + return output + + +def load_help_content_from_plugin(plugin): + cls = plugin + if not inspect.isclass(plugin): + cls = plugin.__class__ + plugin_filepath = inspect.getfile(cls) + plugin_dir = os.path.dirname(plugin_filepath) + basename = os.path.splitext(os.path.basename(plugin_filepath))[0] + filename = basename + ".xml" + filepath = os.path.join(plugin_dir, "help", filename) + return load_help_content_from_filepath(filepath) + + def publish_plugins_discover(paths=None): """Find and return available pyblish plug-ins diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index b60b9f43a7..9a73d1acc6 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -1,3 +1,6 @@ +from .lib import load_help_content_from_plugin + + class PublishValidationError(Exception): """Validation error happened during publishing. @@ -12,13 +15,33 @@ class PublishValidationError(Exception): description(str): Detailed description of an error. It is possible to use Markdown syntax. """ - def __init__(self, message, title=None, description=None): + def __init__(self, message, title=None, description=None, detail=None): self.message = message self.title = title or "< Missing title >" self.description = description or message + self.detail = detail super(PublishValidationError, self).__init__(message) +class PublishXmlValidationError(PublishValidationError): + def __init__( + self, message, plugin, key=None, *formattings_arg, **formatting_kwargs + ): + if key is None: + key = "main" + result = load_help_content_from_plugin(plugin) + content_obj = result["errors"][key] + description = content_obj.description.format( + *formattings_arg, **formatting_kwargs + ) + detail = content_obj.detail.format( + *formattings_arg, **formatting_kwargs + ) + super(PublishXmlValidationError, self).__init__( + message, content_obj.title, description, detail + ) + + class KnownPublishError(Exception): """Publishing crashed because of known error. From c5b9a0bd05ecf8764b2418cdf1fbba0746f02cc2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Dec 2021 13:22:11 +0100 Subject: [PATCH 295/307] added PublishXmlValidationError to 'pipeline.publish' --- openpype/pipeline/publish/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index d106f28617..228c4d8dcb 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -1,5 +1,6 @@ from .publish_plugins import ( PublishValidationError, + PublishXmlValidationError, KnownPublishError, OpenPypePyblishPluginMixin ) @@ -14,6 +15,7 @@ from .lib import ( __all__ = ( "PublishValidationError", + "PublishXmlValidationError", "KnownPublishError", "OpenPypePyblishPluginMixin", From d5b24781bb53234bf9942ab50d1760113d4f8fbd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Dec 2021 13:25:13 +0100 Subject: [PATCH 296/307] reduced formatting possibilities --- openpype/pipeline/publish/publish_plugins.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index 9a73d1acc6..78dbaf2ddc 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -25,18 +25,17 @@ class PublishValidationError(Exception): class PublishXmlValidationError(PublishValidationError): def __init__( - self, message, plugin, key=None, *formattings_arg, **formatting_kwargs + self, message, plugin, key=None, formatting_data=None ): if key is None: key = "main" + + if not formatting_data: + formatting_data = {} result = load_help_content_from_plugin(plugin) content_obj = result["errors"][key] - description = content_obj.description.format( - *formattings_arg, **formatting_kwargs - ) - detail = content_obj.detail.format( - *formattings_arg, **formatting_kwargs - ) + description = content_obj.description.format(**formatting_data) + detail = content_obj.detail.format(**formatting_data) super(PublishXmlValidationError, self).__init__( message, content_obj.title, description, detail ) From a05a6785584e7efa5e6f93e9939c5e91b80038b2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Dec 2021 13:50:05 +0100 Subject: [PATCH 297/307] imported PublishXmlValidationError to 'openpype.pipeline' level --- openpype/pipeline/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index e968df4011..79d6ce4d54 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -9,6 +9,7 @@ from .create import ( from .publish import ( PublishValidationError, + PublishXmlValidationError, KnownPublishError, OpenPypePyblishPluginMixin ) @@ -23,6 +24,7 @@ __all__ = ( "CreatedInstance", "PublishValidationError", + "PublishXmlValidationError", "KnownPublishError", "OpenPypePyblishPluginMixin" ) From fe3af18101de12804bc9361c5c121e4c4238aa74 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Dec 2021 13:53:17 +0100 Subject: [PATCH 298/307] swapped plugin and message args order --- openpype/pipeline/publish/publish_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index 78dbaf2ddc..48fa2499b8 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -25,7 +25,7 @@ class PublishValidationError(Exception): class PublishXmlValidationError(PublishValidationError): def __init__( - self, message, plugin, key=None, formatting_data=None + self, plugin, message, key=None, formatting_data=None ): if key is None: key = "main" From 5683dda65d2f17ec17efc2d4f749d95324295236 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Dec 2021 14:05:12 +0100 Subject: [PATCH 299/307] do not format empty detail --- openpype/pipeline/publish/publish_plugins.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index 48fa2499b8..bce64ec709 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -35,7 +35,9 @@ class PublishXmlValidationError(PublishValidationError): result = load_help_content_from_plugin(plugin) content_obj = result["errors"][key] description = content_obj.description.format(**formatting_data) - detail = content_obj.detail.format(**formatting_data) + detail = content_obj.detail + if detail: + detail = detail.format(**formatting_data) super(PublishXmlValidationError, self).__init__( message, content_obj.title, description, detail ) From d6b386583633c0c510f6b54a6b0999f7312f8e75 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Dec 2021 14:06:27 +0100 Subject: [PATCH 300/307] fix detail node bool --- openpype/pipeline/publish/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index aa30ac22c9..f38e73afe2 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -64,7 +64,7 @@ def load_help_content_from_filepath(filepath): description = child.find("description").text detail_node = child.find("detail") detail = None - if detail_node: + if detail_node is not None: detail = detail_node.text if child.tag == "error": errors[child_id] = HelpContent(title, description, detail) From 0c7a12c1f22938704c3c70bdd9ee375a8c5366d5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Dec 2021 15:59:00 +0100 Subject: [PATCH 301/307] Implemented validators for New publisher for Photoshop --- .../publish/help/validate_instance_asset.xml | 25 +++++++++++++++++++ .../plugins/publish/help/validate_naming.xml | 21 ++++++++++++++++ .../publish/help/validate_unique_subsets.xml | 23 +++++++++++++++++ .../publish/validate_instance_asset.py | 12 +++++++-- .../plugins/publish/validate_naming.py | 22 ++++++++++------ .../publish/validate_unique_subsets.py | 23 +++++++++++++---- 6 files changed, 111 insertions(+), 15 deletions(-) create mode 100644 openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml create mode 100644 openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml create mode 100644 openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml diff --git a/openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml b/openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml new file mode 100644 index 0000000000..3b040e8ea8 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml @@ -0,0 +1,25 @@ + + + +Subset context + +## Invalid subset context + +Asset name found '{found}' in subsets, expected '{expected}'. + +### How to repair? + +You can fix this with `Repair` button on the right. This will use '{expected}' asset name and overwrite '{found}' asset name in scene metadata. + +After that restart `Publish` with a `Reload button`. + +If this is unwanted, close workfile and open again, that way different asset value would be used for context information. + + +### __Detailed Info__ (optional) + +This might happen if you are reuse old workfile and open it in different context. +(Eg. you created subset "renderCompositingDefault" from asset "Robot' in "your_project_Robot_compositing.aep", now you opened this workfile in a context "Sloth" but existing subset for "Robot" asset stayed in the workfile.) + + + \ No newline at end of file diff --git a/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml b/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml new file mode 100644 index 0000000000..21a7370340 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml @@ -0,0 +1,21 @@ + + + +Invalid name + +## Invalid name of subset + +Name of subset is created from a layer name. Some characters (whitespace, '/' etc.) are not allowed because of publishing (files couldn't be saved on some OSes). + +### How to repair? + +You can fix this with `Repair` button on the right. This will remove invalid characters with safe character ('_' by default) in both subset names and matching group names. + +After that restart `Publish` with a `Reload button`. + +Or you use `Subset Manager` to delete existing subsets, remove created groups, rename layers that are used for their creation and use `Create` option in the Openpype menu to create them again. + +Invalid characters and 'safe character' could be configured in Settings. Ask your OpenPype admin to modify them if necessary. + + + \ No newline at end of file diff --git a/openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml b/openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml new file mode 100644 index 0000000000..fa7c76a2dd --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml @@ -0,0 +1,23 @@ + + + +Subsets duplicated + +## Some subsets are duplicated + +Created subsets must be unique. + +Subsets '{duplicates_str}' are duplicated. + +### How to repair? + +Use `Subset Manager` to delete duplicated subset to have only unique subset names and restart `Publish` with a `Reload button`. + + +### __Detailed Info__ (optional) + +Subset names are created from layer names. Layer names are filtered for characters that would break publishing process when files are created. +This replacement process might result in duplicate names of subsets. + + + \ No newline at end of file diff --git a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py index 4dc1972074..8f13cc6b33 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py @@ -1,8 +1,10 @@ from avalon import api import pyblish.api -import openpype.api from avalon import photoshop +import openpype.api +from openpype.pipeline import PublishXmlValidationError + class ValidateInstanceAssetRepair(pyblish.api.Action): """Repair the instance asset.""" @@ -56,4 +58,10 @@ class ValidateInstanceAsset(pyblish.api.InstancePlugin): f"If that's not correct value, close workfile and " f"reopen via Workfiles!" ) - assert instance_asset == current_asset, msg + formatting_data = { + "found": instance_asset, + "expected": current_asset + } + if instance_asset != current_asset: + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) diff --git a/openpype/hosts/photoshop/plugins/publish/validate_naming.py b/openpype/hosts/photoshop/plugins/publish/validate_naming.py index 1635096f4b..d548992f09 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_naming.py @@ -1,9 +1,11 @@ import re import pyblish.api -import openpype.api from avalon import photoshop +import openpype.api +from openpype.pipeline import PublishXmlValidationError + class ValidateNamingRepair(pyblish.api.Action): """Repair the instance asset.""" @@ -69,14 +71,18 @@ class ValidateNaming(pyblish.api.InstancePlugin): replace_char = '' def process(self, instance): - help_msg = ' Use Repair action (A) in Pyblish to fix it.' - msg = "Name \"{}\" is not allowed.{}".format(instance.data["name"], - help_msg) - assert not re.search(self.invalid_chars, instance.data["name"]), msg + msg = "Name \"{}\" is not allowed.".format(instance.data["name"]) - msg = "Subset \"{}\" is not allowed.{}".format(instance.data["subset"], - help_msg) - assert not re.search(self.invalid_chars, instance.data["subset"]), msg + formatting_data = {"error_msg": msg} + if re.search(self.invalid_chars, instance.data["name"]): + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) + + msg = "Subset \"{}\" is not allowed.".format(instance.data["subset"]) + formatting_data = {"error_msg": msg} + if re.search(self.invalid_chars, instance.data["subset"]): + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) @classmethod def get_replace_chars(cls): diff --git a/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py b/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py index 15ae5fbcea..d41fefa971 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py @@ -1,5 +1,8 @@ +import collections + import pyblish.api import openpype.api +from openpype.pipeline import PublishXmlValidationError class ValidateSubsetUniqueness(pyblish.api.ContextPlugin): @@ -19,8 +22,18 @@ class ValidateSubsetUniqueness(pyblish.api.ContextPlugin): if instance.data.get('publish'): subset_names.append(instance.data.get('subset')) - msg = ( - "Instance subset names are not unique. " + - "Remove duplicates via SubsetManager." - ) - assert len(subset_names) == len(set(subset_names)), msg + duplicates = [item + for item, count in + collections.Counter(subset_names).items() + if count > 1] + + if duplicates: + duplicates_str = ",".join(duplicates) + formatting_data = {"duplicates_str": duplicates_str} + msg = ( + "Instance subset names {} are not unique.".format( + duplicates_str) + + " Remove duplicates via SubsetManager." + ) + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) From 96da6a07f612d0a1357f4e46640919b38001d750 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 22 Dec 2021 03:42:14 +0000 Subject: [PATCH 302/307] [Automated] Bump version --- CHANGELOG.md | 20 +++++--------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eb8455a09..0c6d1b8fe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.7.0-nightly.9](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.7.0-nightly.10](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.4...HEAD) @@ -8,14 +8,12 @@ - General: Default modules hierarchy n2 [\#2368](https://github.com/pypeclub/OpenPype/pull/2368) -**🆕 New features** - -- Settings UI use OpenPype styles [\#2296](https://github.com/pypeclub/OpenPype/pull/2296) - **🚀 Enhancements** +- General: Environment variables groups [\#2424](https://github.com/pypeclub/OpenPype/pull/2424) - Settings UI: Hyperlinks to settings [\#2420](https://github.com/pypeclub/OpenPype/pull/2420) - Modules: JobQueue module moved one hierarchy level higher [\#2419](https://github.com/pypeclub/OpenPype/pull/2419) +- TimersManager: Start timer post launch hook [\#2418](https://github.com/pypeclub/OpenPype/pull/2418) - Ftrack: Check existence of object type on recreation [\#2404](https://github.com/pypeclub/OpenPype/pull/2404) - Flame: moving `utility\_scripts` to api folder also with `scripts` [\#2385](https://github.com/pypeclub/OpenPype/pull/2385) - Centos 7 dependency compatibility [\#2384](https://github.com/pypeclub/OpenPype/pull/2384) @@ -27,18 +25,14 @@ - Burnins: Be able recognize mxf OPAtom format [\#2361](https://github.com/pypeclub/OpenPype/pull/2361) - Maya: Add is\_static\_image\_plane and is\_in\_all\_views option in imagePlaneLoader [\#2356](https://github.com/pypeclub/OpenPype/pull/2356) - Local settings: Copyable studio paths [\#2349](https://github.com/pypeclub/OpenPype/pull/2349) -- Assets Widget: Clear model on project change [\#2345](https://github.com/pypeclub/OpenPype/pull/2345) -- General: OpenPype default modules hierarchy [\#2338](https://github.com/pypeclub/OpenPype/pull/2338) -- General: FFprobe error exception contain original error message [\#2328](https://github.com/pypeclub/OpenPype/pull/2328) -- Resolve: Add experimental button to menu [\#2325](https://github.com/pypeclub/OpenPype/pull/2325) -- General: Reduce vendor imports [\#2305](https://github.com/pypeclub/OpenPype/pull/2305) -- Ftrack: Synchronize input links [\#2287](https://github.com/pypeclub/OpenPype/pull/2287) +- General: Multilayer EXRs support [\#2315](https://github.com/pypeclub/OpenPype/pull/2315) **🐛 Bug fixes** - PS: Introduced settings for invalid characters to use in ValidateNaming plugin [\#2417](https://github.com/pypeclub/OpenPype/pull/2417) - Settings UI: Breadcrumbs path does not create new entities [\#2416](https://github.com/pypeclub/OpenPype/pull/2416) - AfterEffects: Variant 2022 is in defaults but missing in schemas [\#2412](https://github.com/pypeclub/OpenPype/pull/2412) +- Nuke: baking representations was not additive [\#2406](https://github.com/pypeclub/OpenPype/pull/2406) - General: Fix access to environments from default settings [\#2403](https://github.com/pypeclub/OpenPype/pull/2403) - Fix: Placeholder Input color set fix [\#2399](https://github.com/pypeclub/OpenPype/pull/2399) - Settings: Fix state change of wrapper label [\#2396](https://github.com/pypeclub/OpenPype/pull/2396) @@ -53,12 +47,9 @@ - Launcher: Minimize button on MacOs [\#2355](https://github.com/pypeclub/OpenPype/pull/2355) - StandalonePublisher: Fix import of constant [\#2354](https://github.com/pypeclub/OpenPype/pull/2354) - Adobe products show issue [\#2347](https://github.com/pypeclub/OpenPype/pull/2347) -- Maya Look Assigner: Fix Python 3 compatibility [\#2343](https://github.com/pypeclub/OpenPype/pull/2343) - Remove wrongly used host for hook [\#2342](https://github.com/pypeclub/OpenPype/pull/2342) - Tools: Use Qt context on tools show [\#2340](https://github.com/pypeclub/OpenPype/pull/2340) - Flame: Fix default argument value in custom dictionary [\#2339](https://github.com/pypeclub/OpenPype/pull/2339) -- Timers Manager: Disable auto stop timer on linux platform [\#2334](https://github.com/pypeclub/OpenPype/pull/2334) -- Fix - provider icons are pulled from a folder [\#2326](https://github.com/pypeclub/OpenPype/pull/2326) - Royal Render: Fix plugin order and OpenPype auto-detection [\#2291](https://github.com/pypeclub/OpenPype/pull/2291) **Merged pull requests:** @@ -67,7 +58,6 @@ - Add validator to check correct version of extension for PS and AE [\#2387](https://github.com/pypeclub/OpenPype/pull/2387) - Linux : flip updating submodules logic [\#2357](https://github.com/pypeclub/OpenPype/pull/2357) - Update of avalon-core [\#2346](https://github.com/pypeclub/OpenPype/pull/2346) -- Maya: configurable model top level validation [\#2321](https://github.com/pypeclub/OpenPype/pull/2321) ## [3.6.4](https://github.com/pypeclub/OpenPype/tree/3.6.4) (2021-11-23) diff --git a/openpype/version.py b/openpype/version.py index 544160d41c..273755dfd0 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.7.0-nightly.9" +__version__ = "3.7.0-nightly.10" diff --git a/pyproject.toml b/pyproject.toml index 07a9ac8e43..ea6d9ee5e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.7.0-nightly.9" # OpenPype +version = "3.7.0-nightly.10" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 5efef322a6415aa0ebb4c985a00e26d1341ab9e0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Dec 2021 11:37:31 +0100 Subject: [PATCH 303/307] added attribute log_mongo_url_components back for older builds --- openpype/lib/log.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/lib/log.py b/openpype/lib/log.py index a34cb898e3..a42faef008 100644 --- a/openpype/lib/log.py +++ b/openpype/lib/log.py @@ -202,6 +202,11 @@ class PypeLogger: use_mongo_logging = None mongo_process_id = None + # Backwards compatibility - was used in start.py + # TODO remove when all old builds are replaced with new one + # not using 'log_mongo_url_components' + log_mongo_url_components = None + # Database name in Mongo log_database_name = os.environ["OPENPYPE_DATABASE_NAME"] # Collection name under database in Mongo @@ -320,6 +325,7 @@ class PypeLogger: # Change initialization state to prevent runtime changes # if is executed during runtime cls.initialized = False + cls.log_mongo_url_components = get_default_components() # Define if should logging to mongo be used use_mongo_logging = bool(log4mongo is not None) From 1914c144cd2998dc40c5253f8ea9597648d51ad5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Dec 2021 11:38:43 +0100 Subject: [PATCH 304/307] uses get_default_components for log components --- start.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/start.py b/start.py index ae6aefe34e..b5a8d95fd0 100644 --- a/start.py +++ b/start.py @@ -1109,15 +1109,15 @@ def get_info(use_staging=None) -> list: # Reinitialize PypeLogger.initialize() - log_components = PypeLogger.log_mongo_url_components - if log_components["host"]: - inf.append(("Logging to MongoDB", log_components["host"])) - inf.append((" - port", log_components["port"] or "")) + mongo_components = get_default_components() + if mongo_components["host"]: + inf.append(("Logging to MongoDB", mongo_components["host"])) + inf.append((" - port", mongo_components["port"] or "")) inf.append((" - database", PypeLogger.log_database_name)) inf.append((" - collection", PypeLogger.log_collection_name)) - inf.append((" - user", log_components["username"] or "")) - if log_components["auth_db"]: - inf.append((" - auth source", log_components["auth_db"])) + inf.append((" - user", mongo_components["username"] or "")) + if mongo_components["auth_db"]: + inf.append((" - auth source", mongo_components["auth_db"])) maximum = max(len(i[0]) for i in inf) formatted = [] From beeea44cb952412b1e4b1d78f0e78340aa5c68c3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Dec 2021 13:09:14 +0100 Subject: [PATCH 305/307] Forced cx_freeze to include sqlite3 into build because of pytest requirements --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cd3ed4f82c..a21645e66a 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,8 @@ install_requires = [ "filecmp", "dns", # Python defaults (cx_Freeze skip them by default) - "dbm" + "dbm", + "sqlite3" ] includes = [] From 8d06754ba286cefb80646c6d3d29577ba00182db Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Dec 2021 15:51:42 +0100 Subject: [PATCH 306/307] update avalon unreal plugin --- repos/avalon-unreal-integration | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-unreal-integration b/repos/avalon-unreal-integration index 43f6ea9439..8529332aed 160000 --- a/repos/avalon-unreal-integration +++ b/repos/avalon-unreal-integration @@ -1 +1 @@ -Subproject commit 43f6ea943980b29c02a170942b566ae11f2b7080 +Subproject commit 8529332aeddddc0bfccc7b1455ce0fa0aa571da9 From 5dedd2655dd501d5ad6aa59161b54fba548768d4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Dec 2021 16:40:20 +0100 Subject: [PATCH 307/307] Revert "Photoshop: New style validations for New publisher" --- .../publish/help/validate_instance_asset.xml | 25 -------- .../plugins/publish/help/validate_naming.xml | 21 ------- .../publish/help/validate_unique_subsets.xml | 23 -------- .../publish/validate_instance_asset.py | 12 +--- .../plugins/publish/validate_naming.py | 22 +++---- .../publish/validate_unique_subsets.py | 23 ++------ openpype/pipeline/__init__.py | 2 - openpype/pipeline/publish/__init__.py | 10 +--- openpype/pipeline/publish/lib.py | 57 ------------------- openpype/pipeline/publish/publish_plugins.py | 26 +-------- 10 files changed, 18 insertions(+), 203 deletions(-) delete mode 100644 openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml delete mode 100644 openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml delete mode 100644 openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml diff --git a/openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml b/openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml deleted file mode 100644 index 3b040e8ea8..0000000000 --- a/openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - -Subset context - -## Invalid subset context - -Asset name found '{found}' in subsets, expected '{expected}'. - -### How to repair? - -You can fix this with `Repair` button on the right. This will use '{expected}' asset name and overwrite '{found}' asset name in scene metadata. - -After that restart `Publish` with a `Reload button`. - -If this is unwanted, close workfile and open again, that way different asset value would be used for context information. - - -### __Detailed Info__ (optional) - -This might happen if you are reuse old workfile and open it in different context. -(Eg. you created subset "renderCompositingDefault" from asset "Robot' in "your_project_Robot_compositing.aep", now you opened this workfile in a context "Sloth" but existing subset for "Robot" asset stayed in the workfile.) - - - \ No newline at end of file diff --git a/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml b/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml deleted file mode 100644 index 21a7370340..0000000000 --- a/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - -Invalid name - -## Invalid name of subset - -Name of subset is created from a layer name. Some characters (whitespace, '/' etc.) are not allowed because of publishing (files couldn't be saved on some OSes). - -### How to repair? - -You can fix this with `Repair` button on the right. This will remove invalid characters with safe character ('_' by default) in both subset names and matching group names. - -After that restart `Publish` with a `Reload button`. - -Or you use `Subset Manager` to delete existing subsets, remove created groups, rename layers that are used for their creation and use `Create` option in the Openpype menu to create them again. - -Invalid characters and 'safe character' could be configured in Settings. Ask your OpenPype admin to modify them if necessary. - - - \ No newline at end of file diff --git a/openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml b/openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml deleted file mode 100644 index fa7c76a2dd..0000000000 --- a/openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - -Subsets duplicated - -## Some subsets are duplicated - -Created subsets must be unique. - -Subsets '{duplicates_str}' are duplicated. - -### How to repair? - -Use `Subset Manager` to delete duplicated subset to have only unique subset names and restart `Publish` with a `Reload button`. - - -### __Detailed Info__ (optional) - -Subset names are created from layer names. Layer names are filtered for characters that would break publishing process when files are created. -This replacement process might result in duplicate names of subsets. - - - \ No newline at end of file diff --git a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py index 8f13cc6b33..4dc1972074 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py @@ -1,9 +1,7 @@ from avalon import api import pyblish.api -from avalon import photoshop - import openpype.api -from openpype.pipeline import PublishXmlValidationError +from avalon import photoshop class ValidateInstanceAssetRepair(pyblish.api.Action): @@ -58,10 +56,4 @@ class ValidateInstanceAsset(pyblish.api.InstancePlugin): f"If that's not correct value, close workfile and " f"reopen via Workfiles!" ) - formatting_data = { - "found": instance_asset, - "expected": current_asset - } - if instance_asset != current_asset: - raise PublishXmlValidationError(self, msg, - formatting_data=formatting_data) + assert instance_asset == current_asset, msg diff --git a/openpype/hosts/photoshop/plugins/publish/validate_naming.py b/openpype/hosts/photoshop/plugins/publish/validate_naming.py index d548992f09..1635096f4b 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_naming.py @@ -1,10 +1,8 @@ import re import pyblish.api -from avalon import photoshop - import openpype.api -from openpype.pipeline import PublishXmlValidationError +from avalon import photoshop class ValidateNamingRepair(pyblish.api.Action): @@ -71,18 +69,14 @@ class ValidateNaming(pyblish.api.InstancePlugin): replace_char = '' def process(self, instance): - msg = "Name \"{}\" is not allowed.".format(instance.data["name"]) + help_msg = ' Use Repair action (A) in Pyblish to fix it.' + msg = "Name \"{}\" is not allowed.{}".format(instance.data["name"], + help_msg) + assert not re.search(self.invalid_chars, instance.data["name"]), msg - formatting_data = {"error_msg": msg} - if re.search(self.invalid_chars, instance.data["name"]): - raise PublishXmlValidationError(self, msg, - formatting_data=formatting_data) - - msg = "Subset \"{}\" is not allowed.".format(instance.data["subset"]) - formatting_data = {"error_msg": msg} - if re.search(self.invalid_chars, instance.data["subset"]): - raise PublishXmlValidationError(self, msg, - formatting_data=formatting_data) + msg = "Subset \"{}\" is not allowed.{}".format(instance.data["subset"], + help_msg) + assert not re.search(self.invalid_chars, instance.data["subset"]), msg @classmethod def get_replace_chars(cls): diff --git a/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py b/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py index d41fefa971..15ae5fbcea 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py @@ -1,8 +1,5 @@ -import collections - import pyblish.api import openpype.api -from openpype.pipeline import PublishXmlValidationError class ValidateSubsetUniqueness(pyblish.api.ContextPlugin): @@ -22,18 +19,8 @@ class ValidateSubsetUniqueness(pyblish.api.ContextPlugin): if instance.data.get('publish'): subset_names.append(instance.data.get('subset')) - duplicates = [item - for item, count in - collections.Counter(subset_names).items() - if count > 1] - - if duplicates: - duplicates_str = ",".join(duplicates) - formatting_data = {"duplicates_str": duplicates_str} - msg = ( - "Instance subset names {} are not unique.".format( - duplicates_str) + - " Remove duplicates via SubsetManager." - ) - raise PublishXmlValidationError(self, msg, - formatting_data=formatting_data) + msg = ( + "Instance subset names are not unique. " + + "Remove duplicates via SubsetManager." + ) + assert len(subset_names) == len(set(subset_names)), msg diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index 79d6ce4d54..e968df4011 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -9,7 +9,6 @@ from .create import ( from .publish import ( PublishValidationError, - PublishXmlValidationError, KnownPublishError, OpenPypePyblishPluginMixin ) @@ -24,7 +23,6 @@ __all__ = ( "CreatedInstance", "PublishValidationError", - "PublishXmlValidationError", "KnownPublishError", "OpenPypePyblishPluginMixin" ) diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index 228c4d8dcb..ca958816fe 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -1,26 +1,20 @@ from .publish_plugins import ( PublishValidationError, - PublishXmlValidationError, KnownPublishError, OpenPypePyblishPluginMixin ) from .lib import ( DiscoverResult, - publish_plugins_discover, - load_help_content_from_plugin, - load_help_content_from_filepath + publish_plugins_discover ) __all__ = ( "PublishValidationError", - "PublishXmlValidationError", "KnownPublishError", "OpenPypePyblishPluginMixin", "DiscoverResult", - "publish_plugins_discover", - "load_help_content_from_plugin", - "load_help_content_from_filepath" + "publish_plugins_discover" ) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index f38e73afe2..0fa712a301 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -1,8 +1,6 @@ import os import sys import types -import inspect -import xml.etree.ElementTree import six import pyblish.plugin @@ -30,61 +28,6 @@ class DiscoverResult: self.plugins[item] = value -class HelpContent: - def __init__(self, title, description, detail=None): - self.title = title - self.description = description - self.detail = detail - - -def load_help_content_from_filepath(filepath): - """Load help content from xml file. - - Xml file may containt errors and warnings. - """ - errors = {} - warnings = {} - output = { - "errors": errors, - "warnings": warnings - } - if not os.path.exists(filepath): - return output - tree = xml.etree.ElementTree.parse(filepath) - root = tree.getroot() - for child in root: - child_id = child.attrib.get("id") - if child_id is None: - continue - - # Make sure ID is string - child_id = str(child_id) - - title = child.find("title").text - description = child.find("description").text - detail_node = child.find("detail") - detail = None - if detail_node is not None: - detail = detail_node.text - if child.tag == "error": - errors[child_id] = HelpContent(title, description, detail) - elif child.tag == "warning": - warnings[child_id] = HelpContent(title, description, detail) - return output - - -def load_help_content_from_plugin(plugin): - cls = plugin - if not inspect.isclass(plugin): - cls = plugin.__class__ - plugin_filepath = inspect.getfile(cls) - plugin_dir = os.path.dirname(plugin_filepath) - basename = os.path.splitext(os.path.basename(plugin_filepath))[0] - filename = basename + ".xml" - filepath = os.path.join(plugin_dir, "help", filename) - return load_help_content_from_filepath(filepath) - - def publish_plugins_discover(paths=None): """Find and return available pyblish plug-ins diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index bce64ec709..b60b9f43a7 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -1,6 +1,3 @@ -from .lib import load_help_content_from_plugin - - class PublishValidationError(Exception): """Validation error happened during publishing. @@ -15,34 +12,13 @@ class PublishValidationError(Exception): description(str): Detailed description of an error. It is possible to use Markdown syntax. """ - def __init__(self, message, title=None, description=None, detail=None): + def __init__(self, message, title=None, description=None): self.message = message self.title = title or "< Missing title >" self.description = description or message - self.detail = detail super(PublishValidationError, self).__init__(message) -class PublishXmlValidationError(PublishValidationError): - def __init__( - self, plugin, message, key=None, formatting_data=None - ): - if key is None: - key = "main" - - if not formatting_data: - formatting_data = {} - result = load_help_content_from_plugin(plugin) - content_obj = result["errors"][key] - description = content_obj.description.format(**formatting_data) - detail = content_obj.detail - if detail: - detail = detail.format(**formatting_data) - super(PublishXmlValidationError, self).__init__( - message, content_obj.title, description, detail - ) - - class KnownPublishError(Exception): """Publishing crashed because of known error.