From d918d020839d261ef5a00bf0f3c77ed7544f3468 Mon Sep 17 00:00:00 2001 From: antirotor Date: Sun, 31 Mar 2019 22:01:39 +0200 Subject: [PATCH 01/29] feat(rv): basic rv action, wip --- pype/ftrack/actions/action_rv.py | 70 ++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 pype/ftrack/actions/action_rv.py diff --git a/pype/ftrack/actions/action_rv.py b/pype/ftrack/actions/action_rv.py new file mode 100644 index 0000000000..72ceda28de --- /dev/null +++ b/pype/ftrack/actions/action_rv.py @@ -0,0 +1,70 @@ +from pype.ftrack import BaseAction +import os +import sys +import json +try: + import ftrack +except ImportError: + dependencies_path = os.path.abspath( + os.path.join(os.environ.get('PYPE_STUDIO_CONFIG'), + 'pype', 'vendor', 'ftrack_legacy')) + + sys.path.append(dependencies_path) + import ftrack + + +class RVAction(BaseAction): + """ Launch RV action """ + identifier = "rv-launch-action" + label = "rv" + description = "rv Launcher" + icon = "https://img.icons8.com/color/48/000000/circled-play.png" + + def _createPlaylistFromSelection(self, selection): + '''Return new selection with temporary playlist from *selection*.''' + + # If selection is only one entity we don't need to create + # a playlist. + if len(selection) == 1: + return selection + + playlist = [] + for entity in selection: + playlist.append({ + 'id': entity['entityId'], + 'type': entity['entityType'] + }) + + playlist = ftrack.createTempData(json.dumps(playlist)) + + selection = [{ + 'entityType': 'tempdata', + 'entityId': playlist.getId() + }] + + return selection + + def discover(self, session, entities, event): + """Return available actions based on *event*. """ + selection = event["data"].get("selection", []) + if len(selection) != 1: + return False + + entityType = selection[0].get("entityType", None) + if entityType in ["assetversion", "task"]: + return True + return False + + def launch(self, session, entities, event): + pass + + def register(session, **kw): + super().register() + + +def main(arguments=None): + pass + + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) From ee3f9571ca3267ceacbd0bcc535553d1f38ec2cc Mon Sep 17 00:00:00 2001 From: antirotor Date: Wed, 3 Apr 2019 22:06:49 +0200 Subject: [PATCH 02/29] feat(rv): initial version of RV player action --- pype/ftrack/actions/action_rv.py | 497 +++++++++++++++++++++++++++++-- 1 file changed, 466 insertions(+), 31 deletions(-) diff --git a/pype/ftrack/actions/action_rv.py b/pype/ftrack/actions/action_rv.py index 72ceda28de..1a42e7d0df 100644 --- a/pype/ftrack/actions/action_rv.py +++ b/pype/ftrack/actions/action_rv.py @@ -2,15 +2,13 @@ from pype.ftrack import BaseAction import os import sys import json -try: - import ftrack -except ImportError: - dependencies_path = os.path.abspath( - os.path.join(os.environ.get('PYPE_STUDIO_CONFIG'), - 'pype', 'vendor', 'ftrack_legacy')) +import subprocess +import ftrack_api +import logging +from pype import pypelib +from app.api import Logger - sys.path.append(dependencies_path) - import ftrack +log = Logger.getLogger(__name__) class RVAction(BaseAction): @@ -19,30 +17,32 @@ class RVAction(BaseAction): label = "rv" description = "rv Launcher" icon = "https://img.icons8.com/color/48/000000/circled-play.png" + type = 'Application' - def _createPlaylistFromSelection(self, selection): - '''Return new selection with temporary playlist from *selection*.''' + def __init__(self, session): + """ Constructor - # If selection is only one entity we don't need to create - # a playlist. - if len(selection) == 1: - return selection + :param session: ftrack Session + :type session: :class:`ftrack_api.Session` + """ + super().__init__(session) + self.rv_path = None + self.config_data = None - playlist = [] - for entity in selection: - playlist.append({ - 'id': entity['entityId'], - 'type': entity['entityType'] - }) + # RV_HOME should be set if properly installed + if os.environ.get('RV_HOME'): + self.rv_path = os.environ.get('RV_HOME') + else: + # if not, fallback to config file location + self.load_config_data() + self.set_rv_path() - playlist = ftrack.createTempData(json.dumps(playlist)) + if self.rv_path is None: + return - selection = [{ - 'entityType': 'tempdata', - 'entityId': playlist.getId() - }] - - return selection + self.allowed_types = self.config_data.get( + 'file_ext', ["img", "mov", "exr"] + ) def discover(self, session, entities, event): """Return available actions based on *event*. """ @@ -55,16 +55,451 @@ class RVAction(BaseAction): return True return False - def launch(self, session, entities, event): - pass + def load_config_data(self): + path_items = [pypelib.get_presets_path(), 'rv', 'config.json'] + filepath = os.path.sep.join(path_items) - def register(session, **kw): + data = dict() + try: + with open(filepath) as data_file: + data = json.load(data_file) + except Exception as e: + log.warning( + 'Failed to load data from RV presets file ({})'.format(e) + ) + + self.config_data = data + + def set_rv_path(self): + for path in self.config_data.get("rv_paths", []): + if os.path.exists(path): + self.rv_path = path + break + + def register(self): + assert (self.rv_path is not None), ( + 'RV is not installed' + ' or paths in presets are not set correctly' + ) super().register() + def interface(self, session, entities, event): + if event['data'].get('values', {}): + return + + entity = entities[0] + versions = [] + + entity_type = entity.entity_type.lower() + if entity_type == "assetversion": + if ( + entity[ + 'components' + ][0]['file_type'][1:] in self.allowed_types + ): + versions.append(entity) + else: + master_entity = entity + if entity_type == "task": + master_entity = entity['parent'] + + for asset in master_entity['assets']: + for version in asset['versions']: + # Get only AssetVersion of selected task + if ( + entity_type == "task" and + version['task']['id'] != entity['id'] + ): + continue + # Get only components with allowed type + filetype = version['components'][0]['file_type'] + if filetype[1:] in self.allowed_types: + versions.append(version) + + if len(versions) < 1: + return { + 'success': False, + 'message': 'There are no Asset Versions to open.' + } + + items = [] + base_label = "v{0} - {1} - {2}" + default_component = self.config_data.get( + 'default_component', None + ) + last_available = None + select_value = None + for version in versions: + for component in version['components']: + label = base_label.format( + str(version['version']).zfill(3), + version['asset']['type']['name'], + component['name'] + ) + + try: + location = component[ + 'component_locations' + ][0]['location'] + file_path = location.get_filesystem_path(component) + except Exception: + file_path = component[ + 'component_locations' + ][0]['resource_identifier'] + + if os.path.isdir(os.path.dirname(file_path)): + last_available = file_path + if component['name'] == default_component: + select_value = file_path + items.append( + {'label': label, 'value': file_path} + ) + + if len(items) == 0: + return { + 'success': False, + 'message': ( + 'There are no Asset Versions with accessible path.' + ) + } + + item = { + 'label': 'Items to view', + 'type': 'enumerator', + 'name': 'path', + 'data': sorted( + items, + key=itemgetter('label'), + reverse=True + ) + } + if select_value is not None: + item['value'] = select_value + else: + item['value'] = last_available + + return {'items': [item]} + + def launch(self, session, entities, event): + """Callback method for RV action.""" + + # Launching application + if "values" not in event["data"]: + return + filename = event['data']['values']['path'] + + fps = entities[0].get('custom_attributes', {}).get('fps', None) + + cmd = [] + # RV path + cmd.append(os.path.normpath(self.rv_path)) + if fps is not None: + cmd.append("-fps {}".format(int(fps))) + cmd.append(os.path.normpath(filename)) + + try: + # Run RV with these commands + subprocess.Popen(' '.join(cmd)) + except FileNotFoundError: + return { + 'success': False, + 'message': 'File "{}" was not found.'.format( + os.path.basename(filename) + ) + } + + return True + + +def register(session): + """Register hooks.""" + if not isinstance(session, ftrack_api.session.Session): + return + + RVAction(session).register() + def main(arguments=None): - pass + '''Set up logging and register action.''' + if arguments is None: + arguments = [] + + import argparse + parser = argparse.ArgumentParser() + # Allow setting of logging level from arguments. + loggingLevels = {} + for level in ( + logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, + logging.ERROR, logging.CRITICAL + ): + loggingLevels[logging.getLevelName(level).lower()] = level + + parser.add_argument( + '-v', '--verbosity', + help='Set the logging output verbosity.', + choices=loggingLevels.keys(), + default='info' + ) + namespace = parser.parse_args(arguments) + + # Set up basic logging + logging.basicConfig(level=loggingLevels[namespace.verbosity]) + + session = ftrack_api.Session() + register(session) + + # Wait for events + logging.info( + 'Registered actions and listening for events. Use Ctrl-C to abort.' + ) + session.event_hub.wait() if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) + +""" +Usage: RV movie and image sequence viewer + + One File: rv foo.jpg + This Directory: rv . + Other Directory: rv /path/to/dir + Image Sequence w/Audio: rv [ in.#.tif in.wav ] + Stereo w/Audio: rv [ left.#.tif right.#.tif in.wav ] + Stereo Movies: rv [ left.mov right.mov ] + Stereo Movie (from rvio): rv stereo.mov + Cuts Sequenced: rv cut1.mov cut2.#.exr cut3.mov + Stereo Cuts Sequenced: rv [ l1.mov r1.mov ] [ l2.mov r2.mov ] + Forced Anamorphic: rv [ -pa 2.0 fullaperture.#.dpx ] + Compare: rv -wipe a.exr b.exr + Difference: rv -diff a.exr b.exr + Slap Comp Over: rv -over a.exr b.exr + Tile Images: rv -tile *.jpg + Cache + Play Movie: rv -l -play foo.mov + Cache Images to Examine: rv -c big.#.exr + Fullscreen on 2nd monitor: rv -fullscreen -screen 1 + Select Source View: rv [ in.exr -select view right ] + Select Source Layer: rv [ in.exr -select layer light1.diffuse ] + (single-view source) + Select Source Layer: rv [ in.exr -select layer left,light1.diffuse ] + (multi-view source) + Select Source Channel: rv [ in.exr -select channel R ] + (single-view, single-layer source) + Select Source Channel: rv [ in.exr -select channel left,Diffuse,R ] + (multi-view, multi-layer source) + +Image Sequence Numbering + + Frames 1 to 100 no padding: image.1-100@.jpg + Frames 1 to 100 padding 4: image.1-100#.jpg -or- image.1-100@@@@.jpg + Frames 1 to 100 padding 5: image.1-100@@@@@.jpg + Frames -100 to -200 padding 4: image.-100--200#jpg + printf style padding 4: image.%04d.jpg + printf style w/range: image.%04d.jpg 1-100 + printf no padding w/range: image.%d.jpg 1-100 + Complicated no pad 1 to 100: image_887f1-100@_982.tif + Stereo pair (left,right): image.#.%V.tif + Stereo pair (L,R): image.#.%v.tif + All Frames, padding 4: image.#.jpg + All Frames in Sequence: image.*.jpg + All Frames in Directory: /path/to/directory + All Frames in current dir: . + +Per-source arguments (inside [ and ] restricts to that source only) + +-pa %f Per-source pixel aspect ratio +-ro %d Per-source range offset +-rs %d Per-source range start +-fps %f Per-source or global fps +-ao %f Per-source audio offset in seconds +-so %f Per-source stereo relative eye offset +-rso %f Per-source stereo right eye offset +-volume %f Per-source or global audio volume (default=1) +-fcdl %S Per-source file CDL +-lcdl %S Per-source look CDL +-flut %S Per-source file LUT +-llut %S Per-source look LUT +-pclut %S Per-source pre-cache software LUT +-cmap %S Per-source channel mapping + (channel names, separated by ',') +-select %S %S Per-source view/layer/channel selection +-crop %d %d %d %d Per-source crop (xmin, ymin, xmax, ymax) +-uncrop %d %d %d %d Per-source uncrop (width, height, xoffset, yoffset) +-in %d Per-source cut-in frame +-out %d Per-source cut-out frame +-noMovieAudio Disable source movie's baked-in audio +-inparams ... Source specific input parameters + + ... Input sequence patterns, images, movies, or directories +-c Use region frame cache +-l Use look-ahead cache +-nc Use no caching +-s %f Image scale reduction +-ns Nuke style sequence notation + (deprecated and ignored -- no longer needed) +-noRanges No separate frame ranges + (i.e. 1-10 will be considered a file) +-sessionType %S Session type (sequence, stack) (deprecated, use -view) +-stereo %S Stereo mode + (hardware, checker, scanline, anaglyph, lumanaglyph, + left, right, pair, mirror, hsqueezed, vsqueezed) +-stereoSwap %d Swap left and right eyes stereo display + (0 == no, 1 == yes, default=0) +-vsync %d Video Sync (1 = on, 0 = off, default = 1) +-comp %S Composite mode + (over, add, difference, replace, topmost) +-layout %S Layout mode (packed, row, column, manual) +-over Same as -comp over -view defaultStack +-diff Same as -comp difference -view defaultStack +-replace Same as -comp replace -view defaultStack +-topmost Same as -comp topmost -view defaultStack +-layer Same as -comp topmost -view defaultStack, with strict + frame ranges +-tile Same as -layout packed -view defaultLayout +-wipe Same as -over with wipes enabled +-view %S Start with a particular view +-noSequence Don't contract files into sequences +-inferSequence Infer sequences from one file +-autoRetime %d Automatically retime conflicting media fps in + sequences and stacks (1 = on, 0 = off, default = 1) +-rthreads %d Number of reader threads (default=1) +-fullscreen Start in fullscreen mode +-present Start in presentation mode (using presentation device) +-presentAudio %d Use presentation audio device in presentation mode + (1 = on, 0 = off) +-presentDevice %S Presentation mode device +-presentVideoFormat %S Presentation mode override video format + (device specific) +-presentDataFormat %S Presentation mode override data format + (device specific) +-screen %d Start on screen (0, 1, 2, ...) +-noBorders No window manager decorations +-geometry %d %d [%d %d] Start geometry X, Y, W, H +-fitMedia Fit the window to the first media shown +-init %S Override init script +-nofloat Turn off floating point by default +-maxbits %d Maximum default bit depth (default=32) +-gamma %f Set display gamma (default=1) +-sRGB Display using linear -> sRGB conversion +-rec709 Display using linear -> Rec 709 conversion +-dlut %S Apply display LUT +-brightness %f Set display relative brightness in stops (default=0) +-resampleMethod %S Resampling method + (area, linear, cubic, nearest, default=area) +-eval %S Evaluate Mu expression at every session start +-pyeval %S Evaluate Python expression at every session start +-nomb Hide menu bar on start up +-play Play on startup +-playMode %d Playback mode (0=Context dependent, 1=Play all frames, + 2=Realtime, default=0) +-loopMode %d Playback loop mode + (0=Loop, 1=Play Once, 2=Ping-Pong, default=0) +-cli Mu command line interface +-vram %f VRAM usage limit in Mb, default = 64.000000 +-cram %f Max region cache RAM usage in Gb, + (6.4Gb available, default 1Gb) +-lram %f Max look-ahead cache RAM usage in Gb, + (6.4Gb available, default 0.2Gb) +-noPBO Prevent use of GL PBOs for pixel transfer +-prefetch Prefetch images for rendering +-useAppleClientStorage Use APPLE_client_storage extension +-useThreadedUpload Use threading for texture uploading/downloading + if possible +-bwait %f Max buffer wait time in cached seconds, default 5.0 +-lookback %f Percentage of the lookahead cache reserved for + frames behind the playhead, default 25 +-yuv Assume YUV hardware conversion +-noaudio Turn off audio +-audiofs %d Use fixed audio frame size + (results are hardware dependant ... try 512) +-audioCachePacket %d Audio cache packet size in samples (default=2048) +-audioMinCache %f Audio cache min size in seconds (default=0.300000) +-audioMaxCache %f Audio cache max size in seconds (default=0.600000) +-audioModule %S Use specific audio module +-audioDevice %S Use specific audio device +-audioRate %f Use specific output audio rate (default=ask hardware) +-audioPrecision %d Use specific output audio precision (default=16) +-audioNice %d Close audio device when not playing + (may cause problems on some hardware) default=0 +-audioNoLock %d Do not use hardware audio/video syncronization + (use software instead, default=0) +-audioPreRoll %d Preroll audio on device open (Linux only; default=0) +-audioGlobalOffset %f Global audio offset in seconds +-audioDeviceLatency %f Audio device latency compensation in milliseconds +-bg %S Background pattern (default=black, white, grey18, + grey50, checker, crosshatch) +-formats Show all supported image and movie formats +-apple Use Quicktime and NSImage libraries (on OS X) +-cinalt Use alternate Cineon/DPX readers +-exrcpus %d EXR thread count (default=0) +-exrRGBA EXR Always read as RGBA (default=false) +-exrInherit EXR guess channel inheritance (default=false) +-exrNoOneChannel EXR never use one channel planar images (default=false) +-exrIOMethod %d [%d] EXR I/O Method (0=standard, 1=buffered, 2=unbuffered, + 3=MemoryMap, 4=AsyncBuffered, 5=AsyncUnbuffered, + default=1) and optional chunk size (default=61440) +-exrReadWindowIsDisplayWindow + EXR read window is display window (default=false) +-exrReadWindow %d EXR Read Window Method (0=Data, 1=Display, + 2=Union, 3=Data inside Display, default=3) +-jpegRGBA Make JPEG four channel RGBA on read + (default=no, use RGB or YUV) +-jpegIOMethod %d [%d] JPEG I/O Method (0=standard, 1=buffered, + 2=unbuffered, 3=MemoryMap, 4=AsyncBuffered, + 5=AsyncUnbuffered, default=1) and optional + chunk size (default=61440) +-cinpixel %S Cineon pixel storage (default=RGB8_PLANAR) +-cinchroma Use Cineon chromaticity values + (for default reader only) +-cinIOMethod %d [%d] Cineon I/O Method (0=standard, 1=buffered, + 2=unbuffered, 3=MemoryMap, 4=AsyncBuffered, + 5=AsyncUnbuffered, default=1) and optional + chunk size (default=61440) +-dpxpixel %S DPX pixel storage (default=RGB8_PLANAR) +-dpxchroma Use DPX chromaticity values (for default reader only) +-dpxIOMethod %d [%d] DPX I/O Method (0=standard, 1=buffered, 2=unbuffered, + 3=MemoryMap, 4=AsyncBuffered, 5=AsyncUnbuffered, + default=1) and optional chunk size (default=61440) +-tgaIOMethod %d [%d] TARGA I/O Method (0=standard, 1=buffered, + 2=unbuffered, 3=MemoryMap, 4=AsyncBuffered, + 5=AsyncUnbuffered, default=1) + and optional chunk size (default=61440) +-tiffIOMethod %d [%d] TIFF I/O Method (0=standard, 1=buffered, + 2=unbuffered, 3=MemoryMap, 4=AsyncBuffered, + 5=AsyncUnbuffered, default=1) and optional + chunk size (default=61440) +-lic %S Use specific license file +-noPrefs Ignore preferences +-resetPrefs Reset preferences to default values +-qtcss %S Use QT style sheet for UI +-qtstyle %S Use QT style +-qtdesktop %d QT desktop aware, default=1 (on) +-xl Aggressively absorb screen space for large media +-mouse %d Force tablet/stylus events to be treated as a + mouse events, default=0 (off) +-network Start networking +-networkPort %d Port for networking +-networkHost %S Alternate host/address for incoming connections +-networkTag %S Tag to mark automatically saved port file +-networkConnect %S [%d] Start networking and connect to host at port +-networkPerm %d Default network connection permission + (0=Ask, 1=Allow, 2=Deny, default=0) +-reuse %d Try to re-use the current session for + incoming URLs (1 = reuse session, + 0 = new session, default = 1) +-nopackages Don't load any packages at startup (for debugging) +-encodeURL Encode the command line as + an rvlink URL, print, and exit +-bakeURL Fully bake the command line as an + rvlink URL, print, and exit +-sendEvent ... Send external events e.g. -sendEvent 'name' 'content' +-flags ... Arbitrary flags (flag, or 'name=value') + for use in Mu code +-debug ... Debug category +-version Show RV version number +-strictlicense Exit rather than consume an rv license if no rvsolo + licenses are available +-prefsPath %S Alternate path to preferences directory +-sleep %d Sleep (in seconds) before starting to + allow attaching debugger +""" From 7aa18d18b9de04da14dc4d37c6bb289928a4d7d6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 4 Apr 2019 19:22:25 +0200 Subject: [PATCH 03/29] added ftrack action that creates folder structure based on presets --- .../actions/action_create_project_folders.py | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 pype/ftrack/actions/action_create_project_folders.py diff --git a/pype/ftrack/actions/action_create_project_folders.py b/pype/ftrack/actions/action_create_project_folders.py new file mode 100644 index 0000000000..4bdce4a5c7 --- /dev/null +++ b/pype/ftrack/actions/action_create_project_folders.py @@ -0,0 +1,137 @@ +import os +import sys +import argparse +import logging +import json + +import ftrack_api +from pype import lib as pypelib +from pype.ftrack import BaseAction + + +class CreateProjectFolders(BaseAction): + '''Edit meta data action.''' + + #: Action identifier. + identifier = 'create.project.folders' + #: Action label. + label = 'Create Project Folders' + #: Action description. + description = 'Creates folder structure' + #: roles that are allowed to register this action + role_list = ['Pypeclub', 'Administrator'] + icon = ( + 'https://cdn2.iconfinder.com/data/icons/' + 'buttons-9/512/Button_Add-01.png' + ) + + def discover(self, session, entities, event): + ''' Validation ''' + + return True + + def launch(self, session, entities, event): + preset_items = [ + pypelib.get_presets_path(), + 'tools', + 'project_folder_structure.json' + ] + filepath = os.path.sep.join(preset_items) + + # Load folder structure template from presets + presets = dict() + try: + with open(filepath) as data_file: + presets = json.load(data_file) + except Exception as e: + msg = 'Unable to load Folder structure preset' + self.log.warning(msg) + return { + 'success': False, + 'message': msg + } + + # Set project root folder + entity = entities[0] + if entity.entity_type.lower() == 'project': + project_name = entity['full_name'] + else: + project_name = entity['project']['full_name'] + project_root_items = [os.environ['AVALON_PROJECTS'], project_name] + project_root = os.path.sep.join(project_root_items) + + # Get paths based on presets + paths = self.get_paths(presets) + + #Create folders + for path in paths: + os.makedirs(path.format(project_root=project_root)) + + return True + + def get_paths(self, data, items=[]): + paths = [] + path_items = [] + path_items.extend(items) + name = data['name'] + if name == '__project_root__': + name = '{project_root}' + path_items.append(name) + subfolders = data.get('subfolders', []) + if len(subfolders) == 0: + return os.path.sep.join(path_items) + for sub in subfolders: + result = self.get_paths(sub, path_items) + if isinstance(result, str): + paths.append(result) + else: + paths.extend(result) + return paths + + +def register(session, **kw): + '''Register plugin. Called when used as an plugin.''' + + if not isinstance(session, ftrack_api.session.Session): + return + + CreateProjectFolders(session).register() + + +def main(arguments=None): + '''Set up logging and register action.''' + if arguments is None: + arguments = [] + + parser = argparse.ArgumentParser() + # Allow setting of logging level from arguments. + loggingLevels = {} + for level in ( + logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, + logging.ERROR, logging.CRITICAL + ): + loggingLevels[logging.getLevelName(level).lower()] = level + + parser.add_argument( + '-v', '--verbosity', + help='Set the logging output verbosity.', + choices=loggingLevels.keys(), + default='info' + ) + namespace = parser.parse_args(arguments) + + # Set up basic logging + logging.basicConfig(level=loggingLevels[namespace.verbosity]) + + session = ftrack_api.Session() + register(session) + + # Wait for events + logging.info( + 'Registered actions and listening for events. Use Ctrl-C to abort.' + ) + session.event_hub.wait() + + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) From cd21de060b1a36b50528a61f289ac8093e919748 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 9 Apr 2019 10:41:51 +0200 Subject: [PATCH 04/29] feat(pype): removing plugins for collect export json --- pype/plugins/global/{publish => _publish_unused}/collect_json.py | 0 .../nuke/{publish => _publish_unused}/extract_post_json.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename pype/plugins/global/{publish => _publish_unused}/collect_json.py (100%) rename pype/plugins/nuke/{publish => _publish_unused}/extract_post_json.py (100%) diff --git a/pype/plugins/global/publish/collect_json.py b/pype/plugins/global/_publish_unused/collect_json.py similarity index 100% rename from pype/plugins/global/publish/collect_json.py rename to pype/plugins/global/_publish_unused/collect_json.py diff --git a/pype/plugins/nuke/publish/extract_post_json.py b/pype/plugins/nuke/_publish_unused/extract_post_json.py similarity index 100% rename from pype/plugins/nuke/publish/extract_post_json.py rename to pype/plugins/nuke/_publish_unused/extract_post_json.py From 72ca8e89a8028b3b1faa62a1ed4857ba9da0b475 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Apr 2019 15:58:15 +0200 Subject: [PATCH 05/29] action creates also ftrack entities based on presets --- .../actions/action_create_project_folders.py | 173 +++++++++++++++--- 1 file changed, 149 insertions(+), 24 deletions(-) diff --git a/pype/ftrack/actions/action_create_project_folders.py b/pype/ftrack/actions/action_create_project_folders.py index 4bdce4a5c7..15bd18cb5f 100644 --- a/pype/ftrack/actions/action_create_project_folders.py +++ b/pype/ftrack/actions/action_create_project_folders.py @@ -1,5 +1,6 @@ import os import sys +import re import argparse import logging import json @@ -25,12 +26,124 @@ class CreateProjectFolders(BaseAction): 'buttons-9/512/Button_Add-01.png' ) + pattern_array = re.compile('\[.*\]') + pattern_ftrack = '.*\[[.]*ftrack[.]*' + pattern_ent_ftrack = 'ftrack\.[^.,\],\s,]*' + project_root_key = '__project_root__' + def discover(self, session, entities, event): ''' Validation ''' return True def launch(self, session, entities, event): + entity = entities[0] + if entity.entity_type.lower() == 'project': + project = entity + else: + project = entity['project'] + + presets = self.load_presets() + try: + # Get paths based on presets + basic_paths = self.get_path_items(presets) + self.create_folders(basic_paths, entity) + self.create_ftrack_entities(basic_paths, project) + except Exception as e: + session.rollback() + return { + 'success': False, + 'message': str(e) + } + + return True + + def get_ftrack_paths(self, paths_items): + all_ftrack_paths = [] + for path_items in paths_items: + ftrack_path_items = [] + is_ftrack = False + for item in reversed(path_items): + if item == self.project_root_key: + continue + if is_ftrack: + ftrack_path_items.append(item) + elif re.match(self.pattern_ftrack, item): + ftrack_path_items.append(item) + is_ftrack = True + ftrack_path_items = list(reversed(ftrack_path_items)) + if ftrack_path_items: + all_ftrack_paths.append(ftrack_path_items) + return all_ftrack_paths + + def compute_ftrack_items(self, in_list, keys): + if len(keys) == 0: + return in_list + key = keys[0] + exist = None + for index, subdict in enumerate(in_list): + if key in subdict: + exist = index + break + if exist is not None: + in_list[exist][key] = self.compute_ftrack_items( + in_list[exist][key], keys[1:] + ) + else: + in_list.append({key: self.compute_ftrack_items([], keys[1:])}) + return in_list + + def translate_ftrack_items(self, paths_items): + main = [] + for path_items in paths_items: + main = self.compute_ftrack_items(main, path_items) + return main + + def create_ftrack_entities(self, basic_paths, project_ent): + only_ftrack_items = self.get_ftrack_paths(basic_paths) + ftrack_paths = self.translate_ftrack_items(only_ftrack_items) + + for separation in ftrack_paths: + parent = project_ent + self.trigger_creation(separation, parent) + + def trigger_creation(self, separation, parent): + for item, subvalues in separation.items(): + matches = re.findall(self.pattern_array, item) + ent_type = 'Folder' + if len(matches) == 0: + name = item + else: + match = matches[0] + name = item.replace(match, '') + ent_type_match = re.findall(self.pattern_ent_ftrack, match) + if len(ent_type_match) > 0: + ent_type_split = ent_type_match[0].split('.') + if len(ent_type_split) == 2: + ent_type = ent_type_split[1] + new_parent = self.create_ftrack_entity(name, ent_type, parent) + if subvalues: + for subvalue in subvalues: + self.trigger_creation(subvalue, new_parent) + + def create_ftrack_entity(self, name, ent_type, parent): + for children in parent['children']: + if children['name'] == name: + return children + data = { + 'name': name, + 'parent_id': parent['id'] + } + if parent.entity_type.lower() == 'project': + data['project_id'] = parent['id'] + else: + data['project_id'] = parent['project']['id'] + + new_ent = self.session.create(ent_type, data) + self.session.commit() + return new_ent + + def load_presets(self): preset_items = [ pypelib.get_presets_path(), 'tools', @@ -50,9 +163,40 @@ class CreateProjectFolders(BaseAction): 'success': False, 'message': msg } + return presets + def get_path_items(self, in_dict): + output = [] + for key, value in in_dict.items(): + if not value: + output.append(key) + else: + paths = self.get_path_items(value) + for path in paths: + if isinstance(path, str): + output.append([key, path]) + else: + p = [key] + p.extend(path) + output.append(p) + return output + + def compute_paths(self, basic_paths_items, project_root): + output = [] + for path_items in basic_paths_items: + clean_items = [] + for path_item in path_items: + matches = re.findall(self.pattern_array, path_item) + if len(matches) > 0: + path_item = path_item.replace(matches[0], '') + if path_item == self.project_root_key: + path_item = project_root + clean_items.append(path_item) + output.append(os.path.normpath(os.path.sep.join(clean_items))) + return output + + def create_folders(self, basic_paths, entity): # Set project root folder - entity = entities[0] if entity.entity_type.lower() == 'project': project_name = entity['full_name'] else: @@ -60,33 +204,14 @@ class CreateProjectFolders(BaseAction): project_root_items = [os.environ['AVALON_PROJECTS'], project_name] project_root = os.path.sep.join(project_root_items) - # Get paths based on presets - paths = self.get_paths(presets) - + full_paths = self.compute_paths(basic_paths, project_root) #Create folders - for path in paths: + for path in full_paths: + if os.path.exists(path): + continue os.makedirs(path.format(project_root=project_root)) - return True - def get_paths(self, data, items=[]): - paths = [] - path_items = [] - path_items.extend(items) - name = data['name'] - if name == '__project_root__': - name = '{project_root}' - path_items.append(name) - subfolders = data.get('subfolders', []) - if len(subfolders) == 0: - return os.path.sep.join(path_items) - for sub in subfolders: - result = self.get_paths(sub, path_items) - if isinstance(result, str): - paths.append(result) - else: - paths.extend(result) - return paths def register(session, **kw): From 918d588225eff2001904260abc243c989955432a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 9 Apr 2019 16:31:11 +0200 Subject: [PATCH 06/29] templates are loaded from database when app is launched --- pype/ftrack/lib/ftrack_app_handler.py | 46 +++++++++++++++++++-------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 30acd4d849..7a087ca825 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -200,21 +200,39 @@ class AppAction(BaseHandler): application = avalonlib.get_application(os.environ["AVALON_APP_NAME"]) - data = {"project": {"name": entity['project']['full_name'], - "code": entity['project']['name']}, - "task": entity['name'], - "asset": entity['parent']['name'], - "app": application["application_dir"], - "hierarchy": hierarchy} - try: - anatomy = anatomy.format(data) - except Exception as e: - self.log.error( - "{0} Error in anatomy.format: {1}".format(__name__, e) + data = { + "root": os.environ["AVALON_PROJECTS"], + "project": { + "name": entity['project']['full_name'], + "code": entity['project']['name'] + }, + "task": entity['name'], + "asset": entity['parent']['name'], + "app": application["application_dir"], + "hierarchy": hierarchy, + } + + av_project = database[project_name].find_one({"type": 'project'}) + templates = None + if av_project: + work_template = av_project.get('config', {}).get('template', {}).get( + 'work', None ) - os.environ["AVALON_WORKDIR"] = os.path.join( - anatomy.work.root, anatomy.work.folder - ) + work_template = None + try: + work_template = work_template.format(**data) + except Exception: + try: + anatomy = anatomy.format(data) + work_template = os.path.join( + anatomy.work.root, + anatomy.work.folder + ) + except Exception as e: + self.log.error( + "{0} Error in anatomy.format: {1}".format(__name__, e) + ) + os.environ["AVALON_WORKDIR"] = os.path.normpath(work_template) # collect all parents from the task parents = [] From 63e7da261fc823db9ca26221418ff418c3b47385 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 10 Apr 2019 11:32:04 +0200 Subject: [PATCH 07/29] added discover validations --- pype/ftrack/actions/action_create_folders.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py index 7ce5526164..4495a09730 100644 --- a/pype/ftrack/actions/action_create_folders.py +++ b/pype/ftrack/actions/action_create_folders.py @@ -28,7 +28,11 @@ class CreateFolders(BaseAction): def discover(self, session, entities, event): ''' Validation ''' - + not_allowed = ['assetversion'] + if len(entities) != 1: + return False + if entities[0].entity_type.lower() in not_allowed: + return False return True def getShotAsset(self, entity): From 6fa648ba4623f8cb11bdd25a9b6b534c799bfd24 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 10 Apr 2019 11:32:59 +0200 Subject: [PATCH 08/29] added get presets function --- pype/ftrack/actions/action_create_folders.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py index 4495a09730..16aa3f493d 100644 --- a/pype/ftrack/actions/action_create_folders.py +++ b/pype/ftrack/actions/action_create_folders.py @@ -134,6 +134,16 @@ class CreateFolders(BaseAction): 'message': 'Created Folders Successfully!' } + def get_presets(self): + fpath_items = [pypelib.get_presets_path(), 'tools', 'sw_folders.json'] + filepath = os.path.normpath(os.path.sep.join(fpath_items)) + presets = dict() + try: + with open(filepath) as data_file: + presets = json.load(data_file) + except Exception as e: + self.log.warning('Wasn\'t able to load presets') + return dict(presets) def register(session, **kw): '''Register plugin. Called when used as an plugin.''' From 0e9ce8010a953e40a38d14bd2d9fafd84dd0b4cd Mon Sep 17 00:00:00 2001 From: antirotor Date: Wed, 10 Apr 2019 11:33:14 +0200 Subject: [PATCH 09/29] feat(rv): tweaked how path to rv is handled --- pype/ftrack/actions/action_rv.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pype/ftrack/actions/action_rv.py b/pype/ftrack/actions/action_rv.py index 1a42e7d0df..7a45ca9886 100644 --- a/pype/ftrack/actions/action_rv.py +++ b/pype/ftrack/actions/action_rv.py @@ -71,10 +71,7 @@ class RVAction(BaseAction): self.config_data = data def set_rv_path(self): - for path in self.config_data.get("rv_paths", []): - if os.path.exists(path): - self.rv_path = path - break + self.rv_path = self.config_data.get("rv_path") def register(self): assert (self.rv_path is not None), ( From 48e07cc877d001ee70ca6c0b15e159805f17e571 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 10 Apr 2019 11:33:21 +0200 Subject: [PATCH 10/29] added partial dict that helps with formatting --- pype/ftrack/actions/action_create_folders.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py index 16aa3f493d..b63c4b7888 100644 --- a/pype/ftrack/actions/action_create_folders.py +++ b/pype/ftrack/actions/action_create_folders.py @@ -145,6 +145,19 @@ class CreateFolders(BaseAction): self.log.warning('Wasn\'t able to load presets') return dict(presets) + + +class PartialDict(dict): + def __getitem__(self, item): + out = super().__getitem__(item) + if isinstance(out, dict): + return '{'+item+'}' + return out + + def __missing__(self, key): + return '{'+key+'}' + + def register(session, **kw): '''Register plugin. Called when used as an plugin.''' From 4bf0e796b95128050e64cbc7a65a17b9d5488fb8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 10 Apr 2019 11:34:07 +0200 Subject: [PATCH 11/29] added formatting helpers for anatomy paths --- pype/ftrack/actions/action_create_folders.py | 70 ++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py index b63c4b7888..3af05a21e8 100644 --- a/pype/ftrack/actions/action_create_folders.py +++ b/pype/ftrack/actions/action_create_folders.py @@ -145,6 +145,76 @@ class CreateFolders(BaseAction): self.log.warning('Wasn\'t able to load presets') return dict(presets) + def template_format(self, template, data): + + partial_data = PartialDict(data) + + # remove subdict items from string (like 'project[name]') + subdict = PartialDict() + count = 1 + store_pattern = 5*'_'+'{:0>3}' + regex_patern = "\{\w*\[[^\}]*\]\}" + matches = re.findall(regex_patern, template) + + for match in matches: + key = store_pattern.format(count) + subdict[key] = match + template = template.replace(match, '{'+key+'}') + count += 1 + # solve fillind keys with optional keys + solved = self._solve_with_optional(template, partial_data) + # try to solve subdict and replace them back to string + for k, v in subdict.items(): + try: + v = v.format_map(data) + except (KeyError, TypeError): + pass + subdict[k] = v + + return solved.format_map(subdict) + + def _solve_with_optional(self, template, data): + # Remove optional missing keys + pattern = re.compile(r"(<.*?[^{0]*>)[^0-9]*?") + invalid_optionals = [] + for group in pattern.findall(template): + try: + group.format(**data) + except KeyError: + invalid_optionals.append(group) + for group in invalid_optionals: + template = template.replace(group, "") + + solved = template.format_map(data) + + # solving after format optional in second round + for catch in re.compile(r"(<.*?[^{0]*>)[^0-9]*?").findall(solved): + if "{" in catch: + # remove all optional + solved = solved.replace(catch, "") + else: + # Remove optional symbols + solved = solved.replace(catch, catch[1:-1]) + + return solved + + def compute_template(self, str, data): + first_result = self.template_format(str, data) + if first_result == first_result.split('{')[0]: + return os.path.normpath(first_result) + + index = first_result.index('{') + + regex = '\{\w*[^\}]*\}' + match = re.findall(regex, first_result[index:])[0] + without_missing = str.split(match)[0].split('}') + output_items = [] + for part in without_missing: + if '{' in part: + output_items.append(part + '}') + return os.path.normpath( + self.template_format(''.join(output_items), data) + ) class PartialDict(dict): From d3bcf2cdd3f4e448c219d5935ff7de3dd85298b3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 10 Apr 2019 11:34:48 +0200 Subject: [PATCH 12/29] changed get children method --- pype/ftrack/actions/action_create_folders.py | 22 ++++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py index 3af05a21e8..515d6e08d0 100644 --- a/pype/ftrack/actions/action_create_folders.py +++ b/pype/ftrack/actions/action_create_folders.py @@ -35,15 +35,6 @@ class CreateFolders(BaseAction): return False return True - def getShotAsset(self, entity): - if entity not in self.importable: - if entity['object_type']['name'] != 'Task': - self.importable.add(entity) - - if entity['children']: - children = entity['children'] - for child in children: - self.getShotAsset(child) def launch(self, session, entities, event): '''Callback method for custom action.''' @@ -134,6 +125,19 @@ class CreateFolders(BaseAction): 'message': 'Created Folders Successfully!' } + def get_notask_children(self, entity): + output = [] + if entity.get('object_type', {}).get( + 'name', entity.entity_type + ).lower() == 'task': + return output + else: + output.append(entity) + if entity['children']: + for child in entity['children']: + output.extend(self.get_notask_children(child)) + return output + def get_presets(self): fpath_items = [pypelib.get_presets_path(), 'tools', 'sw_folders.json'] filepath = os.path.normpath(os.path.sep.join(fpath_items)) From 03ea2479cb886da4cd4e27dcdb305d72f84b087e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 10 Apr 2019 11:35:49 +0200 Subject: [PATCH 13/29] added interface for cases when entity does not have only tasks as children --- pype/ftrack/actions/action_create_folders.py | 63 ++++++++++++++++++-- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py index 515d6e08d0..e1ed9c5d52 100644 --- a/pype/ftrack/actions/action_create_folders.py +++ b/pype/ftrack/actions/action_create_folders.py @@ -1,13 +1,15 @@ -import logging import os -import argparse import sys -import errno +import logging +import argparse +import re +import json import ftrack_api from pype.ftrack import BaseAction -import json -from pype import api as pype +from pype import api as pype, lib as pypelib +from avalon import lib as avalonlib +from avalon.tools.libraryloader.io_nonsingleton import DbConnector class CreateFolders(BaseAction): @@ -35,6 +37,57 @@ class CreateFolders(BaseAction): return False return True + def interface(self, session, entities, event): + if event['data'].get('values', {}): + return + entity = entities[0] + without_interface = True + for child in entity['children']: + if child['object_type']['name'].lower() != 'task': + without_interface = False + break + self.without_interface = without_interface + if without_interface: + return + title = 'Create folders' + + entity_name = entity['name'] + msg = ( + '

