diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000000..159ff0baeb --- /dev/null +++ b/changelog.md @@ -0,0 +1,54 @@ +# Pype changelog # +Welcome to pype changelog + +## 2.1 ## + +A large cleanup release. Most of the change are under the hood. + +**new**: +- _(pype)_ add customisable workflow for creating quicktimes from renders or playblasts +- _(pype)_ Added configurable option to add burnins to any generated quicktimes +- _(ftrack)_ Action that identifies what machines pype is running on. +- _(system)_ unify subprocess calls +- _(maya)_ add audio to review quicktimes +- _(nuke)_ add crop before write node to prevent overscan problems in ffmpeg +- **Nuke Studio** publishing and workfiles support +- **Muster** render manager support +- _(nuke)_ Framerange, FPS and Resolution are set automatically at startup +- _(maya)_ Ability to load published sequences as image planes +- _(system)_ Ftrack event that sets asset folder permissions based on task assignees in ftrack. +- _(maya)_ Pyblish plugin that allow validation of maya attributes +- _(system)_ added better startup logging to tray debug, including basic connection information +- _(avalon)_ option to group published subsets to groups in the loader +- _(avalon)_ loader family filters are working now + +**changed**: +- change multiple key attributes to unify their behaviour across the pipeline + - `frameRate` to `fps` + - `startFrame` to `frameStart` + - `endFrame` to `frameEnd` + - `fstart` to `frameStart` + - `fend` to `frameEnd` + - `handle_start` to `handleStart` + - `handle_end` to `handleEnd` + - `resolution_width` to `resolutionWidth` + - `resolution_height` to `resolutionHeight` + - `pixel_aspect` to `pixelAspect` + +- _(nuke)_ write nodes are now created inside group with only some attributes editable by the artist +- rendered frames are now deleted from temporary location after their publishing is finished. +- _(ftrack)_ RV action can now be launched from any entity +- after publishing only refresh button is now available in pyblish UI +- added context instance pyblish-lite so that artist knows if context plugin fails +- _(avalon)_ allow opening selected files using enter key +- _(avalon)_ core updated to v5.2.9 with our forked changes on top + +**fix**: +- faster hierarchy retrieval from db +- _(nuke)_ A lot of stability enhancements +- _(nuke studio)_ A lot of stability enhancements +- _(nuke)_ now only renders a single write node on farm +- _(ftrack)_ pype would crash when launcher project level task +- work directory was sometimes not being created correctly +- major pype.lib cleanup. Removing of unused functions, merging those that were doing the same and general house cleaning. +- _(avalon)_ subsets in maya 2019 weren't behaving correctly in the outliner diff --git a/pype/__init__.py b/pype/__init__.py index 35511eb6c1..a5858f49e7 100644 --- a/pype/__init__.py +++ b/pype/__init__.py @@ -7,6 +7,8 @@ from .lib import filter_pyblish_plugins import logging log = logging.getLogger(__name__) +__version__ = "2.1.0" + PACKAGE_DIR = os.path.dirname(__file__) PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") diff --git a/pype/clockify/__init__.py b/pype/clockify/__init__.py index 5f61acd751..063f88db73 100644 --- a/pype/clockify/__init__.py +++ b/pype/clockify/__init__.py @@ -7,3 +7,6 @@ __all__ = [ 'ClockifySettings', 'ClockifyModule' ] + +def tray_init(tray_widget, main_widget): + return ClockifyModule(main_widget, tray_widget) diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py index 0b84bf3953..1476c3d488 100644 --- a/pype/clockify/clockify.py +++ b/pype/clockify/clockify.py @@ -1,3 +1,4 @@ +import os import threading from pypeapp import style from Qt import QtWidgets @@ -20,9 +21,10 @@ class ClockifyModule: self.bool_workspace_set = False self.bool_timer_run = False - def start_up(self): self.clockapi.set_master(self) self.bool_api_key_set = self.clockapi.set_api() + + def tray_start(self): if self.bool_api_key_set is False: self.show_settings() return @@ -41,7 +43,7 @@ class ClockifyModule: os.path.dirname(__file__), 'ftrack_actions' ]) - current = os.environ('FTRACK_ACTIONS_PATH', '') + current = os.environ.get('FTRACK_ACTIONS_PATH', '') if current: current += os.pathsep os.environ['FTRACK_ACTIONS_PATH'] = current + actions_path @@ -57,6 +59,24 @@ class ClockifyModule: current += os.pathsep os.environ['AVALON_ACTIONS'] = current + actions_path + if 'TimersManager' in modules: + self.timer_manager = modules['TimersManager'] + self.timer_manager.add_module(self) + + def start_timer_manager(self, data): + self.start_timer(data) + + def stop_timer_manager(self): + self.stop_timer() + + def timer_started(self, data): + if hasattr(self, 'timer_manager'): + self.timer_manager.start_timers(data) + + def timer_stopped(self): + if hasattr(self, 'timer_manager'): + self.timer_manager.stop_timers() + def start_timer_check(self): self.bool_thread_check_running = True if self.thread_timer_check is None: @@ -75,21 +95,81 @@ class ClockifyModule: def check_running(self): import time while self.bool_thread_check_running is True: + bool_timer_run = False if self.clockapi.get_in_progress() is not None: - self.bool_timer_run = True - else: - self.bool_timer_run = False - self.set_menu_visibility() + bool_timer_run = True + + if self.bool_timer_run != bool_timer_run: + if self.bool_timer_run is True: + self.timer_stopped() + else: + actual_timer = self.clockapi.get_in_progress() + if not actual_timer: + continue + + actual_project_id = actual_timer["projectId"] + project = self.clockapi.get_project_by_id( + actual_project_id + ) + project_name = project["name"] + + actual_timer_hierarchy = actual_timer["description"] + hierarchy_items = actual_timer_hierarchy.split("/") + task_name = hierarchy_items[-1] + hierarchy = hierarchy_items[:-1] + + data = { + "task_name": task_name, + "hierarchy": hierarchy, + "project_name": project_name + } + + self.timer_started(data) + + self.bool_timer_run = bool_timer_run + self.set_menu_visibility() time.sleep(5) def stop_timer(self): self.clockapi.finish_time_entry() + if self.bool_timer_run: + self.timer_stopped() self.bool_timer_run = False + def start_timer(self, input_data): + actual_timer = self.clockapi.get_in_progress() + actual_timer_hierarchy = None + actual_project_id = None + if actual_timer is not None: + actual_timer_hierarchy = actual_timer.get("description") + actual_project_id = actual_timer.get("projectId") + + desc_items = [val for val in input_data.get("hierarchy", [])] + desc_items.append(input_data["task_name"]) + description = "/".join(desc_items) + + project_id = self.clockapi.get_project_id(input_data["project_name"]) + + if ( + actual_timer is not None and + description == actual_timer_hierarchy and + project_id == actual_project_id + ): + return + + tag_ids = [] + task_tag_id = self.clockapi.get_tag_id(input_data["task_name"]) + if task_tag_id is not None: + tag_ids.append(task_tag_id) + + self.clockapi.start_time_entry( + description, project_id, tag_ids=tag_ids + ) + # Definition of Tray menu - def tray_menu(self, parent): + def tray_menu(self, parent_menu): # Menu for Tray App - self.menu = QtWidgets.QMenu('Clockify', parent) + self.menu = QtWidgets.QMenu('Clockify', parent_menu) self.menu.setProperty('submenu', 'on') self.menu.setStyleSheet(style.load_stylesheet()) @@ -109,7 +189,7 @@ class ClockifyModule: self.set_menu_visibility() - return self.menu + parent_menu.addMenu(self.menu) def show_settings(self): self.widget_settings.input_api_key.setText(self.clockapi.get_api_key()) diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py index f5ebac0cef..ed932eedce 100644 --- a/pype/clockify/clockify_api.py +++ b/pype/clockify/clockify_api.py @@ -1,4 +1,5 @@ import os +import re import requests import json import datetime @@ -22,6 +23,7 @@ class ClockifyAPI(metaclass=Singleton): app_dir = os.path.normpath(appdirs.user_data_dir('pype-app', 'pype')) file_name = 'clockify.json' fpath = os.path.join(app_dir, file_name) + admin_permission_names = ['WORKSPACE_OWN', 'WORKSPACE_ADMIN'] master_parent = None workspace_id = None @@ -55,31 +57,41 @@ class ClockifyAPI(metaclass=Singleton): return False return True - def validate_workspace_perm(self): - test_project = '__test__' - action_url = 'workspaces/{}/projects/'.format(self.workspace_id) - body = { - "name": test_project, "clientId": "", "isPublic": "false", - "estimate": {"type": "AUTO"}, - "color": "#f44336", "billable": "true" - } - response = requests.post( - self.endpoint + action_url, - headers=self.headers, json=body + def validate_workspace_perm(self, workspace_id=None): + user_id = self.get_user_id() + if user_id is None: + return False + if workspace_id is None: + workspace_id = self.workspace_id + action_url = "/workspaces/{}/users/{}/permissions".format( + workspace_id, user_id ) - if response.status_code == 201: - self.delete_project(self.get_project_id(test_project)) - return True - else: - projects = self.get_projects() - if test_project in projects: - try: - self.delete_project(self.get_project_id(test_project)) - return True - except json.decoder.JSONDecodeError: - return False + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + user_permissions = response.json() + for perm in user_permissions: + if perm['name'] in self.admin_permission_names: + return True return False + def get_user_id(self): + action_url = 'v1/user/' + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + # this regex is neccessary: UNICODE strings are crashing + # during json serialization + id_regex ='\"{1}id\"{1}\:{1}\"{1}\w+\"{1}' + result = re.findall(id_regex, str(response.content)) + if len(result) != 1: + # replace with log and better message? + print('User ID was not found (this is a BUG!!!)') + return None + return json.loads('{'+result[0]+'}')['id'] + def set_workspace(self, name=None): if name is None: name = os.environ.get('CLOCKIFY_WORKSPACE', None) @@ -147,6 +159,19 @@ class ClockifyAPI(metaclass=Singleton): project["name"]: project["id"] for project in response.json() } + def get_project_by_id(self, project_id, workspace_id=None): + if workspace_id is None: + workspace_id = self.workspace_id + action_url = 'workspaces/{}/projects/{}/'.format( + workspace_id, project_id + ) + response = requests.get( + self.endpoint + action_url, + headers=self.headers + ) + + return response.json() + def get_tags(self, workspace_id=None): if workspace_id is None: workspace_id = self.workspace_id @@ -279,6 +304,9 @@ class ClockifyAPI(metaclass=Singleton): if workspace_id is None: workspace_id = self.workspace_id current = self.get_in_progress(workspace_id) + if current is None: + return + current_id = current["id"] action_url = 'workspaces/{}/timeEntries/{}'.format( workspace_id, current_id diff --git a/pype/clockify/ftrack_actions/action_clockify_start.py b/pype/clockify/ftrack_actions/action_clockify_start.py index e09d0b76e6..5b54476297 100644 --- a/pype/clockify/ftrack_actions/action_clockify_start.py +++ b/pype/clockify/ftrack_actions/action_clockify_start.py @@ -14,7 +14,7 @@ class StartClockify(BaseAction): #: Action identifier. identifier = 'clockify.start.timer' #: Action label. - label = 'Start timer' + label = 'Clockify - Start timer' #: Action description. description = 'Starts timer on clockify' #: roles that are allowed to register this action @@ -67,42 +67,3 @@ def register(session, **kw): return StartClockify(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:])) diff --git a/pype/ftrack/actions/action_application_loader.py b/pype/ftrack/actions/action_application_loader.py index 41bcc0c0b0..813eee358a 100644 --- a/pype/ftrack/actions/action_application_loader.py +++ b/pype/ftrack/actions/action_application_loader.py @@ -4,11 +4,12 @@ import time from pype.ftrack import AppAction from avalon import lib from pypeapp import Logger +from pype.lib import get_all_avalon_projects log = Logger().get_logger(__name__) -def registerApp(app, session): +def registerApp(app, session, plugins_presets): name = app['name'] variant = "" try: @@ -40,14 +41,18 @@ def registerApp(app, session): # register action AppAction( session, label, name, executable, variant, - icon, description, preactions + icon, description, preactions, plugins_presets ).register() if not variant: log.info('- Variant is not set') -def register(session): +def register(session, plugins_presets={}): + # WARNING getting projects only helps to check connection to mongo + # - without will `discover` of ftrack apps actions take ages + result = get_all_avalon_projects() + apps = [] launchers_path = os.path.join(os.environ["PYPE_CONFIG"], "launchers") @@ -66,7 +71,7 @@ def register(session): app_counter = 0 for app in apps: try: - registerApp(app, session) + registerApp(app, session, plugins_presets) if app_counter%5 == 0: time.sleep(0.1) app_counter += 1 diff --git a/pype/ftrack/actions/action_asset_delete.py b/pype/ftrack/actions/action_asset_delete.py index 684b3862a8..654c78049b 100644 --- a/pype/ftrack/actions/action_asset_delete.py +++ b/pype/ftrack/actions/action_asset_delete.py @@ -78,7 +78,7 @@ class AssetDelete(BaseAction): } -def register(session, **kw): +def register(session, plugins_presets={}): '''Register action. Called when used as an event plugin.''' # Validate that session is an instance of ftrack_api.Session. If not, @@ -87,7 +87,7 @@ def register(session, **kw): if not isinstance(session, ftrack_api.session.Session): return - AssetDelete(session).register() + AssetDelete(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_attributes_remapper.py b/pype/ftrack/actions/action_attributes_remapper.py index e1c32bca35..db33fd1365 100644 --- a/pype/ftrack/actions/action_attributes_remapper.py +++ b/pype/ftrack/actions/action_attributes_remapper.py @@ -11,13 +11,14 @@ class AttributesRemapper(BaseAction): #: Action identifier. identifier = 'attributes.remapper' #: Action label. - label = 'Attributes Remapper' + label = "Pype Doctor" + variant = '- Attributes Remapper' #: Action description. description = 'Remaps attributes in avalon DB' #: roles that are allowed to register this action role_list = ["Pypeclub", "Administrator"] - icon = '{}/ftrack/action_icons/AttributesRemapper.svg'.format( + icon = '{}/ftrack/action_icons/PypeDoctor.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) @@ -249,7 +250,9 @@ class AttributesRemapper(BaseAction): if interface_messages: self.show_interface_from_dict( - event, interface_messages, "Errors during remapping attributes" + messages=interface_messages, + title="Errors during remapping attributes", + event=event ) return True @@ -274,10 +277,10 @@ class AttributesRemapper(BaseAction): self.show_interface(event, items, title) -def register(session, **kw): +def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - AttributesRemapper(session).register() + AttributesRemapper(session, plugins_presets).register() diff --git a/pype/ftrack/actions/action_client_review_sort.py b/pype/ftrack/actions/action_client_review_sort.py index b06a928007..6a659ce5e3 100644 --- a/pype/ftrack/actions/action_client_review_sort.py +++ b/pype/ftrack/actions/action_client_review_sort.py @@ -53,12 +53,12 @@ class ClientReviewSort(BaseAction): } -def register(session, **kw): +def register(session, plugins_presets={}): '''Register action. Called when used as an event plugin.''' if not isinstance(session, ftrack_api.session.Session): return - action_handler = ClientReviewSort(session) + action_handler = ClientReviewSort(session, plugins_presets) action_handler.register() diff --git a/pype/ftrack/actions/action_component_open.py b/pype/ftrack/actions/action_component_open.py index d3213c555a..33f4d38890 100644 --- a/pype/ftrack/actions/action_component_open.py +++ b/pype/ftrack/actions/action_component_open.py @@ -65,7 +65,7 @@ class ComponentOpen(BaseAction): } -def register(session, **kw): +def register(session, plugins_presets={}): '''Register action. Called when used as an event plugin.''' # Validate that session is an instance of ftrack_api.Session. If not, @@ -74,7 +74,7 @@ def register(session, **kw): if not isinstance(session, ftrack_api.session.Session): return - ComponentOpen(session).register() + ComponentOpen(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_create_cust_attrs.py b/pype/ftrack/actions/action_create_cust_attrs.py index 2cd06cc479..47a6bb5d5f 100644 --- a/pype/ftrack/actions/action_create_cust_attrs.py +++ b/pype/ftrack/actions/action_create_cust_attrs.py @@ -110,12 +110,13 @@ class CustomAttributes(BaseAction): #: Action identifier. identifier = 'create.update.attributes' #: Action label. - label = 'Create/Update Avalon Attributes' + label = "Pype Admin" + variant = '- Create/Update Avalon Attributes' #: Action description. description = 'Creates Avalon/Mongo ID for double check' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = '{}/ftrack/action_icons/CustomAttributes.svg'.format( + icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) @@ -568,7 +569,7 @@ class CustomAttributes(BaseAction): } -def register(session, **kw): +def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' # Validate that session is an instance of ftrack_api.Session. If not, @@ -577,7 +578,7 @@ def register(session, **kw): if not isinstance(session, ftrack_api.session.Session): return - CustomAttributes(session).register() + CustomAttributes(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py index 2a777911b4..b9e10f7c30 100644 --- a/pype/ftrack/actions/action_create_folders.py +++ b/pype/ftrack/actions/action_create_folders.py @@ -30,11 +30,13 @@ class CreateFolders(BaseAction): def discover(self, session, entities, event): ''' Validation ''' - not_allowed = ['assetversion'] if len(entities) != 1: return False + + not_allowed = ['assetversion', 'project'] if entities[0].entity_type.lower() in not_allowed: return False + return True def interface(self, session, entities, event): @@ -322,13 +324,13 @@ class PartialDict(dict): return '{'+key+'}' -def register(session, **kw): +def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - CreateFolders(session).register() + CreateFolders(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_create_project_folders.py b/pype/ftrack/actions/action_create_project_structure.py similarity index 95% rename from pype/ftrack/actions/action_create_project_folders.py rename to pype/ftrack/actions/action_create_project_structure.py index 3ccdb08714..74d458b5f8 100644 --- a/pype/ftrack/actions/action_create_project_folders.py +++ b/pype/ftrack/actions/action_create_project_structure.py @@ -13,9 +13,9 @@ class CreateProjectFolders(BaseAction): '''Edit meta data action.''' #: Action identifier. - identifier = 'create.project.folders' + identifier = 'create.project.structure' #: Action label. - label = 'Create Project Folders' + label = 'Create Project Structure' #: Action description. description = 'Creates folder structure' #: roles that are allowed to register this action @@ -31,6 +31,11 @@ class CreateProjectFolders(BaseAction): def discover(self, session, entities, event): ''' Validation ''' + if len(entities) != 1: + return False + + if entities[0].entity_type.lower() != "project": + return False return True @@ -190,13 +195,13 @@ class CreateProjectFolders(BaseAction): -def register(session, **kw): +def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - CreateProjectFolders(session).register() + CreateProjectFolders(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_cust_attr_doctor.py b/pype/ftrack/actions/action_cust_attr_doctor.py index 9d0ce2071f..1b8f250e5b 100644 --- a/pype/ftrack/actions/action_cust_attr_doctor.py +++ b/pype/ftrack/actions/action_cust_attr_doctor.py @@ -12,14 +12,15 @@ class CustomAttributeDoctor(BaseAction): #: Action identifier. identifier = 'custom.attributes.doctor' #: Action label. - label = 'Custom Attributes Doctor' + label = "Pype Doctor" + variant = '- Custom Attributes Doctor' #: Action description. description = ( 'Fix hierarchical custom attributes mainly handles, fstart' ' and fend' ) - icon = '{}/ftrack/action_icons/TestAction.svg'.format( + icon = '{}/ftrack/action_icons/PypeDoctor.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) hierarchical_ca = ['handle_start', 'handle_end', 'fstart', 'fend'] @@ -286,13 +287,13 @@ class CustomAttributeDoctor(BaseAction): return all_roles -def register(session, **kw): +def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - CustomAttributeDoctor(session).register() + CustomAttributeDoctor(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_delete_asset.py b/pype/ftrack/actions/action_delete_asset.py index 96087f4c8e..a408de45b2 100644 --- a/pype/ftrack/actions/action_delete_asset.py +++ b/pype/ftrack/actions/action_delete_asset.py @@ -311,7 +311,7 @@ class DeleteAsset(BaseAction): return assets -def register(session, **kw): +def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' # Validate that session is an instance of ftrack_api.Session. If not, @@ -320,7 +320,7 @@ def register(session, **kw): if not isinstance(session, ftrack_api.session.Session): return - DeleteAsset(session).register() + DeleteAsset(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_delete_asset_byname.py b/pype/ftrack/actions/action_delete_asset_byname.py index fa966096a8..4f2a0e515c 100644 --- a/pype/ftrack/actions/action_delete_asset_byname.py +++ b/pype/ftrack/actions/action_delete_asset_byname.py @@ -13,12 +13,13 @@ class AssetsRemover(BaseAction): #: Action identifier. identifier = 'remove.assets' #: Action label. - label = 'Delete Assets by Name' + label = "Pype Admin" + variant = '- Delete Assets by Name' #: Action description. description = 'Removes assets from Ftrack and Avalon db with all childs' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = '{}/ftrack/action_icons/AssetsRemover.svg'.format( + icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) #: Db @@ -131,7 +132,7 @@ class AssetsRemover(BaseAction): return assets -def register(session, **kw): +def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' # Validate that session is an instance of ftrack_api.Session. If not, @@ -140,7 +141,7 @@ def register(session, **kw): if not isinstance(session, ftrack_api.session.Session): return - AssetsRemover(session).register() + AssetsRemover(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_delete_unpublished.py b/pype/ftrack/actions/action_delete_unpublished.py index 377e118ffb..5e7f783ba7 100644 --- a/pype/ftrack/actions/action_delete_unpublished.py +++ b/pype/ftrack/actions/action_delete_unpublished.py @@ -42,7 +42,7 @@ class VersionsCleanup(BaseAction): } -def register(session, **kw): +def register(session, plugins_presets={}): '''Register action. Called when used as an event plugin.''' # Validate that session is an instance of ftrack_api.Session. If not, @@ -51,7 +51,7 @@ def register(session, **kw): if not isinstance(session, ftrack_api.session.Session): return - VersionsCleanup(session).register() + VersionsCleanup(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index e0c0334e5f..58914fbc1e 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -21,9 +21,9 @@ class DJVViewAction(BaseAction): ) type = 'Application' - def __init__(self, session): + def __init__(self, session, plugins_presets): '''Expects a ftrack_api.Session instance''' - super().__init__(session) + super().__init__(session, plugins_presets) self.djv_path = None self.config_data = config.get_presets()['djv_view']['config'] @@ -218,12 +218,12 @@ class DJVViewAction(BaseAction): return True -def register(session): +def register(session, plugins_presets={}): """Register hooks.""" if not isinstance(session, ftrack_api.session.Session): return - DJVViewAction(session).register() + DJVViewAction(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_job_killer.py b/pype/ftrack/actions/action_job_killer.py index 44acb24d55..8584b26aa4 100644 --- a/pype/ftrack/actions/action_job_killer.py +++ b/pype/ftrack/actions/action_job_killer.py @@ -14,12 +14,13 @@ class JobKiller(BaseAction): #: Action identifier. identifier = 'job.killer' #: Action label. - label = 'Job Killer' + label = "Pype Admin" + variant = '- Job Killer' #: Action description. description = 'Killing selected running jobs' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = '{}/ftrack/action_icons/JobKiller.svg'.format( + icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) @@ -117,7 +118,7 @@ class JobKiller(BaseAction): } -def register(session, **kw): +def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' # Validate that session is an instance of ftrack_api.Session. If not, @@ -126,7 +127,7 @@ def register(session, **kw): if not isinstance(session, ftrack_api.session.Session): return - JobKiller(session).register() + JobKiller(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_multiple_notes.py b/pype/ftrack/actions/action_multiple_notes.py index 338083fe47..6e28b7bed6 100644 --- a/pype/ftrack/actions/action_multiple_notes.py +++ b/pype/ftrack/actions/action_multiple_notes.py @@ -112,13 +112,13 @@ class MultipleNotes(BaseAction): return True -def register(session, **kw): +def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - MultipleNotes(session).register() + MultipleNotes(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_prepare_project.py b/pype/ftrack/actions/action_prepare_project.py new file mode 100644 index 0000000000..60a7435907 --- /dev/null +++ b/pype/ftrack/actions/action_prepare_project.py @@ -0,0 +1,240 @@ +import os +import json + +from pype.vendor import ftrack_api +from pype.ftrack import BaseAction +from pypeapp import config +from pype.ftrack.lib import get_avalon_attr + + +class PrepareProject(BaseAction): + '''Edit meta data action.''' + + #: Action identifier. + identifier = 'prepare.project' + #: Action label. + label = 'Prepare Project' + #: Action description. + description = 'Set basic attributes on the project' + #: roles that are allowed to register this action + role_list = ["Pypeclub", "Administrator", "Project manager"] + icon = '{}/ftrack/action_icons/PrepareProject.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') + ) + + def discover(self, session, entities, event): + ''' Validation ''' + if len(entities) != 1: + return False + + if entities[0].entity_type.lower() != "project": + return False + + return True + + def interface(self, session, entities, event): + if event['data'].get('values', {}): + return + + # Inform user that this may take a while + self.show_message(event, "Preparing data... Please wait", True) + + self.log.debug("Loading custom attributes") + cust_attrs, hier_cust_attrs = get_avalon_attr(session, True) + project_defaults = config.get_presets().get("ftrack", {}).get( + "project_defaults", {} + ) + + self.log.debug("Preparing data which will be shown") + attributes_to_set = {} + for attr in hier_cust_attrs: + key = attr["key"] + attributes_to_set[key] = { + "label": attr["label"], + "object": attr, + "default": project_defaults.get(key) + } + + for attr in cust_attrs: + if attr["entity_type"].lower() != "show": + continue + key = attr["key"] + attributes_to_set[key] = { + "label": attr["label"], + "object": attr, + "default": project_defaults.get(key) + } + + # Sort by label + attributes_to_set = dict(sorted( + attributes_to_set.items(), + key=lambda x: x[1]["label"] + )) + self.log.debug("Preparing interface for keys: \"{}\"".format( + str([key for key in attributes_to_set]) + )) + + title = "Set Attribute values" + items = [] + multiselect_enumerators = [] + + # This item will be last (before enumerators) + # - sets value of auto synchronization + auto_sync_name = "avalon_auto_sync" + auto_sync_item = { + "name": auto_sync_name, + "type": "boolean", + "value": project_defaults.get(auto_sync_name, False), + "label": "AutoSync to Avalon" + } + + item_splitter = {'type': 'label', 'value': '---'} + + for key, in_data in attributes_to_set.items(): + attr = in_data["object"] + + # initial item definition + item = { + "name": key, + "label": in_data["label"] + } + + # cust attr type - may have different visualization + type_name = attr["type"]["name"].lower() + easy_types = ["text", "boolean", "date", "number"] + + easy_type = False + if type_name in easy_types: + easy_type = True + + elif type_name == "enumerator": + + attr_config = json.loads(attr["config"]) + attr_config_data = json.loads(attr_config["data"]) + + if attr_config["multiSelect"] is True: + multiselect_enumerators.append(item_splitter) + + multiselect_enumerators.append({ + "type": "label", + "value": in_data["label"] + }) + + default = in_data["default"] + names = [] + for option in sorted( + attr_config_data, key=lambda x: x["menu"] + ): + name = option["value"] + new_name = "__{}__{}".format(key, name) + names.append(new_name) + item = { + "name": new_name, + "type": "boolean", + "label": "- {}".format(option["menu"]) + } + if default: + if ( + isinstance(default, list) or + isinstance(default, tuple) + ): + if name in default: + item["value"] = True + else: + if name == default: + item["value"] = True + + multiselect_enumerators.append(item) + + multiselect_enumerators.append({ + "type": "hidden", + "name": "__hidden__{}".format(key), + "value": json.dumps(names) + }) + else: + easy_type = True + item["data"] = attr_config_data + + else: + self.log.warning(( + "Custom attribute \"{}\" has type \"{}\"." + " I don't know how to handle" + ).format(key, type_name)) + items.append({ + "type": "label", + "value": ( + "!!! Can't handle Custom attritubte type \"{}\"" + " (key: \"{}\")" + ).format(type_name, key) + }) + + if easy_type: + item["type"] = type_name + + # default value in interface + default = in_data["default"] + if default is not None: + item["value"] = default + + items.append(item) + + # Add autosync attribute + items.append(auto_sync_item) + + # Add enumerator items at the end + for item in multiselect_enumerators: + items.append(item) + + return { + 'items': items, + 'title': title + } + + def launch(self, session, entities, event): + if not event['data'].get('values', {}): + return + + in_data = event['data']['values'] + # Find hidden items for multiselect enumerators + keys_to_process = [] + for key in in_data: + if key.startswith("__hidden__"): + keys_to_process.append(key) + + self.log.debug("Preparing data for Multiselect Enumerators") + enumerators = {} + for key in keys_to_process: + new_key = key.replace("__hidden__", "") + enumerator_items = in_data.pop(key) + enumerators[new_key] = json.loads(enumerator_items) + + # find values set for multiselect enumerator + for key, enumerator_items in enumerators.items(): + in_data[key] = [] + + name = "__{}__".format(key) + + for item in enumerator_items: + value = in_data.pop(item) + if value is True: + new_key = item.replace(name, "") + in_data[key].append(new_key) + + self.log.debug("Setting Custom Attribute values:") + entity = entities[0] + for key, value in in_data.items(): + entity["custom_attributes"][key] = value + self.log.debug("- Key \"{}\" set to \"{}\"".format(key, value)) + + session.commit() + + return True + + +def register(session, plugins_presets={}): + '''Register plugin. Called when used as an plugin.''' + + if not isinstance(session, ftrack_api.session.Session): + return + + PrepareProject(session, plugins_presets).register() diff --git a/pype/ftrack/actions/action_rv.py b/pype/ftrack/actions/action_rv.py index 223fd0a94b..6b6591355f 100644 --- a/pype/ftrack/actions/action_rv.py +++ b/pype/ftrack/actions/action_rv.py @@ -23,13 +23,13 @@ class RVAction(BaseAction): ) type = 'Application' - def __init__(self, session): + def __init__(self, session, plugins_presets): """ Constructor :param session: ftrack Session :type session: :class:`ftrack_api.Session` """ - super().__init__(session) + super().__init__(session, plugins_presets) self.rv_path = None self.config_data = None @@ -326,12 +326,12 @@ class RVAction(BaseAction): return paths -def register(session): +def register(session, plugins_presets={}): """Register hooks.""" if not isinstance(session, ftrack_api.session.Session): return - RVAction(session).register() + RVAction(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_set_version.py b/pype/ftrack/actions/action_set_version.py index f6e745b3ec..5bf965e3ef 100644 --- a/pype/ftrack/actions/action_set_version.py +++ b/pype/ftrack/actions/action_set_version.py @@ -71,7 +71,7 @@ class SetVersion(BaseAction): } -def register(session, **kw): +def register(session, plugins_presets={}): '''Register action. Called when used as an event plugin.''' # Validate that session is an instance of ftrack_api.Session. If not, @@ -80,7 +80,7 @@ def register(session, **kw): if not isinstance(session, ftrack_api.session.Session): return - SetVersion(session).register() + SetVersion(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_start_timer.py b/pype/ftrack/actions/action_start_timer.py index d27908541e..ad83edfc9e 100644 --- a/pype/ftrack/actions/action_start_timer.py +++ b/pype/ftrack/actions/action_start_timer.py @@ -70,10 +70,10 @@ class StartTimer(BaseAction): self.log.info('Starting Clockify timer for task: ' + task['name']) -def register(session, **kw): +def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - StartTimer(session).register() + StartTimer(session, plugins_presets).register() diff --git a/pype/ftrack/actions/action_sync_hier_attrs_local.py b/pype/ftrack/actions/action_sync_hier_attrs_local.py index df95159a2c..01434470f3 100644 --- a/pype/ftrack/actions/action_sync_hier_attrs_local.py +++ b/pype/ftrack/actions/action_sync_hier_attrs_local.py @@ -19,11 +19,12 @@ class SyncHierarchicalAttrs(BaseAction): #: Action identifier. identifier = 'sync.hierarchical.attrs.local' #: Action label. - label = 'Sync HierAttrs - Local' + label = "Pype Admin" + variant = '- Sync Hier Attrs (Local)' #: Action description. description = 'Synchronize hierarchical attributes' #: Icon - icon = '{}/ftrack/action_icons/SyncHierarchicalAttrsLocal.svg'.format( + icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) @@ -186,7 +187,10 @@ class SyncHierarchicalAttrs(BaseAction): job['status'] = 'failed' session.commit() if self.interface_messages: - self.show_interface_from_dict(self.interface_messages, event) + title = "Errors during SyncHierarchicalAttrs" + self.show_interface_from_dict( + messages=self.interface_messages, title=title, event=event + ) return True @@ -302,13 +306,13 @@ class SyncHierarchicalAttrs(BaseAction): self.update_hierarchical_attribute(child, key, value) -def register(session, **kw): +def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - SyncHierarchicalAttrs(session).register() + SyncHierarchicalAttrs(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_sync_to_avalon_local.py b/pype/ftrack/actions/action_sync_to_avalon_local.py index 34070c7e1f..ad39b0ca12 100644 --- a/pype/ftrack/actions/action_sync_to_avalon_local.py +++ b/pype/ftrack/actions/action_sync_to_avalon_local.py @@ -47,11 +47,12 @@ class SyncToAvalon(BaseAction): #: Action identifier. identifier = 'sync.to.avalon.local' #: Action label. - label = 'SyncToAvalon - Local' + label = "Pype Admin" + variant = '- Sync To Avalon (Local)' #: Action description. description = 'Send data from Ftrack to Avalon' #: Action icon. - icon = '{}/ftrack/action_icons/SyncToAvalon-local.svg'.format( + icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) #: roles that are allowed to register this action @@ -59,7 +60,7 @@ class SyncToAvalon(BaseAction): #: Action priority priority = 200 - def __init__(self, session): + def __init__(self, session, plugins_presets): super(SyncToAvalon, self).__init__(session) # reload utils on initialize (in case of server restart) @@ -212,7 +213,7 @@ class SyncToAvalon(BaseAction): self.add_childs_to_importable(child) -def register(session, **kw): +def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' # Validate that session is an instance of ftrack_api.Session. If not, @@ -221,7 +222,7 @@ def register(session, **kw): if not isinstance(session, ftrack_api.session.Session): return - SyncToAvalon(session).register() + SyncToAvalon(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_test.py b/pype/ftrack/actions/action_test.py index b7cfd4934a..a2bc8bf892 100644 --- a/pype/ftrack/actions/action_test.py +++ b/pype/ftrack/actions/action_test.py @@ -40,13 +40,13 @@ class TestAction(BaseAction): return True -def register(session, **kw): +def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - TestAction(session).register() + TestAction(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_thumbToChildern.py b/pype/ftrack/actions/action_thumbnail_to_childern.py similarity index 92% rename from pype/ftrack/actions/action_thumbToChildern.py rename to pype/ftrack/actions/action_thumbnail_to_childern.py index 4e7f1298f5..101b678512 100644 --- a/pype/ftrack/actions/action_thumbToChildern.py +++ b/pype/ftrack/actions/action_thumbnail_to_childern.py @@ -14,9 +14,11 @@ class ThumbToChildren(BaseAction): # Action identifier identifier = 'thumb.to.children' # Action label - label = 'Thumbnail to Children' + label = 'Thumbnail' + # Action variant + variant = " to Children" # Action icon - icon = '{}/ftrack/action_icons/thumbToChildren.svg'.format( + icon = '{}/ftrack/action_icons/Thumbnail.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) @@ -64,12 +66,12 @@ class ThumbToChildren(BaseAction): } -def register(session, **kw): +def register(session, plugins_presets={}): '''Register action. Called when used as an event plugin.''' if not isinstance(session, ftrack_api.session.Session): return - ThumbToChildren(session).register() + ThumbToChildren(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_thumbToParent.py b/pype/ftrack/actions/action_thumbnail_to_parent.py similarity index 94% rename from pype/ftrack/actions/action_thumbToParent.py rename to pype/ftrack/actions/action_thumbnail_to_parent.py index 632d2a50b2..c382d9303c 100644 --- a/pype/ftrack/actions/action_thumbToParent.py +++ b/pype/ftrack/actions/action_thumbnail_to_parent.py @@ -13,9 +13,11 @@ class ThumbToParent(BaseAction): # Action identifier identifier = 'thumb.to.parent' # Action label - label = 'Thumbnail to Parent' + label = 'Thumbnail' + # Action variant + variant = " to Parent" # Action icon - icon = '{}/ftrack/action_icons/thumbToParent.svg'.format( + icon = '{}/ftrack/action_icons/Thumbnail.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) @@ -86,12 +88,12 @@ class ThumbToParent(BaseAction): } -def register(session, **kw): +def register(session, plugins_presets={}): '''Register action. Called when used as an event plugin.''' if not isinstance(session, ftrack_api.session.Session): return - ThumbToParent(session).register() + ThumbToParent(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/actions/action_where_run_ask.py b/pype/ftrack/actions/action_where_run_ask.py index 2bd1ed3d22..95bbf1fdd7 100644 --- a/pype/ftrack/actions/action_where_run_ask.py +++ b/pype/ftrack/actions/action_where_run_ask.py @@ -45,10 +45,10 @@ class ActionAskWhereIRun(BaseAction): return True -def register(session, **kw): +def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - ActionAskWhereIRun(session).register() + ActionAskWhereIRun(session, plugins_presets).register() diff --git a/pype/ftrack/actions/action_where_run_show.py b/pype/ftrack/actions/action_where_run_show.py index 7875d2e262..7fea23e3b7 100644 --- a/pype/ftrack/actions/action_where_run_show.py +++ b/pype/ftrack/actions/action_where_run_show.py @@ -77,10 +77,10 @@ class ActionShowWhereIRun(BaseAction): return True -def register(session, **kw): +def register(session, plugins_presets={}): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - ActionShowWhereIRun(session).register() + ActionShowWhereIRun(session, plugins_presets).register() diff --git a/pype/ftrack/events/action_sync_hier_attrs.py b/pype/ftrack/events/action_sync_hier_attrs.py index a94d43cad2..22ad7bf5aa 100644 --- a/pype/ftrack/events/action_sync_hier_attrs.py +++ b/pype/ftrack/events/action_sync_hier_attrs.py @@ -20,11 +20,12 @@ class SyncHierarchicalAttrs(BaseAction): #: Action identifier. identifier = 'sync.hierarchical.attrs' #: Action label. - label = 'Sync HierAttrs' + label = "Pype Admin" + variant = '- Sync Hier Attrs (server)' #: Action description. description = 'Synchronize hierarchical attributes' #: Icon - icon = '{}/ftrack/action_icons/SyncHierarchicalAttrs.svg'.format( + icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get( 'PYPE_STATICS_SERVER', 'http://localhost:{}'.format( @@ -333,13 +334,13 @@ class SyncHierarchicalAttrs(BaseAction): self.update_hierarchical_attribute(child, key, value) -def register(session, **kw): +def register(session, plugins_presets): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - SyncHierarchicalAttrs(session).register() + SyncHierarchicalAttrs(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/events/action_sync_to_avalon.py b/pype/ftrack/events/action_sync_to_avalon.py index e78b209fac..5628554c85 100644 --- a/pype/ftrack/events/action_sync_to_avalon.py +++ b/pype/ftrack/events/action_sync_to_avalon.py @@ -48,11 +48,12 @@ class Sync_To_Avalon(BaseAction): #: Action identifier. identifier = 'sync.to.avalon' #: Action label. - label = 'SyncToAvalon' + label = "Pype Admin" + variant = "- Sync To Avalon (Server)" #: Action description. description = 'Send data from Ftrack to Avalon' #: Action icon. - icon = '{}/ftrack/action_icons/SyncToAvalon.svg'.format( + icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get( 'PYPE_STATICS_SERVER', 'http://localhost:{}'.format( @@ -242,7 +243,7 @@ class Sync_To_Avalon(BaseAction): self.add_childs_to_importable(child) -def register(session, **kw): +def register(session, plugins_presets): '''Register plugin. Called when used as an plugin.''' # Validate that session is an instance of ftrack_api.Session. If not, @@ -251,7 +252,7 @@ def register(session, **kw): if not isinstance(session, ftrack_api.session.Session): return - Sync_To_Avalon(session).register() + SyncToAvalon(session, plugins_presets).register() def main(arguments=None): diff --git a/pype/ftrack/events/event_del_avalon_id_from_new.py b/pype/ftrack/events/event_del_avalon_id_from_new.py index f27a329429..6f6320f51b 100644 --- a/pype/ftrack/events/event_del_avalon_id_from_new.py +++ b/pype/ftrack/events/event_del_avalon_id_from_new.py @@ -51,9 +51,9 @@ class DelAvalonIdFromNew(BaseEvent): continue -def register(session, **kw): +def register(session, plugins_presets): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - DelAvalonIdFromNew(session).register() + DelAvalonIdFromNew(session, plugins_presets).register() diff --git a/pype/ftrack/events/event_next_task_update.py b/pype/ftrack/events/event_next_task_update.py index 1ae06050bc..e25514a2b4 100644 --- a/pype/ftrack/events/event_next_task_update.py +++ b/pype/ftrack/events/event_next_task_update.py @@ -86,9 +86,9 @@ class NextTaskUpdate(BaseEvent): session.rollback() -def register(session, **kw): +def register(session, plugins_presets): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - NextTaskUpdate(session).register() + NextTaskUpdate(session, plugins_presets).register() diff --git a/pype/ftrack/events/event_radio_buttons.py b/pype/ftrack/events/event_radio_buttons.py index a185280ecc..9c6f2d490a 100644 --- a/pype/ftrack/events/event_radio_buttons.py +++ b/pype/ftrack/events/event_radio_buttons.py @@ -34,9 +34,9 @@ class Radio_buttons(BaseEvent): session.commit() -def register(session): +def register(session, plugins_presets): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - Radio_buttons(session).register() + Radio_buttons(session, plugins_presets).register() diff --git a/pype/ftrack/events/event_sync_hier_attr.py b/pype/ftrack/events/event_sync_hier_attr.py index 867e2cde2b..7c5c4b820b 100644 --- a/pype/ftrack/events/event_sync_hier_attr.py +++ b/pype/ftrack/events/event_sync_hier_attr.py @@ -115,9 +115,9 @@ class SyncHierarchicalAttrs(BaseEvent): self.update_hierarchical_attribute(child, key, value) -def register(session, **kw): +def register(session, plugins_presets): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - SyncHierarchicalAttrs(session).register() + SyncHierarchicalAttrs(session, plugins_presets).register() diff --git a/pype/ftrack/events/event_sync_to_avalon.py b/pype/ftrack/events/event_sync_to_avalon.py index dbd58111b3..ae7ebbbf90 100644 --- a/pype/ftrack/events/event_sync_to_avalon.py +++ b/pype/ftrack/events/event_sync_to_avalon.py @@ -118,10 +118,10 @@ class Sync_to_Avalon(BaseEvent): return -def register(session, **kw): +def register(session, plugins_presets): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - Sync_to_Avalon(session).register() + Sync_to_Avalon(session, plugins_presets).register() diff --git a/pype/ftrack/events/event_test.py b/pype/ftrack/events/event_test.py index cc7851afb7..94d99dbf67 100644 --- a/pype/ftrack/events/event_test.py +++ b/pype/ftrack/events/event_test.py @@ -20,9 +20,9 @@ class Test_Event(BaseEvent): return True -def register(session, **kw): +def register(session, plugins_presets): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - Test_Event(session).register() + Test_Event(session, plugins_presets).register() diff --git a/pype/ftrack/events/event_thumbnail_updates.py b/pype/ftrack/events/event_thumbnail_updates.py index 042f6cc600..7f52177161 100644 --- a/pype/ftrack/events/event_thumbnail_updates.py +++ b/pype/ftrack/events/event_thumbnail_updates.py @@ -45,9 +45,9 @@ class ThumbnailEvents(BaseEvent): pass -def register(session, **kw): +def register(session, plugins_presets): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - ThumbnailEvents(session).register() + ThumbnailEvents(session, plugins_presets).register() diff --git a/pype/ftrack/events/event_user_assigment.py b/pype/ftrack/events/event_user_assigment.py index 0bb7f21590..3e250b988a 100644 --- a/pype/ftrack/events/event_user_assigment.py +++ b/pype/ftrack/events/event_user_assigment.py @@ -229,11 +229,11 @@ class UserAssigmentEvent(BaseEvent): return True -def register(session, **kw): +def register(session, plugins_presets): """ Register plugin. Called when used as an plugin. """ if not isinstance(session, ftrack_api.session.Session): return - UserAssigmentEvent(session).register() + UserAssigmentEvent(session, plugins_presets).register() diff --git a/pype/ftrack/events/event_version_to_task_statuses.py b/pype/ftrack/events/event_version_to_task_statuses.py index 8b14e025d3..306d594647 100644 --- a/pype/ftrack/events/event_version_to_task_statuses.py +++ b/pype/ftrack/events/event_version_to_task_statuses.py @@ -69,9 +69,9 @@ class VersionToTaskStatus(BaseEvent): path, task_status['name'])) -def register(session, **kw): +def register(session, plugins_presets): '''Register plugin. Called when used as an plugin.''' if not isinstance(session, ftrack_api.session.Session): return - VersionToTaskStatus(session).register() + VersionToTaskStatus(session, plugins_presets).register() diff --git a/pype/ftrack/ftrack_server/ftrack_server.py b/pype/ftrack/ftrack_server/ftrack_server.py index 81fd6eee63..2a58c12d09 100644 --- a/pype/ftrack/ftrack_server/ftrack_server.py +++ b/pype/ftrack/ftrack_server/ftrack_server.py @@ -5,7 +5,9 @@ import importlib from pype.vendor import ftrack_api import time import logging -from pypeapp import Logger +import inspect +from pypeapp import Logger, config + log = Logger().get_logger(__name__) @@ -27,8 +29,8 @@ PYTHONPATH # Path to ftrack_api and paths to all modules used in actions """ -class FtrackServer(): - def __init__(self, type='action'): +class FtrackServer: + def __init__(self, server_type='action'): """ - 'type' is by default set to 'action' - Runs Action server - enter 'event' for Event server @@ -43,21 +45,12 @@ class FtrackServer(): ftrack_log = logging.getLogger("ftrack_api") ftrack_log.setLevel(logging.WARNING) - self.type = type - self.actionsAvailable = True - self.eventsAvailable = True - # Separate all paths - if "FTRACK_ACTIONS_PATH" in os.environ: - all_action_paths = os.environ["FTRACK_ACTIONS_PATH"] - self.actionsPaths = all_action_paths.split(os.pathsep) - else: - self.actionsAvailable = False + env_key = "FTRACK_ACTIONS_PATH" + if server_type.lower() == 'event': + env_key = "FTRACK_EVENTS_PATH" - if "FTRACK_EVENTS_PATH" in os.environ: - all_event_paths = os.environ["FTRACK_EVENTS_PATH"] - self.eventsPaths = all_event_paths.split(os.pathsep) - else: - self.eventsAvailable = False + self.server_type = server_type + self.env_key = env_key def stop_session(self): if self.session.event_hub.connected is True: @@ -67,7 +60,7 @@ class FtrackServer(): def set_files(self, paths): # Iterate all paths - functions = [] + register_functions_dict = [] for path in paths: # add path to PYTHON PATH if path not in sys.path: @@ -92,13 +85,11 @@ class FtrackServer(): # separate files by register function if 'register' not in mod_functions: - msg = ( - '"{0}" - Missing register method' - ).format(file, self.type) + msg = ('"{}" - Missing register method').format(file) log.warning(msg) continue - functions.append({ + register_functions_dict.append({ 'name': file, 'register': mod_functions['register'] }) @@ -108,43 +99,47 @@ class FtrackServer(): ) log.warning(msg) - if len(functions) < 1: + if len(register_functions_dict) < 1: raise Exception + # Load presets for setting plugins + key = "user" + if self.server_type.lower() == "event": + key = "server" + plugins_presets = config.get_presets().get( + "ftrack", {} + ).get("plugins", {}).get(key, {}) + function_counter = 0 - for function in functions: + for function_dict in register_functions_dict: + register = function_dict["register"] try: - function['register'](self.session) + if len(inspect.signature(register).parameters) == 1: + register(self.session) + else: + register(self.session, plugins_presets=plugins_presets) + if function_counter%7 == 0: time.sleep(0.1) function_counter += 1 - except Exception as e: + except Exception as exc: msg = '"{}" - register was not successful ({})'.format( - function['name'], str(e) + function_dict['name'], str(exc) ) log.warning(msg) def run_server(self): self.session = ftrack_api.Session(auto_connect_event_hub=True,) - if self.type.lower() == 'event': - if self.eventsAvailable is False: - msg = ( - 'FTRACK_EVENTS_PATH is not set' - ', event server won\'t launch' - ) - log.error(msg) - return - self.set_files(self.eventsPaths) - else: - if self.actionsAvailable is False: - msg = ( - 'FTRACK_ACTIONS_PATH is not set' - ', action server won\'t launch' - ) - log.error(msg) - return - self.set_files(self.actionsPaths) + paths_str = os.environ.get(self.env_key) + if paths_str is None: + log.error(( + "Env var \"{}\" is not set, \"{}\" server won\'t launch" + ).format(self.env_key, self.server_type)) + return + + paths = paths_str.split(os.pathsep) + self.set_files(paths) log.info(60*"*") log.info('Registration of actions/events has finished!') diff --git a/pype/ftrack/lib/avalon_sync.py b/pype/ftrack/lib/avalon_sync.py index b677529ec3..169bc4b051 100644 --- a/pype/ftrack/lib/avalon_sync.py +++ b/pype/ftrack/lib/avalon_sync.py @@ -116,13 +116,13 @@ def import_to_avalon( # not override existing templates! templates = av_project['config'].get('template', None) if templates is not None: - for key, value in config['template'].items(): + for key, value in proj_config['template'].items(): if ( key in templates and templates[key] is not None and templates[key] != value ): - config['template'][key] = templates[key] + proj_config['template'][key] = templates[key] projectId = av_project['_id'] @@ -142,7 +142,7 @@ def import_to_avalon( {'_id': ObjectId(projectId)}, {'$set': { 'name': project_name, - 'config': config, + 'config': proj_config, 'data': data }} ) @@ -326,13 +326,26 @@ def import_to_avalon( return output -def get_avalon_attr(session): +def get_avalon_attr(session, split_hierarchical=False): custom_attributes = [] + hier_custom_attributes = [] query = 'CustomAttributeGroup where name is "avalon"' all_avalon_attr = session.query(query).one() for cust_attr in all_avalon_attr['custom_attribute_configurations']: - if 'avalon_' not in cust_attr['key']: - custom_attributes.append(cust_attr) + if 'avalon_' in cust_attr['key']: + continue + + if split_hierarchical: + if cust_attr["is_hierarchical"]: + hier_custom_attributes.append(cust_attr) + continue + + custom_attributes.append(cust_attr) + + if split_hierarchical: + # return tuple + return custom_attributes, hier_custom_attributes + return custom_attributes diff --git a/pype/ftrack/lib/ftrack_action_handler.py b/pype/ftrack/lib/ftrack_action_handler.py index 7a25155718..7fd7eccfb7 100644 --- a/pype/ftrack/lib/ftrack_action_handler.py +++ b/pype/ftrack/lib/ftrack_action_handler.py @@ -21,9 +21,9 @@ class BaseAction(BaseHandler): icon = None type = 'Action' - def __init__(self, session): + def __init__(self, session, plugins_presets={}): '''Expects a ftrack_api.Session instance''' - super().__init__(session) + super().__init__(session, plugins_presets) if self.label is None: raise ValueError( diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 7389d1c2c3..dbb38a3247 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -26,10 +26,10 @@ class AppAction(BaseHandler): preactions = ['start.timer'] def __init__( - self, session, label, name, executable, - variant=None, icon=None, description=None, preactions=[] + self, session, label, name, executable, variant=None, + icon=None, description=None, preactions=[], plugins_presets={} ): - super().__init__(session) + super().__init__(session, plugins_presets) '''Expects a ftrack_api.Session instance''' if label is None: diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index 1bb3eb7a8f..c6aa176363 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -29,7 +29,7 @@ class BaseHandler(object): ignore_me = False preactions = [] - def __init__(self, session): + def __init__(self, session, plugins_presets={}): '''Expects a ftrack_api.Session instance''' self._session = session self.log = Logger().get_logger(self.__class__.__name__) @@ -37,13 +37,23 @@ class BaseHandler(object): # Using decorator self.register = self.register_decorator(self.register) self.launch = self.launch_log(self.launch) + self.plugins_presets = plugins_presets # Decorator def register_decorator(self, func): @functools.wraps(func) def wrapper_register(*args, **kwargs): + + presets_data = self.plugins_presets.get(self.__class__.__name__) + if presets_data: + for key, value in presets_data.items(): + if not hasattr(self, key): + continue + setattr(self, key, value) + if self.ignore_me: return + label = self.__class__.__name__ if hasattr(self, 'label'): if self.variant is None: @@ -495,13 +505,12 @@ class BaseHandler(object): ) def show_interface_from_dict( - self, messages, event=None, user=None, username=None, user_id=None + self, messages, title="", event=None, user=None, username=None, user_id=None ): if not messages: self.log.debug("No messages to show! (messages dict is empty)") return items = [] - title = 'Errors during mirroring' splitter = {'type': 'label', 'value': '---'} first = True for key, value in messages.items(): diff --git a/pype/ftrack/lib/ftrack_event_handler.py b/pype/ftrack/lib/ftrack_event_handler.py index b2f47c3fbb..db55eef16e 100644 --- a/pype/ftrack/lib/ftrack_event_handler.py +++ b/pype/ftrack/lib/ftrack_event_handler.py @@ -15,9 +15,9 @@ class BaseEvent(BaseHandler): type = 'Event' - def __init__(self, session): + def __init__(self, session, plugins_presets={}): '''Expects a ftrack_api.Session instance''' - super().__init__(session) + super().__init__(session, plugins_presets) # Decorator def launch_log(self, func): diff --git a/pype/ftrack/tray/ftrack_module.py b/pype/ftrack/tray/ftrack_module.py index adcce9c2b1..ce2754c25d 100644 --- a/pype/ftrack/tray/ftrack_module.py +++ b/pype/ftrack/tray/ftrack_module.py @@ -88,9 +88,11 @@ class FtrackModule: def set_action_server(self): try: self.action_server.run_server() - except Exception: - msg = 'Ftrack Action server crashed! Please try to start again.' - log.error(msg) + except Exception as exc: + log.error( + "Ftrack Action server crashed! Please try to start again.", + exc_info=True + ) # TODO show message to user self.bool_action_server = False self.set_menu_visibility() diff --git a/pype/lib.py b/pype/lib.py index 66cef40674..6eee38f6d8 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -51,7 +51,7 @@ def get_hierarchy(asset_name=None): }) not_set = "PARENTS_NOT_SET" - entity_parents = entity.get("data", {}).get("parents", not_set) + entity_parents = asset_entity.get("data", {}).get("parents", not_set) # If entity already have parents then just return joined if entity_parents != not_set: @@ -467,10 +467,18 @@ def filter_pyblish_plugins(plugins): host = api.current_host() + presets = config.get_presets().get('plugins', {}).get(host, {}).get( + "publish", {} + ) + # iterate over plugins for plugin in plugins[:]: + # skip if there are no presets to process + if not presets: + continue + try: - config_data = config.get_presets()['plugins'][host]["publish"][plugin.__name__] # noqa: E501 + config_data = presets[plugin.__name__] # noqa: E501 except KeyError: continue @@ -483,3 +491,7 @@ def filter_pyblish_plugins(plugins): option, value, plugin.__name__)) setattr(plugin, option, value) + + # Remove already processed plugins from dictionary + # WARNING Requires plugins with unique names + presets.pop(plugin.__name__) diff --git a/pype/nuke/__init__.py b/pype/nuke/__init__.py index 94f591b2e8..0c4cdc10ab 100644 --- a/pype/nuke/__init__.py +++ b/pype/nuke/__init__.py @@ -59,13 +59,14 @@ class NukeHandler(logging.Handler): '''Adding Nuke Logging Handler''' +log.info([handler.get_name() for handler in logging.root.handlers[:]]) nuke_handler = NukeHandler() if nuke_handler.get_name() \ not in [handler.get_name() for handler in logging.root.handlers[:]]: logging.getLogger().addHandler(nuke_handler) logging.getLogger().setLevel(logging.INFO) - +log.info([handler.get_name() for handler in logging.root.handlers[:]]) def reload_config(): """Attempt to reload pipeline at run-time. @@ -77,10 +78,7 @@ def reload_config(): import importlib for module in ( - "app", - "app.api", "{}.api".format(AVALON_CONFIG), - "{}.templates".format(AVALON_CONFIG), "{}.nuke.actions".format(AVALON_CONFIG), "{}.nuke.templates".format(AVALON_CONFIG), "{}.nuke.menu".format(AVALON_CONFIG), @@ -96,9 +94,8 @@ def reload_config(): def install(): - - # api.set_avalon_workdir() - # reload_config() + ''' Installing all requarements for Nuke host + ''' log.info("Registering Nuke plug-ins..") pyblish.register_plugin_path(PUBLISH_PATH) @@ -117,8 +114,6 @@ def install(): avalon.data["familiesStateDefault"] = False avalon.data["familiesStateToggled"] = family_states - menu.install() - # Workfiles. launch_workfiles = os.environ.get("WORKFILES_STARTUP") @@ -128,14 +123,21 @@ def install(): # Set context settings. nuke.addOnCreate(lib.set_context_settings, nodeClass="Root") + menu.install() + + def launch_workfiles_app(): + '''Function letting start workfiles after start of host + ''' if not self.workfiles_launched: self.workfiles_launched = True workfiles.show(os.environ["AVALON_WORKDIR"]) def uninstall(): + '''Uninstalling host's integration + ''' log.info("Deregistering Nuke plug-ins..") pyblish.deregister_plugin_path(PUBLISH_PATH) avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) @@ -144,8 +146,13 @@ def uninstall(): pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) + reload_config() + menu.uninstall() + + def on_pyblish_instance_toggled(instance, old_value, new_value): """Toggle node passthrough states on instance toggles.""" + log.info("instance toggle: {}, old_value: {}, new_value:{} ".format( instance, old_value, new_value)) diff --git a/pype/nuke/actions.py b/pype/nuke/actions.py index 640e41a7de..c0c95e9080 100644 --- a/pype/nuke/actions.py +++ b/pype/nuke/actions.py @@ -1,6 +1,3 @@ -# absolute_import is needed to counter the `module has no cmds error` in Maya -from __future__ import absolute_import - import pyblish.api from avalon.nuke.lib import ( @@ -12,7 +9,7 @@ from ..action import get_errored_instances_from_context class SelectInvalidAction(pyblish.api.Action): - """Select invalid nodes in Maya when plug-in failed. + """Select invalid nodes in Nuke when plug-in failed. To retrieve the invalid nodes this assumes a static `get_invalid()` method is available on the plugin. diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index ec6d9514f4..bd6a581e6f 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -20,6 +20,8 @@ self._project = None def onScriptLoad(): + ''' Callback for ffmpeg support + ''' if nuke.env['LINUX']: nuke.tcl('load ffmpegReader') nuke.tcl('load ffmpegWriter') @@ -37,6 +39,7 @@ def checkInventoryVersions(): and check if the node is having actual version. If not then it will color it to red. """ + # TODO: make it for all nodes not just Read (Loader # get all Loader nodes by avalon attribute metadata for each in nuke.allNodes(): @@ -76,13 +79,16 @@ def checkInventoryVersions(): def writes_version_sync(): + ''' Callback synchronizing version of publishable write nodes + ''' + # TODO: make it work with new write node group try: rootVersion = pype.get_version_from_path(nuke.root().name()) padding = len(rootVersion) new_version = "v" + str("{" + ":0>{}".format(padding) + "}").format( int(rootVersion) ) - log.info("new_version: {}".format(new_version)) + log.debug("new_version: {}".format(new_version)) except Exception: return @@ -92,32 +98,34 @@ def writes_version_sync(): try: if avalon_knob_data['families'] not in ["render"]: - log.info(avalon_knob_data['families']) + log.debug(avalon_knob_data['families']) continue node_file = each['file'].value() - log.info("node_file: {}".format(node_file)) node_version = "v" + pype.get_version_from_path(node_file) - log.info("node_version: {}".format(node_version)) + log.debug("node_version: {}".format(node_version)) node_new_file = node_file.replace(node_version, new_version) each['file'].setValue(node_new_file) if not os.path.isdir(os.path.dirname(node_new_file)): - log.info("path does not exist") + log.warning("Path does not exist! I am creating it.") os.makedirs(os.path.dirname(node_new_file), 0o766) except Exception as e: - log.debug( + log.warning( "Write node: `{}` has no version in path: {}".format(each.name(), e)) def version_up_script(): + ''' Raising working script's version + ''' import nukescripts nukescripts.script_and_write_nodes_version_up() def get_render_path(node): - + ''' Generate Render path from presets regarding avalon knob data + ''' data = dict() data['avalon'] = avalon.nuke.get_avalon_knob_data(node) @@ -141,12 +149,24 @@ def get_render_path(node): def format_anatomy(data): + ''' Helping function for formating of anatomy paths + + Arguments: + data (dict): dictionary with attributes used for formating + + Return: + path (str) + ''' + # TODO: perhaps should be nonPublic + from .templates import ( get_anatomy ) + # TODO: remove get_anatomy and import directly Anatomy() here anatomy = get_anatomy() - log.info("__ anatomy.templates: {}".format(anatomy.templates)) + log.debug("__ anatomy.templates: {}".format(anatomy.templates)) + # TODO: perhaps should be in try! padding = int(anatomy.templates['render']['padding']) version = data.get("version", None) @@ -167,17 +187,24 @@ def format_anatomy(data): "hierarchy": pype.get_hierarchy(), "frame": "#" * padding, }) - log.info("__ data: {}".format(data)) - log.info("__ format_anatomy: {}".format(anatomy.format(data))) return anatomy.format(data) def script_name(): + ''' Returns nuke script path + ''' return nuke.root().knob('name').value() +def add_button_write_to_read(node): + name = "createReadNode" + label = "Create Read" + value = "import write_to_read;write_to_read.write_to_read(nuke.thisNode())" + k = nuke.PyScript_Knob(name, label, value) + k.setFlag(0x1000) + node.addKnob(k) def create_write_node(name, data, prenodes=None): - '''Creating write node which is group node + ''' Creating write node which is group node Arguments: name (str): name of node @@ -200,6 +227,8 @@ def create_write_node(name, data, prenodes=None): ) ] + Return: + node (obj): group node with avalon data as Knobs ''' nuke_dataflow_writes = get_node_dataflow_preset(**data) @@ -212,7 +241,6 @@ def create_write_node(name, data, prenodes=None): "nuke_dataflow_writes": nuke_dataflow_writes, "nuke_colorspace_writes": nuke_colorspace_writes }) - anatomy_filled = format_anatomy(data) except Exception as e: @@ -228,7 +256,7 @@ def create_write_node(name, data, prenodes=None): # create directory if not os.path.isdir(os.path.dirname(fpath)): - log.info("path does not exist") + log.warning("Path does not exist! I am creating it.") os.makedirs(os.path.dirname(fpath), 0o766) _data = OrderedDict({ @@ -303,11 +331,15 @@ def create_write_node(name, data, prenodes=None): # imprinting group node GN = avalon.nuke.imprint(GN, data["avalon"]) + divider = nuke.Text_Knob('') GN.addKnob(divider) add_rendering_knobs(GN) + # adding write to read button + add_button_write_to_read(GN) + divider = nuke.Text_Knob('') GN.addKnob(divider) @@ -325,6 +357,14 @@ def create_write_node(name, data, prenodes=None): def add_rendering_knobs(node): + ''' Adds additional rendering knobs to given node + + Arguments: + node (obj): nuke node object to be fixed + + Return: + node (obj): with added knobs + ''' if "render" not in node.knobs(): knob = nuke.Boolean_Knob("render", "Render") knob.setFlag(0x1000) @@ -338,6 +378,12 @@ def add_rendering_knobs(node): def set_viewers_colorspace(viewer): + ''' Adds correct colorspace to viewer + + Arguments: + viewer (obj): nuke viewer node object to be fixed + + ''' assert isinstance(viewer, dict), log.error( "set_viewers_colorspace(): argument should be dictionary") @@ -381,6 +427,12 @@ def set_viewers_colorspace(viewer): def set_root_colorspace(root_dict): + ''' Adds correct colorspace to root + + Arguments: + root_dict (dict): nuke root node as dictionary + + ''' assert isinstance(root_dict, dict), log.error( "set_root_colorspace(): argument should be dictionary") @@ -397,17 +449,26 @@ def set_root_colorspace(root_dict): for knob, value in root_dict.items(): if nuke.root()[knob].value() not in value: nuke.root()[knob].setValue(str(value)) - log.info("nuke.root()['{}'] changed to: {}".format(knob, value)) + log.debug("nuke.root()['{}'] changed to: {}".format(knob, value)) def set_writes_colorspace(write_dict): + ''' Adds correct colorspace to write node dict + + Arguments: + write_dict (dict): nuke write node as dictionary + + ''' + # TODO: complete this function so any write node in scene will have fixed colorspace following presets for the project assert isinstance(write_dict, dict), log.error( "set_root_colorspace(): argument should be dictionary") - log.info("set_writes_colorspace(): {}".format(write_dict)) + + log.debug("__ set_writes_colorspace(): {}".format(write_dict)) def set_colorspace(): - + ''' Setting colorpace following presets + ''' nuke_colorspace = get_colorspace_preset().get("nuke", None) try: @@ -428,7 +489,7 @@ def set_colorspace(): try: for key in nuke_colorspace: - log.info("{}".format(key)) + log.debug("Preset's colorspace key: {}".format(key)) except TypeError: log.error("Nuke is not in templates! \n\n\n" "contact your supervisor!") @@ -474,10 +535,6 @@ def reset_frame_range_handles(): root["first_frame"].setValue(frame_start) root["last_frame"].setValue(frame_end) - log.info("__ handle_start: `{}`".format(handle_start)) - log.info("__ handle_end: `{}`".format(handle_end)) - log.info("__ fps: `{}`".format(fps)) - # setting active viewers nuke.frame(int(asset_entity["data"]["frameStart"])) @@ -488,15 +545,11 @@ def reset_frame_range_handles(): for node in nuke.allNodes(filter="Viewer"): node['frame_range'].setValue(range) node['frame_range_lock'].setValue(True) - - log.info("_frameRange: {}".format(range)) - log.info("frameRange: {}".format(node['frame_range'].value())) - node['frame_range'].setValue(range) node['frame_range_lock'].setValue(True) # adding handle_start/end to root avalon knob - if not avalon.nuke.set_avalon_knob_data(root, { + if not avalon.nuke.imprint(root, { "handleStart": int(handle_start), "handleEnd": int(handle_end) }): diff --git a/pype/nuke/menu.py b/pype/nuke/menu.py index 169ac81096..4f5410f8fd 100644 --- a/pype/nuke/menu.py +++ b/pype/nuke/menu.py @@ -2,10 +2,11 @@ import nuke from avalon.api import Session from pype.nuke import lib +from pypeapp import Logger +log = Logger().get_logger(__name__, "nuke") def install(): - menubar = nuke.menu("Nuke") menu = menubar.findItem(Session["AVALON_LABEL"]) @@ -15,8 +16,11 @@ def install(): 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, lib.reset_resolution, index=rm_item[0]) + menu.addCommand(new_name, lib.reset_resolution, index=(rm_item[0])) # replace reset frame range from avalon core to pype's name = "Reset Frame Range" @@ -24,8 +28,10 @@ def install(): 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, lib.reset_frame_range_handles, index=rm_item[0]) + menu.addCommand(new_name, lib.reset_frame_range_handles, index=(rm_item[0])) # add colorspace menu item name = "Set colorspace" @@ -33,9 +39,22 @@ def install(): name, lib.set_colorspace, index=(rm_item[0]+2) ) + log.debug("Adding menu item: {}".format(name)) # add item that applies all setting above name = "Apply all settings" menu.addCommand( name, lib.set_context_settings, index=(rm_item[0]+3) ) + log.debug("Adding menu item: {}".format(name)) + + + +def uninstall(): + + menubar = nuke.menu("Nuke") + menu = menubar.findItem(Session["AVALON_LABEL"]) + + for item in menu.items(): + log.info("Removing menu item: {}".format(item.name())) + menu.removeItem(item.name()) diff --git a/pype/nuke/templates.py b/pype/nuke/templates.py index 797335d982..6434d73f1d 100644 --- a/pype/nuke/templates.py +++ b/pype/nuke/templates.py @@ -20,6 +20,8 @@ def get_colorspace_preset(): def get_node_dataflow_preset(**kwarg): + ''' Get preset data for dataflow (fileType, compression, bitDepth) + ''' log.info(kwarg) host = kwarg.get("host", "nuke") cls = kwarg.get("class", None) @@ -39,6 +41,8 @@ def get_node_dataflow_preset(**kwarg): def get_node_colorspace_preset(**kwarg): + ''' Get preset data for colorspace + ''' log.info(kwarg) host = kwarg.get("host", "nuke") cls = kwarg.get("class", None) diff --git a/pype/nukestudio/__init__.py b/pype/nukestudio/__init__.py index 2e7ccdd675..c900848a93 100644 --- a/pype/nukestudio/__init__.py +++ b/pype/nukestudio/__init__.py @@ -1,5 +1,6 @@ import os - +from pypeapp import Logger +import hiero from avalon.tools import workfiles from avalon import api as avalon from pyblish import api as pyblish @@ -13,20 +14,12 @@ from .workio import ( work_root ) -from .. import api - from .menu import ( install as menu_install, _update_menu_task_label ) from .tags import add_tags_from_presets -from pypeapp import Logger - -import hiero - -log = Logger().get_logger(__name__, "nukestudio") - __all__ = [ # Workfiles API "open", @@ -35,11 +28,16 @@ __all__ = [ "has_unsaved_changes", "file_extensions", "work_root", - ] +] + +# get logger +log = Logger().get_logger(__name__, "nukestudio") +''' Creating all important host related variables ''' AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") +# plugin root path PARENT_DIR = os.path.dirname(__file__) PACKAGE_DIR = os.path.dirname(PARENT_DIR) PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") @@ -49,13 +47,21 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "nukestudio", "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "nukestudio", "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "nukestudio", "inventory") - +# registering particular pyblish gui but `lite` is recomended!! if os.getenv("PYBLISH_GUI", None): pyblish.register_gui(os.getenv("PYBLISH_GUI", None)) def install(config): + """ + Installing Nukestudio integration for avalon + Args: + config (obj): avalon config module `pype` in our case, it is not used but required by avalon.api.install() + + """ + + # adding all events _register_events() log.info("Registering NukeStudio plug-ins..") @@ -74,6 +80,7 @@ def install(config): avalon.data["familiesStateDefault"] = False avalon.data["familiesStateToggled"] = family_states + # install menu menu_install() # Workfiles. @@ -91,11 +98,26 @@ def install(config): def add_tags(event): + """ + Event for automatic tag creation after nukestudio start + + Args: + event (obj): required but unused + """ + add_tags_from_presets() def launch_workfiles_app(event): - workfiles.show(os.environ["AVALON_WORKDIR"]) + """ + Event for launching workfiles after nukestudio start + + Args: + event (obj): required but unused + """ + from .lib import set_workfiles + + set_workfiles() # Closing the new project. event.sender.close() @@ -107,6 +129,10 @@ def launch_workfiles_app(event): def uninstall(): + """ + Uninstalling Nukestudio integration for avalon + + """ log.info("Deregistering NukeStudio plug-ins..") pyblish.deregister_host("nukestudio") pyblish.deregister_plugin_path(PUBLISH_PATH) @@ -115,6 +141,11 @@ def uninstall(): def _register_events(): + """ + Adding all callbacks. + """ + + # if task changed then change notext of nukestudio avalon.on("taskChanged", _update_menu_task_label) log.info("Installed event callback for 'taskChanged'..") @@ -129,4 +160,5 @@ def ls(): See the `container.json` schema for details on how it should look, and the Maya equivalent, which is in `avalon.maya.pipeline` """ + # TODO: listing all availabe containers form sequence return diff --git a/pype/nukestudio/lib.py b/pype/nukestudio/lib.py index 9136fb359a..6674e8a3aa 100644 --- a/pype/nukestudio/lib.py +++ b/pype/nukestudio/lib.py @@ -1,19 +1,13 @@ -# Standard library import os import sys - -# Pyblish libraries -import pyblish.api - -import avalon.api as avalon -import pype.api as pype - -from avalon.vendor.Qt import (QtWidgets, QtGui) - -# Host libraries import hiero - +import pyblish.api +import avalon.api as avalon +from avalon.vendor.Qt import (QtWidgets, QtGui) +import pype.api as pype from pypeapp import Logger + + log = Logger().get_logger(__name__, "nukestudio") cached_process = None @@ -30,12 +24,18 @@ AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") def set_workfiles(): ''' Wrapping function for workfiles launcher ''' from avalon.tools import workfiles + + # import session to get project dir S = avalon.Session active_project_root = os.path.normpath( os.path.join(S['AVALON_PROJECTS'], S['AVALON_PROJECT']) ) workdir = os.environ["AVALON_WORKDIR"] + + # show workfile gui workfiles.show(workdir) + + # getting project project = hiero.core.projects()[-1] # set project root with backward compatibility @@ -64,11 +64,10 @@ def set_workfiles(): # set fps to hiero project project.setFramerate(fps) + # TODO: add auto colorspace set from project drop log.info("Project property has been synchronised with Avalon db") - - def reload_config(): """Attempt to reload pipeline at run-time. @@ -189,6 +188,10 @@ def add_submission(): class PublishAction(QtWidgets.QAction): + """ + Action with is showing as menu item + """ + def __init__(self): QtWidgets.QAction.__init__(self, "Publish", None) self.triggered.connect(self.publish) @@ -213,7 +216,8 @@ class PublishAction(QtWidgets.QAction): def _show_no_gui(): - """Popup with information about how to register a new GUI + """ + Popup with information about how to register a new GUI In the event of no GUI being registered or available, this information dialog will appear to guide the user through how to get set up with one. diff --git a/pype/nukestudio/menu.py b/pype/nukestudio/menu.py index 6babceff41..a996389524 100644 --- a/pype/nukestudio/menu.py +++ b/pype/nukestudio/menu.py @@ -1,24 +1,23 @@ import os import sys import hiero.core +from pypeapp import Logger +from avalon.api import Session +from hiero.ui import findMenuAction +# this way we secure compatibility between nuke 10 and 11 try: from PySide.QtGui import * except Exception: from PySide2.QtGui import * from PySide2.QtWidgets import * -from hiero.ui import findMenuAction - -from avalon.api import Session - from .tags import add_tags_from_presets from .lib import ( reload_config, set_workfiles ) -from pypeapp import Logger log = Logger().get_logger(__name__, "nukestudio") @@ -45,6 +44,11 @@ def _update_menu_task_label(*args): def install(): + """ + Installing menu into Nukestudio + + """ + # here is the best place to add menu from avalon.tools import ( creator, @@ -127,8 +131,6 @@ def install(): 'icon': QIcon('icons:ColorAdd.png') }] - - # Create menu items for a in actions: add_to_menu = menu diff --git a/pype/nukestudio/tags.py b/pype/nukestudio/tags.py index d9574bdf2b..8ae88d731c 100644 --- a/pype/nukestudio/tags.py +++ b/pype/nukestudio/tags.py @@ -1,5 +1,6 @@ import re import os +import hiero from pypeapp import ( config, @@ -7,8 +8,6 @@ from pypeapp import ( ) from avalon import io -import hiero - log = Logger().get_logger(__name__, "nukestudio") diff --git a/pype/plugins/maya/load/load_reference.py b/pype/plugins/maya/load/load_reference.py index 199d79c941..fb4b90a1cd 100644 --- a/pype/plugins/maya/load/load_reference.py +++ b/pype/plugins/maya/load/load_reference.py @@ -74,12 +74,14 @@ class ReferenceLoader(pype.maya.plugin.ReferenceLoader): # for backwards compatibility class AbcLoader(ReferenceLoader): + label = "Deprecated loader (don't use)" families = ["pointcache", "animation"] representations = ["abc"] tool_names = [] # for backwards compatibility class ModelLoader(ReferenceLoader): + label = "Deprecated loader (don't use)" families = ["model", "pointcache"] representations = ["abc"] tool_names = [] diff --git a/pype/plugins/maya/publish/collect_renderlayers.py b/pype/plugins/maya/publish/collect_renderlayers.py index 593ab2e74d..ce80039362 100644 --- a/pype/plugins/maya/publish/collect_renderlayers.py +++ b/pype/plugins/maya/publish/collect_renderlayers.py @@ -64,9 +64,9 @@ class CollectMayaRenderlayers(pyblish.api.ContextPlugin): "subset": layername, "setMembers": layer, "publish": True, - "frameStart": self.get_render_attribute("frameStart", + "frameStart": self.get_render_attribute("startFrame", layer=layer), - "frameEnd": self.get_render_attribute("frameEnd", + "frameEnd": self.get_render_attribute("endFrame", layer=layer), "byFrameStep": self.get_render_attribute("byFrameStep", layer=layer), diff --git a/pype/plugins/maya/publish/validate_scene_set_workspace.py b/pype/plugins/maya/publish/validate_scene_set_workspace.py index 778c7eae86..bda397cf2a 100644 --- a/pype/plugins/maya/publish/validate_scene_set_workspace.py +++ b/pype/plugins/maya/publish/validate_scene_set_workspace.py @@ -12,7 +12,7 @@ def is_subdir(path, root_dir): root_dir = os.path.realpath(root_dir) # If not on same drive - if os.path.splitdrive(path)[0] != os.path.splitdrive(root_dir)[0]: + if os.path.splitdrive(path)[0].lower() != os.path.splitdrive(root_dir)[0].lower(): # noqa: E501 return False # Get 'relative path' (can contain ../ which means going up) diff --git a/pype/plugins/nuke/create/create_write.py b/pype/plugins/nuke/create/create_write.py index 588e5ee6f3..03107238b5 100644 --- a/pype/plugins/nuke/create/create_write.py +++ b/pype/plugins/nuke/create/create_write.py @@ -16,7 +16,6 @@ def subset_to_families(subset, family, families): new_subset = families + subset_sufx return "{}.{}".format(family, new_subset) - class CreateWriteRender(avalon.nuke.Creator): # change this to template preset preset = "render" diff --git a/pype/plugins/nuke/load/load_sequence.py b/pype/plugins/nuke/load/load_sequence.py index 44b43d0f9a..5fd43d3481 100644 --- a/pype/plugins/nuke/load/load_sequence.py +++ b/pype/plugins/nuke/load/load_sequence.py @@ -76,7 +76,7 @@ class LoadSequence(api.Loader): """Load image sequence into Nuke""" families = ["write", "source", "plate", "render"] - representations = ["exr", "dpx"] + representations = ["exr", "dpx", "jpg", "jpeg"] label = "Load sequence" order = -10 @@ -94,20 +94,11 @@ class LoadSequence(api.Loader): log.info("version_data: {}\n".format(version_data)) + self.first_frame = int(nuke.root()["first_frame"].getValue()) + self.handle_start = version_data.get("handleStart", 0) + first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) - handles = version_data.get("handles", 0) - handle_start = version_data.get("handleStart", 0) - handle_end = version_data.get("handleEnd", 0) - - # fix handle start and end if none are available - if not handle_start and not handle_end: - handle_start = handles - handle_end = handles - - # # create handles offset - # first -= handle_start - # last += handle_end # Fallback to asset name when namespace is None if namespace is None: @@ -138,7 +129,7 @@ class LoadSequence(api.Loader): r["last"].setValue(int(last)) # add additional metadata from the version to imprint to Avalon knob - add_keys = ["frameStart", "frameEnd", "handles", + add_keys = ["frameStart", "frameEnd", "source", "colorspace", "author", "fps", "version", "handleStart", "handleEnd"] @@ -147,12 +138,18 @@ class LoadSequence(api.Loader): if k is 'version': data_imprint.update({k: context["version"]['name']}) else: - data_imprint.update({k: context["version"]['data'].get(k, str(None))}) + data_imprint.update( + {k: context["version"]['data'].get(k, str(None))}) data_imprint.update({"objectName": read_name}) r["tile_color"].setValue(int("0x4ecd25ff", 16)) + if version_data.get("retime", None): + speed = version_data.get("speed", 1) + time_warp_nodes = version_data.get("timewarps", []) + self.make_retimes(r, speed, time_warp_nodes) + return containerise(r, name=name, namespace=namespace, @@ -160,6 +157,34 @@ class LoadSequence(api.Loader): loader=self.__class__.__name__, data=data_imprint) + def make_retimes(self, node, speed, time_warp_nodes): + ''' Create all retime and timewarping nodes with coppied animation ''' + if speed != 1: + rtn = nuke.createNode( + "Retime", + "speed {}".format(speed)) + rtn["before"].setValue("continue") + rtn["after"].setValue("continue") + rtn["input.first_lock"].setValue(True) + rtn["input.first"].setValue( + self.handle_start + self.first_frame + ) + + if time_warp_nodes != []: + for timewarp in time_warp_nodes: + twn = nuke.createNode(timewarp["Class"], + "name {}".format(timewarp["name"])) + if isinstance(timewarp["lookup"], list): + # if array for animation + twn["lookup"].setAnimated() + for i, value in enumerate(timewarp["lookup"]): + twn["lookup"].setValueAt( + (self.first_frame + i) + value, + (self.first_frame + i)) + else: + # if static value `int` + twn["lookup"].setValue(timewarp["lookup"]) + def switch(self, container, representation): self.update(container, representation) @@ -200,11 +225,11 @@ class LoadSequence(api.Loader): version_data = version.get("data", {}) + self.first_frame = int(nuke.root()["first_frame"].getValue()) + self.handle_start = version_data.get("handleStart", 0) + first = version_data.get("frameStart", None) last = version_data.get("frameEnd", None) - handles = version_data.get("handles", 0) - handle_start = version_data.get("handleStart", 0) - handle_end = version_data.get("handleEnd", 0) if first is None: log.warning("Missing start frame for updated version" @@ -212,15 +237,6 @@ class LoadSequence(api.Loader): "{} ({})".format(node['name'].value(), representation)) first = 0 - # fix handle start and end if none are available - if not handle_start and not handle_end: - handle_start = handles - handle_end = handles - - # create handles offset - first -= handle_start - last += handle_end - # Update the loader's path whilst preserving some values with preserve_trim(node): node["file"].setValue(file["path"]) @@ -241,7 +257,6 @@ class LoadSequence(api.Loader): "version": version.get("name"), "colorspace": version_data.get("colorspace"), "source": version_data.get("source"), - "handles": version_data.get("handles"), "handleStart": version_data.get("handleStart"), "handleEnd": version_data.get("handleEnd"), "fps": version_data.get("fps"), @@ -255,6 +270,11 @@ class LoadSequence(api.Loader): else: node["tile_color"].setValue(int("0x4ecd25ff", 16)) + if version_data.get("retime", None): + speed = version_data.get("speed", 1) + time_warp_nodes = version_data.get("timewarps", []) + self.make_retimes(node, speed, time_warp_nodes) + # Update the imprinted representation update_container( node, diff --git a/pype/plugins/nuke/publish/collect_asset_info.py b/pype/plugins/nuke/publish/collect_asset_info.py index 4bfcb0ab00..76b93ef3d0 100644 --- a/pype/plugins/nuke/publish/collect_asset_info.py +++ b/pype/plugins/nuke/publish/collect_asset_info.py @@ -1,4 +1,3 @@ -import nuke from avalon import api, io import pyblish.api @@ -19,5 +18,6 @@ class CollectAssetInfo(pyblish.api.ContextPlugin): self.log.info("asset_data: {}".format(asset_data)) context.data['handles'] = int(asset_data["data"].get("handles", 0)) - context.data["handleStart"] = int(asset_data["data"].get("handleStart", 0)) + context.data["handleStart"] = int(asset_data["data"].get( + "handleStart", 0)) context.data["handleEnd"] = int(asset_data["data"].get("handleEnd", 0)) diff --git a/pype/plugins/nuke/publish/collect_legacy_read.py b/pype/plugins/nuke/publish/collect_legacy_read.py new file mode 100644 index 0000000000..6b6ce57245 --- /dev/null +++ b/pype/plugins/nuke/publish/collect_legacy_read.py @@ -0,0 +1,30 @@ +import toml + +import nuke + +import pyblish.api + + +class CollectReadLegacy(pyblish.api.ContextPlugin): + """Collect legacy read nodes.""" + + order = pyblish.api.CollectorOrder + label = "Collect Read Legacy" + hosts = ["nuke", "nukeassist"] + + def process(self, context): + + for node in nuke.allNodes(): + if node.Class() != "Read": + continue + + if "avalon" not in node.knobs().keys(): + continue + + if not toml.loads(node["avalon"].value()): + return + + instance = context.create_instance( + node.name(), family="read.legacy" + ) + instance.append(node) diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py index a5782ac223..0017de3ec4 100644 --- a/pype/plugins/nuke/publish/submit_nuke_deadline.py +++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py @@ -27,9 +27,13 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): def process(self, instance): - # root = nuke.root() - # node_subset_name = instance.data.get("name", None) - node = instance[1] + node = None + for x in instance: + if x.Class() == "Write": + node = x + + if node is None: + return DEADLINE_REST_URL = os.environ.get("DEADLINE_REST_URL", "http://localhost:8082") diff --git a/pype/plugins/nuke/publish/validate_read_legacy.py b/pype/plugins/nuke/publish/validate_read_legacy.py new file mode 100644 index 0000000000..22a9b3678e --- /dev/null +++ b/pype/plugins/nuke/publish/validate_read_legacy.py @@ -0,0 +1,83 @@ +import os +import toml + +import nuke + +import pyblish.api +from avalon import api +from bson.objectid import ObjectId + + +class RepairReadLegacyAction(pyblish.api.Action): + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + + # Get the errored instances + failed = [] + for result in context.data["results"]: + if (result["error"] is not None and result["instance"] is not None + and result["instance"] not in failed): + failed.append(result["instance"]) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(failed, plugin) + + for instance in instances: + + data = toml.loads(instance[0]["avalon"].value()) + data["name"] = instance[0].name() + data["xpos"] = instance[0].xpos() + data["ypos"] = instance[0].ypos() + data["extension"] = os.path.splitext( + instance[0]["file"].value() + )[1][1:] + + data["connections"] = [] + for d in instance[0].dependent(): + for i in range(d.inputs()): + if d.input(i) == instance[0]: + data["connections"].append([i, d]) + + nuke.delete(instance[0]) + + loader_name = "LoadSequence" + if data["extension"] == "mov": + loader_name = "LoadMov" + + loader_plugin = None + for Loader in api.discover(api.Loader): + if Loader.__name__ != loader_name: + continue + + loader_plugin = Loader + + api.load( + Loader=loader_plugin, + representation=ObjectId(data["representation"]) + ) + + node = nuke.toNode(data["name"]) + for connection in data["connections"]: + connection[1].setInput(connection[0], node) + + node.setXYpos(data["xpos"], data["ypos"]) + + +class ValidateReadLegacy(pyblish.api.InstancePlugin): + """Validate legacy read instance[0]s.""" + + order = pyblish.api.ValidatorOrder + optional = True + families = ["read.legacy"] + label = "Read Legacy" + hosts = ["nuke"] + actions = [RepairReadLegacyAction] + + def process(self, instance): + + msg = "Clean up legacy read node \"{}\"".format(instance) + assert False, msg diff --git a/pype/plugins/nukestudio/publish/collect_calculate_retime.py b/pype/plugins/nukestudio/publish/collect_calculate_retime.py new file mode 100644 index 0000000000..a97b43a4ce --- /dev/null +++ b/pype/plugins/nukestudio/publish/collect_calculate_retime.py @@ -0,0 +1,121 @@ +from pyblish import api +import hiero +import math + + +class CollectCalculateRetime(api.InstancePlugin): + """Calculate Retiming of selected track items.""" + + order = api.CollectorOrder + 0.02 + label = "Collect Calculate Retiming" + hosts = ["nukestudio"] + families = ['retime'] + + def process(self, instance): + margin_in = instance.data["retimeMarginIn"] + margin_out = instance.data["retimeMarginOut"] + self.log.debug("margin_in: '{0}', margin_out: '{1}'".format(margin_in, margin_out)) + + handle_start = instance.data["handleStart"] + handle_end = instance.data["handleEnd"] + + track_item = instance.data["item"] + + # define basic clip frame range variables + timeline_in = int(track_item.timelineIn()) + timeline_out = int(track_item.timelineOut()) + source_in = int(track_item.sourceIn()) + source_out = int(track_item.sourceOut()) + speed = track_item.playbackSpeed() + self.log.debug("_BEFORE: \n timeline_in: `{0}`,\n timeline_out: `{1}`,\ + \n source_in: `{2}`,\n source_out: `{3}`,\n speed: `{4}`,\n handle_start: `{5}`,\n handle_end: `{6}`".format( + timeline_in, + timeline_out, + source_in, + source_out, + speed, + handle_start, + handle_end + )) + + # loop withing subtrack items + source_in_change = 0 + source_out_change = 0 + for s_track_item in track_item.linkedItems(): + if isinstance(s_track_item, hiero.core.EffectTrackItem) \ + and "TimeWarp" in s_track_item.node().Class(): + + # adding timewarp attribute to instance + if not instance.data.get("timeWarpNodes", None): + instance.data["timeWarpNodes"] = list() + + # ignore item if not enabled + if s_track_item.isEnabled(): + node = s_track_item.node() + name = node["name"].value() + look_up = node["lookup"].value() + animated = node["lookup"].isAnimated() + if animated: + look_up = [((node["lookup"].getValueAt(i)) - i) + for i in range((timeline_in - handle_start), (timeline_out + handle_end) + 1) + ] + # calculate differnce + diff_in = (node["lookup"].getValueAt( + timeline_in)) - timeline_in + diff_out = (node["lookup"].getValueAt( + timeline_out)) - timeline_out + + # calculate source + source_in_change += diff_in + source_out_change += diff_out + + # calculate speed + speed_in = (node["lookup"].getValueAt(timeline_in) / ( + float(timeline_in) * .01)) * .01 + speed_out = (node["lookup"].getValueAt(timeline_out) / ( + float(timeline_out) * .01)) * .01 + + # calculate handles + handle_start = int( + math.ceil( + (handle_start * speed_in * 1000) / 1000.0) + ) + + handle_end = int( + math.ceil( + (handle_end * speed_out * 1000) / 1000.0) + ) + self.log.debug( + ("diff_in, diff_out", diff_in, diff_out)) + self.log.debug( + ("source_in_change, source_out_change", source_in_change, source_out_change)) + + instance.data["timeWarpNodes"].append({"Class": "TimeWarp", + "name": name, + "lookup": look_up}) + + self.log.debug((source_in_change, source_out_change)) + # recalculate handles by the speed + handle_start *= speed + handle_end *= speed + self.log.debug("speed: handle_start: '{0}', handle_end: '{1}'".format(handle_start, handle_end)) + + source_in += int(source_in_change) + source_out += int(source_out_change * speed) + handle_start += (margin_in) + handle_end += (margin_out) + self.log.debug("margin: handle_start: '{0}', handle_end: '{1}'".format(handle_start, handle_end)) + + # add all data to Instance + instance.data["sourceIn"] = source_in + instance.data["sourceOut"] = source_out + instance.data["sourceInH"] = int(source_in - math.ceil( + (handle_start * 1000) / 1000.0)) + instance.data["sourceOutH"] = int(source_out + math.ceil( + (handle_end * 1000) / 1000.0)) + instance.data["speed"] = speed + + self.log.debug("timeWarpNodes: {}".format(instance.data["timeWarpNodes"])) + self.log.debug("sourceIn: {}".format(instance.data["sourceIn"])) + self.log.debug("sourceOut: {}".format(instance.data["sourceOut"])) + self.log.debug("speed: {}".format(instance.data["speed"])) diff --git a/pype/plugins/nukestudio/publish/collect_frame_ranges.py b/pype/plugins/nukestudio/publish/collect_frame_ranges.py index 392dbba68b..38224f683d 100644 --- a/pype/plugins/nukestudio/publish/collect_frame_ranges.py +++ b/pype/plugins/nukestudio/publish/collect_frame_ranges.py @@ -1,5 +1,6 @@ import pyblish.api + class CollectClipFrameRanges(pyblish.api.InstancePlugin): """Collect all frame range data: source(In,Out), timeline(In,Out), edit_(in, out), f(start, end)""" @@ -15,8 +16,10 @@ class CollectClipFrameRanges(pyblish.api.InstancePlugin): handle_start = instance.data["handleStart"] handle_end = instance.data["handleEnd"] - source_in_h = instance.data["sourceIn"] - handle_start - source_out_h = instance.data["sourceOut"] + handle_end + source_in_h = instance.data("sourceInH", + instance.data("sourceIn") - handle_start) + source_out_h = instance.data("sourceOutH", + instance.data("sourceOut") + handle_end) timeline_in = instance.data["clipIn"] timeline_out = instance.data["clipOut"] diff --git a/pype/plugins/nukestudio/publish/collect_plates.py b/pype/plugins/nukestudio/publish/collect_plates.py index c9e6305062..9843307f14 100644 --- a/pype/plugins/nukestudio/publish/collect_plates.py +++ b/pype/plugins/nukestudio/publish/collect_plates.py @@ -137,7 +137,6 @@ class CollectPlatesData(api.InstancePlugin): "subset": name, "fps": instance.context.data["fps"] }) - instance.data["versionData"] = version_data try: basename, ext = os.path.splitext(source_file) @@ -156,9 +155,11 @@ class CollectPlatesData(api.InstancePlugin): start_frame = source_first_frame + instance.data["sourceInH"] duration = instance.data["sourceOutH"] - instance.data["sourceInH"] end_frame = start_frame + duration + self.log.debug("start_frame: `{}`".format(start_frame)) + self.log.debug("end_frame: `{}`".format(end_frame)) files = [file % i for i in range(start_frame, (end_frame + 1), 1)] except Exception as e: - self.log.debug("Exception in file: {}".format(e)) + self.log.warning("Exception in file: {}".format(e)) head, ext = os.path.splitext(source_file) ext = ext[1:] files = source_file @@ -207,16 +208,41 @@ class CollectPlatesData(api.InstancePlugin): thumb_representation) # adding representation for plates + frame_start = instance.data["frameStart"] - \ + instance.data["handleStart"] + frame_end = instance.data["frameEnd"] + instance.data["handleEnd"] + + # exception for retimes + if instance.data.get("retime"): + source_in_h = instance.data["sourceInH"] + source_in = instance.data["sourceIn"] + source_handle_start = source_in_h - source_in + frame_start = instance.data["frameStart"] + source_handle_start + duration = instance.data["sourceOutH"] - instance.data["sourceInH"] + frame_end = frame_start + duration + plates_representation = { 'files': files, 'stagingDir': staging_dir, 'name': ext, 'ext': ext, - "frameStart": instance.data["frameStart"] - instance.data["handleStart"], - "frameEnd": instance.data["frameEnd"] + instance.data["handleEnd"], + "frameStart": frame_start, + "frameEnd": frame_end, } instance.data["representations"].append(plates_representation) + # deal with retimed clip + if instance.data.get("retime"): + version_data.update({ + "retime": True, + "speed": instance.data.get("speed", 1), + "timewarps": instance.data.get("timeWarpNodes", []), + "frameStart": frame_start, + "frameEnd": frame_end, + }) + + instance.data["versionData"] = version_data + # testing families family = instance.data["family"] families = instance.data["families"] diff --git a/pype/plugins/nukestudio/publish/collect_tag_framestart.py b/pype/plugins/nukestudio/publish/collect_tag_framestart.py index c2778ea680..c73a2dd1ee 100644 --- a/pype/plugins/nukestudio/publish/collect_tag_framestart.py +++ b/pype/plugins/nukestudio/publish/collect_tag_framestart.py @@ -1,5 +1,5 @@ from pyblish import api - +import os class CollectClipTagFrameStart(api.InstancePlugin): """Collect FrameStart from Tags of selected track items.""" @@ -19,8 +19,20 @@ class CollectClipTagFrameStart(api.InstancePlugin): # gets only task family tags and collect labels if "frameStart" in t_family: + t_value = t_metadata.get("tag.value", "") + + # backward compatibility t_number = t_metadata.get("tag.number", "") - start_frame = int(t_number) + + try: + start_frame = int(t_number) or int(t_value) + except ValueError: + if "source" in t_value: + source_first = instance.data["sourceFirst"] + source_in = instance.data["sourceIn"] + handle_start = instance.data["handleStart"] + start_frame = (source_first + source_in) - handle_start + instance.data["startingFrame"] = start_frame self.log.info("Start frame on `{0}` set to `{1}`".format( instance, start_frame diff --git a/pype/plugins/nukestudio/publish/collect_tag_retime.py b/pype/plugins/nukestudio/publish/collect_tag_retime.py new file mode 100644 index 0000000000..32e49e1b2a --- /dev/null +++ b/pype/plugins/nukestudio/publish/collect_tag_retime.py @@ -0,0 +1,32 @@ +from pyblish import api + + +class CollectTagRetime(api.InstancePlugin): + """Collect Retiming from Tags of selected track items.""" + + order = api.CollectorOrder + 0.014 + label = "Collect Retiming Tag" + hosts = ["nukestudio"] + families = ['clip'] + + def process(self, instance): + # gets tags + tags = instance.data["tags"] + + for t in tags: + t_metadata = dict(t["metadata"]) + t_family = t_metadata.get("tag.family", "") + + # gets only task family tags and collect labels + if "retiming" in t_family: + margin_in = t_metadata.get("tag.marginIn", "") + margin_out = t_metadata.get("tag.marginOut", "") + + instance.data["retimeMarginIn"] = int(margin_in) + instance.data["retimeMarginOut"] = int(margin_out) + instance.data["retime"] = True + + self.log.info("retimeMarginIn: `{}`".format(margin_in)) + self.log.info("retimeMarginOut: `{}`".format(margin_out)) + + instance.data["families"] += ["retime"] diff --git a/res/ftrack/action_icons/AttributesRemapper.svg b/res/ftrack/action_icons/AttributesRemapper.svg deleted file mode 100644 index 94bf8c4f14..0000000000 --- a/res/ftrack/action_icons/AttributesRemapper.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/ftrack/action_icons/CustomAttributes.svg b/res/ftrack/action_icons/CustomAttributes.svg deleted file mode 100644 index ee1af3378e..0000000000 --- a/res/ftrack/action_icons/CustomAttributes.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/res/ftrack/action_icons/JobKiller.svg b/res/ftrack/action_icons/JobKiller.svg deleted file mode 100644 index 595c780a9b..0000000000 --- a/res/ftrack/action_icons/JobKiller.svg +++ /dev/null @@ -1,374 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/res/ftrack/action_icons/PrepareProject.svg b/res/ftrack/action_icons/PrepareProject.svg new file mode 100644 index 0000000000..bd6b460ce3 --- /dev/null +++ b/res/ftrack/action_icons/PrepareProject.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/PypeAdmin.svg b/res/ftrack/action_icons/PypeAdmin.svg new file mode 100644 index 0000000000..c95a29dacb --- /dev/null +++ b/res/ftrack/action_icons/PypeAdmin.svg @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/PypeDoctor.svg b/res/ftrack/action_icons/PypeDoctor.svg new file mode 100644 index 0000000000..e921d99ee5 --- /dev/null +++ b/res/ftrack/action_icons/PypeDoctor.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/SyncHierarchicalAttrsLocal.svg b/res/ftrack/action_icons/SyncHierarchicalAttrsLocal.svg deleted file mode 100644 index f58448ac06..0000000000 --- a/res/ftrack/action_icons/SyncHierarchicalAttrsLocal.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/res/ftrack/action_icons/SyncToAvalon-local.svg b/res/ftrack/action_icons/SyncToAvalon-local.svg deleted file mode 100644 index bf4708e8a5..0000000000 --- a/res/ftrack/action_icons/SyncToAvalon-local.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/res/ftrack/action_icons/Thumbnail.svg b/res/ftrack/action_icons/Thumbnail.svg new file mode 100644 index 0000000000..a8780b9a04 --- /dev/null +++ b/res/ftrack/action_icons/Thumbnail.svg @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/thumbToChildren.svg b/res/ftrack/action_icons/thumbToChildren.svg deleted file mode 100644 index 709b9549f3..0000000000 --- a/res/ftrack/action_icons/thumbToChildren.svg +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/res/ftrack/action_icons/thumbToParent.svg b/res/ftrack/action_icons/thumbToParent.svg deleted file mode 100644 index 3efa426332..0000000000 --- a/res/ftrack/action_icons/thumbToParent.svg +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/setup/nuke/nuke_path/write_to_read.py b/setup/nuke/nuke_path/write_to_read.py new file mode 100644 index 0000000000..9667dccab6 --- /dev/null +++ b/setup/nuke/nuke_path/write_to_read.py @@ -0,0 +1,141 @@ +import re +import os +import glob +import nuke +from pype import api as pype +log = pype.Logger().get_logger(__name__, "nuke") + +SINGLE_FILE_FORMATS = ['avi', 'mp4', 'mxf', 'mov', 'mpg', 'mpeg', 'wmv', 'm4v', + 'm2v'] + + +def evaluate_filepath_new(k_value, k_eval, project_dir, first_frame): + # get combined relative path + combined_relative_path = None + if k_eval is not None and project_dir is not None: + combined_relative_path = os.path.abspath( + os.path.join(project_dir, k_eval)) + combined_relative_path = combined_relative_path.replace('\\', '/') + filetype = combined_relative_path.split('.')[-1] + frame_number = re.findall(r'\d+', combined_relative_path)[-1] + basename = combined_relative_path[: combined_relative_path.rfind( + frame_number)] + filepath_glob = basename + '*' + filetype + glob_search_results = glob.glob(filepath_glob) + if len(glob_search_results) <= 0: + combined_relative_path = None + + try: + k_value = k_value % first_frame + if os.path.exists(k_value): + filepath = k_value + elif os.path.exists(k_eval): + filepath = k_eval + elif not isinstance(project_dir, type(None)) and \ + not isinstance(combined_relative_path, type(None)): + filepath = combined_relative_path + + filepath = os.path.abspath(filepath) + except Exception as E: + log.error("Cannot create Read node. Perhaps it needs to be rendered first :) Error: `{}`".format(E)) + return + + filepath = filepath.replace('\\', '/') + current_frame = re.findall(r'\d+', filepath)[-1] + padding = len(current_frame) + basename = filepath[: filepath.rfind(current_frame)] + filetype = filepath.split('.')[-1] + + # sequence or not? + if filetype in SINGLE_FILE_FORMATS: + pass + else: + # Image sequence needs hashes + filepath = basename + '#' * padding + '.' + filetype + + # relative path? make it relative again + if not isinstance(project_dir, type(None)): + filepath = filepath.replace(project_dir, '.') + + # get first and last frame from disk + frames = [] + firstframe = 0 + lastframe = 0 + filepath_glob = basename + '*' + filetype + glob_search_results = glob.glob(filepath_glob) + for f in glob_search_results: + frame = re.findall(r'\d+', f)[-1] + frames.append(frame) + frames = sorted(frames) + firstframe = frames[0] + lastframe = frames[len(frames) - 1] + if lastframe < 0: + lastframe = firstframe + + return filepath, firstframe, lastframe + + +def create_read_node(ndata, comp_start): + read = nuke.createNode('Read', 'file ' + ndata['filepath']) + read.knob('colorspace').setValue(int(ndata['colorspace'])) + read.knob('raw').setValue(ndata['rawdata']) + read.knob('first').setValue(int(ndata['firstframe'])) + read.knob('last').setValue(int(ndata['lastframe'])) + read.knob('origfirst').setValue(int(ndata['firstframe'])) + read.knob('origlast').setValue(int(ndata['lastframe'])) + if comp_start == int(ndata['firstframe']): + read.knob('frame_mode').setValue("1") + read.knob('frame').setValue(str(comp_start)) + else: + read.knob('frame_mode').setValue("0") + read.knob('xpos').setValue(ndata['new_xpos']) + read.knob('ypos').setValue(ndata['new_ypos']) + nuke.inputs(read, 0) + return + + +def write_to_read(gn): + comp_start = nuke.Root().knob('first_frame').value() + comp_end = nuke.Root().knob('last_frame').value() + project_dir = nuke.Root().knob('project_directory').getValue() + if not os.path.exists(project_dir): + project_dir = nuke.Root().knob('project_directory').evaluate() + + group_read_nodes = [] + + with gn: + height = gn.screenHeight() # get group height and position + new_xpos = int(gn.knob('xpos').value()) + new_ypos = int(gn.knob('ypos').value()) + height + 20 + group_writes = [n for n in nuke.allNodes() if n.Class() == "Write"] + print("__ group_writes: {}".format(group_writes)) + if group_writes != []: + # there can be only 1 write node, taking first + n = group_writes[0] + + if n.knob('file') is not None: + myfiletranslated, firstFrame, lastFrame = evaluate_filepath_new( + n.knob('file').getValue(), + n.knob('file').evaluate(), + project_dir, + comp_start + ) + # get node data + ndata = { + 'filepath': myfiletranslated, + 'firstframe': firstFrame, + 'lastframe': lastFrame, + 'new_xpos': new_xpos, + 'new_ypos': new_ypos, + 'colorspace': n.knob('colorspace').getValue(), + 'rawdata': n.knob('raw').value(), + 'write_frame_mode': str(n.knob('frame_mode').value()), + 'write_frame': n.knob('frame').value() + } + group_read_nodes.append(ndata) + + + # create reads in one go + for oneread in group_read_nodes: + # create read node + create_read_node(oneread, comp_start) diff --git a/setup/nukestudio/hiero_plugin_path/Icons/retiming.png b/setup/nukestudio/hiero_plugin_path/Icons/retiming.png new file mode 100644 index 0000000000..4487ac0422 Binary files /dev/null and b/setup/nukestudio/hiero_plugin_path/Icons/retiming.png differ diff --git a/setup/nukestudio/hiero_plugin_path/Icons/retiming.psd b/setup/nukestudio/hiero_plugin_path/Icons/retiming.psd new file mode 100644 index 0000000000..bac6fc6b58 Binary files /dev/null and b/setup/nukestudio/hiero_plugin_path/Icons/retiming.psd differ