Do you want create folders also' + ' for all children of "{}"?

' + ) + if entity.entity_type.lower() == 'project': + entity_name = entity['full_name'] + msg = msg.replace(' also', '') + msg += '

(Project root won\'t be created if not checked)

' + items = [] + item_msg = { + 'type': 'label', + 'value': msg.format(entity_name) + } + item_label = { + 'type': 'label', + 'value': 'With all chilren entities' + } + item = { + 'name': 'children_included', + 'type': 'boolean', + 'value': False + } + items.append(item_msg) + items.append(item_label) + items.append(item) + + if len(items) == 0: + return { + 'success': False, + 'message': 'Didn\'t found any running jobs' + } + else: + return { + 'items': items, + 'title': title + } def launch(self, session, entities, event): '''Callback method for custom action.''' From 4db62fcb8442c65415c600cd1ec179f60c55d2ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 10 Apr 2019 11:36:37 +0200 Subject: [PATCH 14/29] interfae handling added --- pype/ftrack/actions/action_create_folders.py | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py index e1ed9c5d52..1762683a3d 100644 --- a/pype/ftrack/actions/action_create_folders.py +++ b/pype/ftrack/actions/action_create_folders.py @@ -91,6 +91,30 @@ class CreateFolders(BaseAction): def launch(self, session, entities, event): '''Callback method for custom action.''' + with_childrens = True + if self.without_interface is False: + if 'values' not in event['data']: + return + with_childrens = event['data']['values']['children_included'] + entity = entities[0] + if entity.entity_type.lower() == 'project': + proj = entity + else: + proj = entity['project'] + project_name = proj['full_name'] + project_code = proj['name'] + if entity.entity_type.lower() == 'project' and with_childrens == False: + return { + 'success': True, + 'message': 'Nothing was created' + } + data = { + "root": os.environ["AVALON_PROJECTS"], + "project": { + "name": project_name, + "code": project_code + } + } ####################################################################### From 67a2715034cccbd2316fe6ab838d02eb4bf7ecc8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 10 Apr 2019 11:40:32 +0200 Subject: [PATCH 15/29] added getting filled anatomy paths logic --- pype/ftrack/actions/action_create_folders.py | 173 ++++++++++--------- 1 file changed, 96 insertions(+), 77 deletions(-) diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py index 1762683a3d..15ca86809d 100644 --- a/pype/ftrack/actions/action_create_folders.py +++ b/pype/ftrack/actions/action_create_folders.py @@ -27,6 +27,7 @@ class CreateFolders(BaseAction): 'https://cdn1.iconfinder.com/data/icons/hawcons/32/' '698620-icon-105-folder-add-512.png' ) + db = DbConnector() def discover(self, session, entities, event): ''' Validation ''' @@ -115,87 +116,105 @@ class CreateFolders(BaseAction): "code": project_code } } + all_entities = [] + all_entities.append(entity) + if with_childrens: + all_entities = self.get_notask_children(entity) - ####################################################################### - - # JOB SETTINGS - userId = event['source']['user']['id'] - user = session.query('User where id is ' + userId).one() - - job = session.create('Job', { - 'user': user, - 'status': 'running', - 'data': json.dumps({ - 'description': 'Creating Folders.' - }) - }) - + av_project = None try: - self.importable = set([]) - # self.importable = [] - - self.Anatomy = pype.Anatomy - - project = entities[0]['project'] - - paths_collected = set([]) - - # get all child entities separately/unique - for entity in entities: - self.getShotAsset(entity) - - for ent in self.importable: - self.log.info("{}".format(ent['name'])) - - for entity in self.importable: - print(entity['name']) - - anatomy = pype.Anatomy - parents = entity['link'] - - hierarchy_names = [] - for p in parents[1:-1]: - hierarchy_names.append(p['name']) - - if hierarchy_names: - # hierarchy = os.path.sep.join(hierarchy) - hierarchy = os.path.join(*hierarchy_names) - - template_data = {"project": {"name": project['full_name'], - "code": project['name']}, - "asset": entity['name'], - "hierarchy": hierarchy} - - for task in entity['children']: - if task['object_type']['name'] == 'Task': - self.log.info('child: {}'.format(task['name'])) - template_data['task'] = task['name'] - anatomy_filled = anatomy.format(template_data) - paths_collected.add(anatomy_filled.work.folder) - paths_collected.add(anatomy_filled.publish.folder) - - for path in paths_collected: - self.log.info(path) - try: - os.makedirs(path) - except OSError as error: - if error.errno != errno.EEXIST: - raise - - job['status'] = 'done' - session.commit() - - except ValueError as ve: - job['status'] = 'failed' - session.commit() - message = str(ve) - self.log.error('Error during syncToAvalon: {}'.format(message)) - + self.db.install() + self.db.Session['AVALON_PROJECT'] = project_name + av_project = self.db.find_one({'type': 'project'}) + template_work = av_project['config']['template']['work'] + template_publish = av_project['config']['template']['publish'] + self.db.uninstall() except Exception: - job['status'] = 'failed' - session.commit() + anatomy = pype.Anatomy + template_work = anatomy.avalon.work + template_publish = anatomy.avalon.publish - ####################################################################### + collected_paths = [] + presets = self.get_presets() + for entity in all_entities: + if entity.entity_type.lower() == 'project': + continue + ent_data = data.copy() + + asset_name = entity['name'] + ent_data['asset'] = asset_name + + parents = entity['link'] + hierarchy_names = [p['name'] for p in parents[1:-1]] + hierarchy = '' + if hierarchy_names: + hierarchy = os.path.sep.join(hierarchy_names) + ent_data['hierarchy'] = hierarchy + + tasks_created = False + if entity['children']: + for child in entity['children']: + if child['object_type']['name'].lower() != 'task': + continue + tasks_created = True + task_type_name = child['type']['name'].lower() + task_data = ent_data.copy() + task_data['task'] = child['name'] + possible_apps = presets.get(task_type_name, []) + template_work_created = False + template_publish_created = False + apps = [] + for app in possible_apps: + try: + app_data = avalonlib.get_application(app) + app_dir = app_data['application_dir'] + except ValueError: + app_dir = app + apps.append(app_dir) + + # Template wok + if '{app}' in template_work: + for app in apps: + template_work_created = True + app_data = task_data.copy() + app_data['app'] = app + collected_paths.append( + self.compute_template( + template_work, app_data + ) + ) + if template_work_created is False: + collected_paths.append( + self.compute_template(template_work, task_data) + ) + # Template publish + if '{app}' in template_publish: + for app in apps: + template_publish_created = True + app_data = task_data.copy() + app_data['app'] = app + collected_paths.append( + self.compute_template( + template_publish, app_data + ) + ) + if template_publish_created is False: + collected_paths.append( + self.compute_template(template_publish, task_data) + ) + + if not tasks_created: + # create path for entity + collected_paths.append( + self.compute_template(template_work, ent_data) + ) + collected_paths.append( + self.compute_template(template_publish, ent_data) + ) + + for path in set(collected_paths): + if not os.path.exists(path): + os.makedirs(path) return { 'success': True, From 7a26a52358256d8e08572f834f1cb4a71d5f2d7f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 10 Apr 2019 11:40:54 +0200 Subject: [PATCH 16/29] removed create sw folders action --- .../actions/action_create_sw_folders.py | 155 ------------------ 1 file changed, 155 deletions(-) delete mode 100644 pype/ftrack/actions/action_create_sw_folders.py diff --git a/pype/ftrack/actions/action_create_sw_folders.py b/pype/ftrack/actions/action_create_sw_folders.py deleted file mode 100644 index f6b14cb764..0000000000 --- a/pype/ftrack/actions/action_create_sw_folders.py +++ /dev/null @@ -1,155 +0,0 @@ -import os -import sys -import json -import argparse -import logging - -import ftrack_api -from avalon import lib as avalonlib -from avalon.tools.libraryloader.io_nonsingleton import DbConnector -from pype import lib as pypelib -from pype.ftrack import BaseAction - - -class CreateSWFolders(BaseAction): - '''Edit meta data action.''' - - #: Action identifier. - identifier = 'create.sw.folders' - #: Action label. - label = 'Create SW Folders' - #: Action description. - description = 'Creates folders for all SW in project' - - - def __init__(self, session): - super().__init__(session) - self.avalon_db = DbConnector() - self.avalon_db.install() - - def discover(self, session, entities, event): - ''' Validation ''' - - return True - - def launch(self, session, entities, event): - if len(entities) != 1: - self.log.warning( - 'There are more entities in selection!' - ) - return False - entity = entities[0] - if entity.entity_type.lower() != 'task': - self.log.warning( - 'Selected entity is not Task!' - ) - return False - asset = entity['parent'] - project = asset['project'] - - project_name = project["full_name"] - self.avalon_db.Session['AVALON_PROJECT'] = project_name - av_project = self.avalon_db.find_one({'type': 'project'}) - av_asset = self.avalon_db.find_one({ - 'type': 'asset', - 'name': asset['name'] - }) - - templates = av_project["config"]["template"] - template = templates.get("work", None) - if template is None: - return False - - - data = { - "root": os.environ["AVALON_PROJECTS"], - "project": { - "name": project_name, - "code": project["name"] - }, - "hierarchy": av_asset['data']['hierarchy'], - "asset": asset['name'], - "task": entity['name'], - } - - apps = [] - if '{app}' in template: - # Apps in project - for app in av_project['data']['applications']: - app_data = avalonlib.get_application(app) - app_dir = app_data['application_dir'] - if app_dir not in apps: - apps.append(app_dir) - # Apps in presets - path_items = [pypelib.get_presets_path(), 'tools', 'sw_folders.json'] - filepath = os.path.sep.join(path_items) - - presets = dict() - try: - with open(filepath) as data_file: - presets = json.load(data_file) - except Exception as e: - self.log.warning('Wasn\'t able to load presets') - preset_apps = presets.get(project_name, presets.get('__default__', [])) - for app in preset_apps: - if app not in apps: - apps.append(app) - - # Create folders for apps - for app in apps: - data['app'] = app - self.log.info('Created folder for app {}'.format(app)) - path = os.path.normpath(template.format(**data)) - if os.path.exists(path): - continue - os.makedirs(path) - - return True - - -def register(session, **kw): - '''Register plugin. Called when used as an plugin.''' - - if not isinstance(session, ftrack_api.session.Session): - return - - CreateSWFolders(session).register() - - -def main(arguments=None): - '''Set up logging and register action.''' - if arguments is None: - arguments = [] - - parser = argparse.ArgumentParser() - # Allow setting of logging level from arguments. - loggingLevels = {} - for level in ( - logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, - logging.ERROR, logging.CRITICAL - ): - loggingLevels[logging.getLevelName(level).lower()] = level - - parser.add_argument( - '-v', '--verbosity', - help='Set the logging output verbosity.', - choices=loggingLevels.keys(), - default='info' - ) - namespace = parser.parse_args(arguments) - - # Set up basic logging - logging.basicConfig(level=loggingLevels[namespace.verbosity]) - - session = ftrack_api.Session() - register(session) - - # Wait for events - logging.info( - 'Registered actions and listening for events. Use Ctrl-C to abort.' - ) - session.event_hub.wait() - - -if __name__ == '__main__': - raise SystemExit(main(sys.argv[1:])) From 30fa666f9114f5f1a8a5da3bb598d14d64166e2d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 Apr 2019 15:25:11 +0200 Subject: [PATCH 17/29] typo(plugins): removing EXR from extract_quicktime --- pype/plugins/global/publish/extract_quicktime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/extract_quicktime.py b/pype/plugins/global/publish/extract_quicktime.py index 621078e3c0..fd34c46e5e 100644 --- a/pype/plugins/global/publish/extract_quicktime.py +++ b/pype/plugins/global/publish/extract_quicktime.py @@ -15,7 +15,7 @@ class ExtractQuicktimeEXR(pyblish.api.InstancePlugin): publish the shading network. Same goes for file dependent assets. """ - label = "Extract Quicktime EXR" + label = "Extract Quicktime" order = pyblish.api.ExtractorOrder families = ["imagesequence", "render", "write", "source"] hosts = ["shell"] From 9b1795c552b3b49ef94741a9c342987a1a290039 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 Apr 2019 15:30:20 +0200 Subject: [PATCH 18/29] feat(plugins): adding synchronization versions into `integrate_ftrack_instances` --- pype/plugins/ftrack/publish/integrate_ftrack_instances.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py index d8e9e116f9..e166af2954 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py @@ -38,6 +38,9 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): assumed_data = instance.data["assumedTemplateData"] assumed_version = assumed_data["version"] version_number = int(assumed_version) + if instance.data.get('version'): + version_number = int(instance.data.get('version')) + family = instance.data['family'].lower() asset_type = '' From 2d8b07cbb86cb9b6f47e65c24a34edbd1b34e288 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 Apr 2019 16:38:09 +0200 Subject: [PATCH 19/29] fix(nuke): moving data update into format_anatomy() from create_write_node() --- pype/nuke/lib.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index aa8ebbd995..96e3dfb24a 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -1,4 +1,5 @@ import sys +import os from collections import OrderedDict from pprint import pprint from avalon.vendor.Qt import QtGui @@ -62,17 +63,27 @@ def format_anatomy(data): from .templates import ( get_anatomy ) - file = script_name() anatomy = get_anatomy() # TODO: perhaps should be in try! padding = anatomy.render.padding + version = data.get("version", None) + if not version: + file = script_name() + data["version"] = pype.get_version_from_path(file) data.update({ + "subset": data["avalon"]["subset"], + "asset": data["avalon"]["asset"], + "task": pype.get_task(), + "family": data["avalon"]["family"], + "project": {"name": pype.get_project_name(), + "code": pype.get_project_code()}, + "representation": ["nuke_dataflow_writes"].file_type, + "app": data["application"]["application_dir"], "hierarchy": pype.get_hierarchy(), - "frame": "#"*padding, - "version": pype.get_version_from_path(file) + "frame": "#" * padding, }) # log.info("format_anatomy:anatomy: {}".format(anatomy)) @@ -88,20 +99,20 @@ def create_write_node(name, data): get_dataflow, get_colorspace ) + nuke_dataflow_writes = get_dataflow(**data) nuke_colorspace_writes = get_colorspace(**data) application = lib.get_application(os.environ["AVALON_APP_NAME"]) + try: - anatomy_filled = format_anatomy({ - "subset": data["avalon"]["subset"], - "asset": data["avalon"]["asset"], - "task": pype.get_task(), - "family": data["avalon"]["family"], - "project": {"name": pype.get_project_name(), - "code": pype.get_project_code()}, - "representation": nuke_dataflow_writes.file_type, - "app": application["application_dir"], + data.update({ + "application": application, + "nuke_dataflow_writes": nuke_dataflow_writes, + "nuke_colorspace_writes": nuke_colorspace_writes }) + + anatomy_filled = format_anatomy(data) + except Exception as e: log.error("problem with resolving anatomy tepmlate: {}".format(e)) @@ -134,7 +145,6 @@ def create_write_node(name, data): add_rendering_knobs(instance) return instance - def add_rendering_knobs(node): if "render" not in node.knobs(): knob = nuke.Boolean_Knob("render", "Render") From 115937ab00e27c2f741190dd71ee526000133b9f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 Apr 2019 16:39:15 +0200 Subject: [PATCH 20/29] feat(nuke): adding validating plugins for script, version match, write node attributes --- pype/plugins/nuke/publish/validate_script.py | 2 +- .../nuke/publish/validate_version_match.py | 29 ++++++++++++++++++- .../validate_write_nodes.py | 2 +- 3 files changed, 30 insertions(+), 3 deletions(-) rename pype/plugins/nuke/{_publish_unused => publish}/validate_write_nodes.py (98%) diff --git a/pype/plugins/nuke/publish/validate_script.py b/pype/plugins/nuke/publish/validate_script.py index a4ec60d96d..5939083a61 100644 --- a/pype/plugins/nuke/publish/validate_script.py +++ b/pype/plugins/nuke/publish/validate_script.py @@ -8,7 +8,7 @@ class ValidateScript(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder + 0.1 families = ["nukescript"] - label = "Check nukescript settings" + label = "Check script settings" hosts = ["nuke"] def process(self, instance): diff --git a/pype/plugins/nuke/publish/validate_version_match.py b/pype/plugins/nuke/publish/validate_version_match.py index 64646ea5dc..532a6ff0ad 100644 --- a/pype/plugins/nuke/publish/validate_version_match.py +++ b/pype/plugins/nuke/publish/validate_version_match.py @@ -1,4 +1,30 @@ +import os import pyblish.api +import pype.utils + + +@pyblish.api.log +class RepairNukeWriteNodeVersionAction(pyblish.api.Action): + label = "Repair" + on = "failed" + icon = "wrench" + + def process(self, context, plugin): + + instances = pype.utils.filter_instances(context, plugin) + + for instance in instances: + if "create_directories" in instance[0].knobs(): + instance[0]['create_directories'].setValue(True) + else: + path, file = os.path.split(instance[0].data['outputFilename']) + self.log.info(path) + + if not os.path.exists(path): + os.makedirs(path) + + if "metadata" in instance[0].knobs().keys(): + instance[0]["metadata"].setValue("all metadata") class ValidateVersionMatch(pyblish.api.InstancePlugin): @@ -6,8 +32,9 @@ class ValidateVersionMatch(pyblish.api.InstancePlugin): label = "Validate Version Match" order = pyblish.api.ValidatorOrder + actions = [RepairNukeWriteNodeVersionAction] hosts = ["nuke"] - families = ['render.frames'] + families = ['write'] def process(self, instance): diff --git a/pype/plugins/nuke/_publish_unused/validate_write_nodes.py b/pype/plugins/nuke/publish/validate_write_nodes.py similarity index 98% rename from pype/plugins/nuke/_publish_unused/validate_write_nodes.py rename to pype/plugins/nuke/publish/validate_write_nodes.py index e58c0ed585..c31ac045f6 100644 --- a/pype/plugins/nuke/_publish_unused/validate_write_nodes.py +++ b/pype/plugins/nuke/publish/validate_write_nodes.py @@ -32,7 +32,7 @@ class ValidateNukeWriteNode(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder optional = True - families = ["write.render"] + families = ["write"] label = "Write Node" actions = [RepairNukeWriteNodeAction] hosts = ["nuke"] From d531a69285a70cbc58d04b46b4a388cda899189a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 Apr 2019 16:43:52 +0200 Subject: [PATCH 21/29] fix(nuke): moving PYBLISH_GUI var from avalon.core --- pype/nuke/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/nuke/__init__.py b/pype/nuke/__init__.py index ae00342f09..c89592d030 100644 --- a/pype/nuke/__init__.py +++ b/pype/nuke/__init__.py @@ -36,6 +36,9 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "nuke", "inventory") self = sys.modules[__name__] self.nLogger = None +if os.getenv("PYBLISH_GUI", None): + pyblish.register_gui(os.getenv("PYBLISH_GUI", None)) + class NukeHandler(api.Logger.logging.Handler): ''' From b8f64d293e6a5f048b041ced5264d5031a045ba9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 Apr 2019 18:33:09 +0200 Subject: [PATCH 22/29] feat(nuke): updating validation of nodes write --- pype/nuke/lib.py | 30 +++++++++++++++++-- .../validate_version_match.py | 18 ++++------- .../nuke/publish/validate_write_nodes.py | 26 +++++++++++----- 3 files changed, 52 insertions(+), 22 deletions(-) rename pype/plugins/nuke/{publish => _publish_unused}/validate_version_match.py (63%) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index 96e3dfb24a..f5467c3ddc 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -59,6 +59,32 @@ def version_up_script(): nukescripts.script_and_write_nodes_version_up() +def get_render_path(node): + from .templates import ( + get_dataflow, + get_colorspace + ) + data = dict() + data['avalon'] = get_avalon_knob_data(node) + + data_preset = { + "class": data['avalon']['family'], + "preset": data['avalon']['families'] + } + + nuke_dataflow_writes = get_dataflow(**data_preset) + nuke_colorspace_writes = get_colorspace(**data_preset) + + application = lib.get_application(os.environ["AVALON_APP_NAME"]) + data.update({ + "application": application, + "nuke_dataflow_writes": nuke_dataflow_writes, + "nuke_colorspace_writes": nuke_colorspace_writes + }) + + anatomy_filled = format_anatomy(data) + return anatomy_filled.render.path + def format_anatomy(data): from .templates import ( get_anatomy @@ -76,11 +102,11 @@ def format_anatomy(data): data.update({ "subset": data["avalon"]["subset"], "asset": data["avalon"]["asset"], - "task": pype.get_task(), + "task": str(pype.get_task()).lower(), "family": data["avalon"]["family"], "project": {"name": pype.get_project_name(), "code": pype.get_project_code()}, - "representation": ["nuke_dataflow_writes"].file_type, + "representation": data["nuke_dataflow_writes"].file_type, "app": data["application"]["application_dir"], "hierarchy": pype.get_hierarchy(), "frame": "#" * padding, diff --git a/pype/plugins/nuke/publish/validate_version_match.py b/pype/plugins/nuke/_publish_unused/validate_version_match.py similarity index 63% rename from pype/plugins/nuke/publish/validate_version_match.py rename to pype/plugins/nuke/_publish_unused/validate_version_match.py index 532a6ff0ad..1358d9a7b3 100644 --- a/pype/plugins/nuke/publish/validate_version_match.py +++ b/pype/plugins/nuke/_publish_unused/validate_version_match.py @@ -3,6 +3,7 @@ import pyblish.api import pype.utils + @pyblish.api.log class RepairNukeWriteNodeVersionAction(pyblish.api.Action): label = "Repair" @@ -10,21 +11,14 @@ class RepairNukeWriteNodeVersionAction(pyblish.api.Action): icon = "wrench" def process(self, context, plugin): - + import pype.nuke.lib as nukelib instances = pype.utils.filter_instances(context, plugin) for instance in instances: - if "create_directories" in instance[0].knobs(): - instance[0]['create_directories'].setValue(True) - else: - path, file = os.path.split(instance[0].data['outputFilename']) - self.log.info(path) - - if not os.path.exists(path): - os.makedirs(path) - - if "metadata" in instance[0].knobs().keys(): - instance[0]["metadata"].setValue("all metadata") + node = instance[0] + render_path = nukelib.get_render_path(node) + self.log.info("render_path: {}".format(render_path)) + node['file'].setValue(render_path.replace("\\", "/")) class ValidateVersionMatch(pyblish.api.InstancePlugin): diff --git a/pype/plugins/nuke/publish/validate_write_nodes.py b/pype/plugins/nuke/publish/validate_write_nodes.py index c31ac045f6..9f56ae90f8 100644 --- a/pype/plugins/nuke/publish/validate_write_nodes.py +++ b/pype/plugins/nuke/publish/validate_write_nodes.py @@ -2,7 +2,6 @@ import os import pyblish.api import pype.utils - @pyblish.api.log class RepairNukeWriteNodeAction(pyblish.api.Action): label = "Repair" @@ -10,20 +9,25 @@ class RepairNukeWriteNodeAction(pyblish.api.Action): icon = "wrench" def process(self, context, plugin): - + import pype.nuke.lib as nukelib instances = pype.utils.filter_instances(context, plugin) + for instance in instances: + node = instance[0] + render_path = nukelib.get_render_path(node).replace("\\", "/") + self.log.info("render_path: {}".format(render_path)) + node['file'].setValue(render_path) if "create_directories" in instance[0].knobs(): - instance[0]['create_directories'].setValue(True) + node['create_directories'].setValue(True) else: - path, file = os.path.split(instance[0].data['outputFilename']) + path, file = os.path.split(render_path) self.log.info(path) if not os.path.exists(path): os.makedirs(path) - if "metadata" in instance[0].knobs().keys(): + if "metadata" in node.knobs().keys(): instance[0]["metadata"].setValue("all metadata") @@ -38,10 +42,16 @@ class ValidateNukeWriteNode(pyblish.api.InstancePlugin): hosts = ["nuke"] def process(self, instance): + import pype.nuke.lib as nukelib + # validate: create_directories, created path, node file knob, version, metadata + # TODO: colorspace, dataflow from presets + node = instance[0] + render_path = nukelib.get_render_path(node).replace("\\", "/") + self.log.info("render_path: {}".format(render_path)) + + msg_file = "path is not correct" + assert node['file'].value() is render_path, msg_file - # Validate output directory exists, if not creating directories. - # The existence of the knob is queried because previous version - # of Nuke did not have this feature. if "create_directories" in instance[0].knobs(): msg = "Use Create Directories" assert instance[0].knobs()['create_directories'].value() is True, msg From bf0f11e9925faab3da97d5b5e1cfafabb7bf2b90 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 10 Apr 2019 19:13:00 +0200 Subject: [PATCH 23/29] on task fill returns path up to first unfilled key & added path log --- pype/ftrack/actions/action_create_folders.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py index 15ca86809d..f7620cd609 100644 --- a/pype/ftrack/actions/action_create_folders.py +++ b/pype/ftrack/actions/action_create_folders.py @@ -195,12 +195,14 @@ class CreateFolders(BaseAction): app_data['app'] = app collected_paths.append( self.compute_template( - template_publish, app_data + template_publish, app_data, True ) ) if template_publish_created is False: collected_paths.append( - self.compute_template(template_publish, task_data) + self.compute_template( + template_publish, task_data, True + ) ) if not tasks_created: @@ -211,8 +213,10 @@ class CreateFolders(BaseAction): collected_paths.append( self.compute_template(template_publish, ent_data) ) - + if len(collected_paths) > 0: + self.log.info('Creating folders:') for path in set(collected_paths): + self.log.info(path) if not os.path.exists(path): os.makedirs(path) @@ -298,10 +302,12 @@ class CreateFolders(BaseAction): return solved - def compute_template(self, str, data): + def compute_template(self, str, data, task=False): first_result = self.template_format(str, data) if first_result == first_result.split('{')[0]: return os.path.normpath(first_result) + if task: + return os.path.normpath(first_result.split('{')[0]) index = first_result.index('{') From ed4feae385e6d335eea8c06f232daef62589f8a1 Mon Sep 17 00:00:00 2001 From: antirotor Date: Wed, 10 Apr 2019 22:16:42 +0200 Subject: [PATCH 24/29] feat(rv): basic working version of rv action --- pype/ftrack/actions/action_rv.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/pype/ftrack/actions/action_rv.py b/pype/ftrack/actions/action_rv.py index 7a45ca9886..a1b46f9e74 100644 --- a/pype/ftrack/actions/action_rv.py +++ b/pype/ftrack/actions/action_rv.py @@ -5,7 +5,9 @@ import json import subprocess import ftrack_api import logging -from pype import pypelib +import operator +import re +from pype import lib as pypelib from app.api import Logger log = Logger.getLogger(__name__) @@ -13,7 +15,7 @@ log = Logger.getLogger(__name__) class RVAction(BaseAction): """ Launch RV action """ - identifier = "rv-launch-action" + identifier = "rv.launch.action" label = "rv" description = "rv Launcher" icon = "https://img.icons8.com/color/48/000000/circled-play.png" @@ -166,7 +168,7 @@ class RVAction(BaseAction): 'name': 'path', 'data': sorted( items, - key=itemgetter('label'), + key=operator.itemgetter('label'), reverse=True ) } @@ -179,7 +181,6 @@ class RVAction(BaseAction): def launch(self, session, entities, event): """Callback method for RV action.""" - # Launching application if "values" not in event["data"]: return @@ -188,20 +189,32 @@ class RVAction(BaseAction): fps = entities[0].get('custom_attributes', {}).get('fps', None) cmd = [] + # change frame number to padding string for RV to play sequence + try: + frame = re.findall(r'(\d+).', filename)[-1] + except KeyError: + # we didn't detected frame number + pass + else: + padding = '#' * len(frame) + pos = filename.rfind(frame) + filename = filename[:pos] + padding + filename[ + filename.rfind('.'):] + # RV path cmd.append(os.path.normpath(self.rv_path)) if fps is not None: cmd.append("-fps {}".format(int(fps))) cmd.append(os.path.normpath(filename)) - + log.info('Running rv: {}'.format(' '.join(cmd))) try: # Run RV with these commands - subprocess.Popen(' '.join(cmd)) - except FileNotFoundError: + subprocess.Popen(' '.join(cmd), shell=True) + except Exception as e: return { 'success': False, 'message': 'File "{}" was not found.'.format( - os.path.basename(filename) + e ) } From e86da58990299d3e88f95c31b4ba998226c80bb3 Mon Sep 17 00:00:00 2001 From: antirotor Date: Wed, 10 Apr 2019 22:20:08 +0200 Subject: [PATCH 25/29] fix(rv): fixed handling of RV_HOME --- pype/ftrack/actions/action_rv.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/ftrack/actions/action_rv.py b/pype/ftrack/actions/action_rv.py index a1b46f9e74..d5ff83e8c8 100644 --- a/pype/ftrack/actions/action_rv.py +++ b/pype/ftrack/actions/action_rv.py @@ -33,7 +33,11 @@ class RVAction(BaseAction): # RV_HOME should be set if properly installed if os.environ.get('RV_HOME'): - self.rv_path = os.environ.get('RV_HOME') + self.rv_path = os.path.join( + os.environ.get('RV_HOME'), + 'bin', + 'rv' + ) else: # if not, fallback to config file location self.load_config_data() From 38bd772d86e211c1b3549e066b2073aafe4ba492 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 11 Apr 2019 12:19:22 +0200 Subject: [PATCH 26/29] feat(nuke): validating write node done with additional functions in pype.nuke.lib --- pype/nuke/lib.py | 50 ++++++++++--- .../nuke/publish/validate_write_nodes.py | 71 ++++++++++--------- 2 files changed, 77 insertions(+), 44 deletions(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index f5467c3ddc..f472b4cb0a 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -7,6 +7,10 @@ from avalon import api, io, lib import avalon.nuke import pype.api as pype import nuke +from .templates import ( + get_dataflow, + get_colorspace +) log = pype.Logger.getLogger(__name__, "nuke") self = sys.modules[__name__] @@ -60,10 +64,7 @@ def version_up_script(): def get_render_path(node): - from .templates import ( - get_dataflow, - get_colorspace - ) + data = dict() data['avalon'] = get_avalon_knob_data(node) @@ -83,7 +84,7 @@ def get_render_path(node): }) anatomy_filled = format_anatomy(data) - return anatomy_filled.render.path + return anatomy_filled.render.path.replace("\\", "/") def format_anatomy(data): from .templates import ( @@ -121,11 +122,6 @@ def script_name(): def create_write_node(name, data): - from .templates import ( - get_dataflow, - get_colorspace - ) - nuke_dataflow_writes = get_dataflow(**data) nuke_colorspace_writes = get_colorspace(**data) application = lib.get_application(os.environ["AVALON_APP_NAME"]) @@ -437,3 +433,37 @@ def get_additional_data(container): ] return {"color": QtGui.QColor().fromRgbF(rgba[0], rgba[1], rgba[2])} + + +def get_write_node_template_attr(node): + ''' Gets all defined data from presets + + ''' + # get avalon data from node + data = dict() + data['avalon'] = get_avalon_knob_data(node) + data_preset = { + "class": data['avalon']['family'], + "preset": data['avalon']['families'] + } + + # get template data + nuke_dataflow_writes = get_dataflow(**data_preset) + nuke_colorspace_writes = get_colorspace(**data_preset) + + # collecting correct data + correct_data = OrderedDict({ + "file": get_render_path(node) + }) + + # adding dataflow template + {correct_data.update({k: v}) + for k, v in nuke_dataflow_writes.items() + if k not in ["id", "previous"]} + + # adding colorspace template + {correct_data.update({k: v}) + for k, v in nuke_colorspace_writes.items()} + + # fix badly encoded data + return avalon.nuke.lib.fix_data_for_node_create(correct_data) diff --git a/pype/plugins/nuke/publish/validate_write_nodes.py b/pype/plugins/nuke/publish/validate_write_nodes.py index 9f56ae90f8..105c133ebe 100644 --- a/pype/plugins/nuke/publish/validate_write_nodes.py +++ b/pype/plugins/nuke/publish/validate_write_nodes.py @@ -1,6 +1,8 @@ import os import pyblish.api import pype.utils +import pype.nuke.lib as nukelib +import avalon.nuke @pyblish.api.log class RepairNukeWriteNodeAction(pyblish.api.Action): @@ -9,26 +11,14 @@ class RepairNukeWriteNodeAction(pyblish.api.Action): icon = "wrench" def process(self, context, plugin): - import pype.nuke.lib as nukelib instances = pype.utils.filter_instances(context, plugin) for instance in instances: node = instance[0] - render_path = nukelib.get_render_path(node).replace("\\", "/") - self.log.info("render_path: {}".format(render_path)) - node['file'].setValue(render_path) - - if "create_directories" in instance[0].knobs(): - node['create_directories'].setValue(True) - else: - path, file = os.path.split(render_path) - self.log.info(path) - - if not os.path.exists(path): - os.makedirs(path) - - if "metadata" in node.knobs().keys(): - instance[0]["metadata"].setValue("all metadata") + correct_data = nukelib.get_write_node_template_attr(node) + for k, v in correct_data.items(): + node[k].setValue(v) + self.log.info("Node attributes were fixed") class ValidateNukeWriteNode(pyblish.api.InstancePlugin): @@ -42,25 +32,38 @@ class ValidateNukeWriteNode(pyblish.api.InstancePlugin): hosts = ["nuke"] def process(self, instance): - import pype.nuke.lib as nukelib - # validate: create_directories, created path, node file knob, version, metadata - # TODO: colorspace, dataflow from presets + node = instance[0] - render_path = nukelib.get_render_path(node).replace("\\", "/") - self.log.info("render_path: {}".format(render_path)) + correct_data = nukelib.get_write_node_template_attr(node) - msg_file = "path is not correct" - assert node['file'].value() is render_path, msg_file + check = [] + for k, v in correct_data.items(): + if k is 'file': + padding = len(v.split('#')) + ref_path = avalon.nuke.lib.get_node_path(v, padding) + n_path = avalon.nuke.lib.get_node_path(node[k].value(), padding) + isnt = False + for i, p in enumerate(ref_path): + if str(n_path[i]) not in str(p): + if not isnt: + isnt = True + else: + continue + if isnt: + check.append([k, v, node[k].value()]) + else: + if str(node[k].value()) not in str(v): + check.append([k, v, node[k].value()]) - if "create_directories" in instance[0].knobs(): - msg = "Use Create Directories" - assert instance[0].knobs()['create_directories'].value() is True, msg - else: - path, file = os.path.split(instance.data['outputFilename']) - msg = "Output directory doesn't exist: \"{0}\"".format(path) - assert os.path.exists(path), msg + self.log.info(check) - # Validate metadata knob - if "metadata" in instance[0].knobs().keys(): - msg = "Metadata needs to be set to \"all metadata\"." - assert instance[0]["metadata"].value() == "all metadata", msg + msg = "Node's attribute `{0}` is not correct!\n" \ + "\nCorrect: `{1}` \n\nWrong: `{2}` \n\n" + + if check: + print_msg = "" + for item in check: + print_msg += msg.format(item[0], item[1], item[2]) + print_msg += "`RMB` click to the validator and `A` to fix!" + + assert not check, print_msg From 7c5555d2b59e7e0bed33ee77d6eb378b7a890999 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 11 Apr 2019 13:02:01 +0200 Subject: [PATCH 27/29] feat(nuke): cleaning --- pype/nuke/__init__.py | 8 +++++--- pype/nuke/test_atom_server.py | 3 --- 2 files changed, 5 insertions(+), 6 deletions(-) delete mode 100644 pype/nuke/test_atom_server.py diff --git a/pype/nuke/__init__.py b/pype/nuke/__init__.py index c89592d030..106efee0e7 100644 --- a/pype/nuke/__init__.py +++ b/pype/nuke/__init__.py @@ -92,13 +92,15 @@ def reload_config(): "{}.templates".format(AVALON_CONFIG), "{}.nuke.actions".format(AVALON_CONFIG), "{}.nuke.templates".format(AVALON_CONFIG), - "{}.nuke.menu".format(AVALON_CONFIG) + "{}.nuke.menu".format(AVALON_CONFIG), + "{}.nuke.lib".format(AVALON_CONFIG), ): log.info("Reloading module: {}...".format(module)) - module = importlib.import_module(module) try: + module = importlib.import_module(module) reload(module) - except Exception: + except Exception as e: + log.warning("Cannot reload module: {}".format(e)) importlib.reload(module) diff --git a/pype/nuke/test_atom_server.py b/pype/nuke/test_atom_server.py deleted file mode 100644 index 8e026bbf1a..0000000000 --- a/pype/nuke/test_atom_server.py +++ /dev/null @@ -1,3 +0,0 @@ -import nuke -n = nuke.createNode("Constant") -print(n) From 0eca2b9b1d7649d3e0348d104b0ab8d24b31ec70 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 Apr 2019 16:23:35 +0200 Subject: [PATCH 28/29] fix(nuke): create_write lost some settings so now they are back --- pype/nuke/lib.py | 1 + pype/plugins/nuke/create/create_write.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index aa8ebbd995..ebf01798f7 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -1,3 +1,4 @@ +import os import sys from collections import OrderedDict from pprint import pprint diff --git a/pype/plugins/nuke/create/create_write.py b/pype/plugins/nuke/create/create_write.py index af7462680e..76a5fcc538 100644 --- a/pype/plugins/nuke/create/create_write.py +++ b/pype/plugins/nuke/create/create_write.py @@ -25,7 +25,7 @@ class CrateWriteRender(avalon.nuke.Creator): name = "WriteRender" label = "Create Write Render" hosts = ["nuke"] - family = "write" + family = "{}_write".format(preset) families = preset icon = "sign-out" @@ -68,7 +68,7 @@ class CrateWritePrerender(avalon.nuke.Creator): name = "WritePrerender" label = "Create Write Prerender" hosts = ["nuke"] - family = "write" + family = "{}_write".format(preset) families = preset icon = "sign-out" @@ -111,7 +111,7 @@ class CrateWriteStill(avalon.nuke.Creator): name = "WriteStill" label = "Create Write Still" hosts = ["nuke"] - family = "write" + family = "{}_write".format(preset) families = preset icon = "image" From b262c304ce37a60606e3ebd53da943fbafb68535 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 12 Apr 2019 18:53:14 +0200 Subject: [PATCH 29/29] fix logging --- pype/ftrack/lib/ftrack_app_handler.py | 2 +- pype/plugins/launcher/actions/ClockifyStart.py | 2 +- pype/plugins/launcher/actions/ClockifySync.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 138b6d6459..f5c7cfc68b 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -190,7 +190,7 @@ class AppAction(BaseHandler): os.environ["AVALON_APP"] = self.identifier.split("_")[0] os.environ["AVALON_APP_NAME"] = self.identifier - anatomy = Anatomy(project_name=project_name) + anatomy = Anatomy(project=project_name) hierarchy = "" parents = database[project_name].find_one({ diff --git a/pype/plugins/launcher/actions/ClockifyStart.py b/pype/plugins/launcher/actions/ClockifyStart.py index 78a8b4e1b6..9183805c7f 100644 --- a/pype/plugins/launcher/actions/ClockifyStart.py +++ b/pype/plugins/launcher/actions/ClockifyStart.py @@ -5,7 +5,7 @@ try: except Exception: pass -log = Logger.getLogger(__name__, "clockify_start") +log = Logger().get_logger(__name__, "clockify_start") class ClockifyStart(api.Action): diff --git a/pype/plugins/launcher/actions/ClockifySync.py b/pype/plugins/launcher/actions/ClockifySync.py index c50fbc4b25..0895da555d 100644 --- a/pype/plugins/launcher/actions/ClockifySync.py +++ b/pype/plugins/launcher/actions/ClockifySync.py @@ -4,7 +4,7 @@ try: except Exception: pass from pype.api import Logger -log = Logger.getLogger(__name__, "clockify_sync") +log = Logger().get_logger(__name__, "clockify_sync") class ClockifySync(api.Action):