diff --git a/pype/__init__.py b/pype/__init__.py index 755ffa9e7a..751faef320 100644 --- a/pype/__init__.py +++ b/pype/__init__.py @@ -10,10 +10,9 @@ import logging log = logging.getLogger(__name__) # # do not delete these are mandatory -# Anatomy = None -# Dataflow = None -# Metadata = None -# Colorspace = None +Anatomy = None +Dataflow = None +Colorspace = None PACKAGE_DIR = os.path.dirname(__file__) PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") diff --git a/pype/api.py b/pype/api.py index e42c58fab7..fcdcbce82b 100644 --- a/pype/api.py +++ b/pype/api.py @@ -17,12 +17,11 @@ from .action import ( from pypeapp import Logger -# from . import ( -# Anatomy, -# Colorspace, -# Metadata, -# Dataflow -# ) +from . import ( + Anatomy, + Colorspace, + Dataflow +) from .templates import ( load_data_from_templates, @@ -88,7 +87,6 @@ __all__ = [ # preloaded templates "Anatomy", "Colorspace", - "Metadata", "Dataflow", # QtWidgets diff --git a/pype/aport/api.py b/pype/aport/api.py index 42a090dc63..bac3e235df 100644 --- a/pype/aport/api.py +++ b/pype/aport/api.py @@ -56,7 +56,7 @@ def publish(json_data_path, gui): log.info("avalon.session is: \n{}".format(SESSION)) - pype_start = os.path.join(os.getenv('PYPE_SETUP_ROOT'), + pype_start = os.path.join(os.getenv('PYPE_ROOT'), "app", "pype-start.py") publish = "--publish-gui" if gui else "--publish" diff --git a/pype/aport/original/api.py b/pype/aport/original/api.py index b7ae447546..b1fffed1dc 100644 --- a/pype/aport/original/api.py +++ b/pype/aport/original/api.py @@ -56,7 +56,7 @@ def publish(json_data_path, staging_dir=None): return_json_path = os.path.join(staging_dir, "return_data.json") log.info("avalon.session is: \n{}".format(SESSION)) - pype_start = os.path.join(os.getenv('PYPE_SETUP_ROOT'), + pype_start = os.path.join(os.getenv('PYPE_ROOT'), "app", "pype-start.py") args = [pype_start, "--publish", diff --git a/pype/aport/pipeline.py b/pype/aport/original/pipeline.py similarity index 98% rename from pype/aport/pipeline.py rename to pype/aport/original/pipeline.py index 2c37695225..1bfd9a8d1e 100644 --- a/pype/aport/pipeline.py +++ b/pype/aport/original/pipeline.py @@ -55,7 +55,7 @@ def publish(json_data_path, staging_dir=None): return_json_path = os.path.join(staging_dir, "return_data.json") log.debug("avalon.session is: \n{}".format(SESSION)) - pype_start = os.path.join(os.getenv('PYPE_SETUP_ROOT'), + pype_start = os.path.join(os.getenv('PYPE_ROOT'), "app", "pype-start.py") args = [pype_start, "--publish", diff --git a/pype/aport/templates.py b/pype/aport/original/templates.py similarity index 100% rename from pype/aport/templates.py rename to pype/aport/original/templates.py diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py index 17be642be5..0b84bf3953 100644 --- a/pype/clockify/clockify.py +++ b/pype/clockify/clockify.py @@ -35,6 +35,28 @@ class ClockifyModule: self.set_menu_visibility() + def process_modules(self, modules): + if 'FtrackModule' in modules: + actions_path = os.path.sep.join([ + os.path.dirname(__file__), + 'ftrack_actions' + ]) + current = os.environ('FTRACK_ACTIONS_PATH', '') + if current: + current += os.pathsep + os.environ['FTRACK_ACTIONS_PATH'] = current + actions_path + + if 'AvalonApps' in modules: + from launcher import lib + actions_path = os.path.sep.join([ + os.path.dirname(__file__), + 'launcher_actions' + ]) + current = os.environ.get('AVALON_ACTIONS', '') + if current: + current += os.pathsep + os.environ['AVALON_ACTIONS'] = current + actions_path + def start_timer_check(self): self.bool_thread_check_running = True if self.thread_timer_check is None: diff --git a/pype/ftrack/actions/action_clockify_start.py b/pype/clockify/ftrack_actions/action_clockify_start.py similarity index 96% rename from pype/ftrack/actions/action_clockify_start.py rename to pype/clockify/ftrack_actions/action_clockify_start.py index 594ec21b78..e09d0b76e6 100644 --- a/pype/ftrack/actions/action_clockify_start.py +++ b/pype/clockify/ftrack_actions/action_clockify_start.py @@ -1,3 +1,4 @@ +import os import sys import argparse import logging @@ -17,7 +18,9 @@ class StartClockify(BaseAction): #: Action description. description = 'Starts timer on clockify' #: roles that are allowed to register this action - icon = 'https://clockify.me/assets/images/clockify-logo.png' + icon = '{}/app_icons/clockify.png'.format( + os.environ.get('PYPE_STATICS_SERVER', '') + ) #: Clockify api clockapi = ClockifyAPI() diff --git a/pype/ftrack/actions/action_clockify_sync.py b/pype/clockify/ftrack_actions/action_clockify_sync.py similarity index 97% rename from pype/ftrack/actions/action_clockify_sync.py rename to pype/clockify/ftrack_actions/action_clockify_sync.py index 4cc00225e2..695f7581c0 100644 --- a/pype/ftrack/actions/action_clockify_sync.py +++ b/pype/clockify/ftrack_actions/action_clockify_sync.py @@ -1,3 +1,4 @@ +import os import sys import argparse import logging @@ -21,7 +22,9 @@ class SyncClocify(BaseAction): #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] #: icon - icon = 'https://clockify.me/assets/images/clockify-logo-white.svg' + icon = '{}/app_icons/clockify-white.png'.format( + os.environ.get('PYPE_STATICS_SERVER', '') + ) #: CLockifyApi clockapi = ClockifyAPI() diff --git a/pype/plugins/launcher/actions/ClockifyStart.py b/pype/clockify/launcher_actions/ClockifyStart.py similarity index 86% rename from pype/plugins/launcher/actions/ClockifyStart.py rename to pype/clockify/launcher_actions/ClockifyStart.py index 9183805c7f..6a9ceaec73 100644 --- a/pype/plugins/launcher/actions/ClockifyStart.py +++ b/pype/clockify/launcher_actions/ClockifyStart.py @@ -1,9 +1,7 @@ from avalon import api, io from pype.api import Logger -try: - from pype.clockify import ClockifyAPI -except Exception: - pass +from pype.clockify import ClockifyAPI + log = Logger().get_logger(__name__, "clockify_start") @@ -14,13 +12,10 @@ class ClockifyStart(api.Action): label = "Clockify - Start Timer" icon = "clockify_icon" order = 500 - - exec("try: clockapi = ClockifyAPI()\nexcept: clockapi = None") + clockapi = ClockifyAPI() def is_compatible(self, session): """Return whether the action is compatible with the session""" - if self.clockapi is None: - return False if "AVALON_TASK" in session: return True return False diff --git a/pype/plugins/launcher/actions/ClockifySync.py b/pype/clockify/launcher_actions/ClockifySync.py similarity index 87% rename from pype/plugins/launcher/actions/ClockifySync.py rename to pype/clockify/launcher_actions/ClockifySync.py index 0895da555d..3bf389796f 100644 --- a/pype/plugins/launcher/actions/ClockifySync.py +++ b/pype/clockify/launcher_actions/ClockifySync.py @@ -1,8 +1,5 @@ from avalon import api, io -try: - from pype.clockify import ClockifyAPI -except Exception: - pass +from pype.clockify import ClockifyAPI from pype.api import Logger log = Logger().get_logger(__name__, "clockify_sync") @@ -13,16 +10,11 @@ class ClockifySync(api.Action): label = "Sync to Clockify" icon = "clockify_white_icon" order = 500 - exec( - "try:\n\tclockapi = ClockifyAPI()" - "\n\thave_permissions = clockapi.validate_workspace_perm()" - "\nexcept:\n\tclockapi = None" - ) + clockapi = ClockifyAPI() + have_permissions = clockapi.validate_workspace_perm() def is_compatible(self, session): """Return whether the action is compatible with the session""" - if self.clockapi is None: - return False return self.have_permissions def process(self, session, **kwargs): diff --git a/pype/clockify/widget_settings.py b/pype/clockify/widget_settings.py index ad92c299bb..7142548fa6 100644 --- a/pype/clockify/widget_settings.py +++ b/pype/clockify/widget_settings.py @@ -26,7 +26,7 @@ class ClockifySettings(QtWidgets.QWidget): elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'): self.setWindowIcon(self.parent.parent.icon) else: - pype_setup = os.getenv('PYPE_SETUP_ROOT') + pype_setup = os.getenv('PYPE_ROOT') items = [pype_setup, "app", "resources", "icon.png"] fname = os.path.sep.join(items) icon = QtGui.QIcon(fname) diff --git a/pype/ftrack/actions/action_application_loader.py b/pype/ftrack/actions/action_application_loader.py index 50714e4535..1b0f48f9be 100644 --- a/pype/ftrack/actions/action_application_loader.py +++ b/pype/ftrack/actions/action_application_loader.py @@ -1,3 +1,4 @@ +import os import toml import time from pype.ftrack import AppAction @@ -35,10 +36,15 @@ def registerApp(app, session): label = apptoml.get('ftrack_label', app.get('label', name)) icon = apptoml.get('ftrack_icon', None) description = apptoml.get('description', None) + preactions = apptoml.get('preactions', []) + + if icon: + icon = icon.format(os.environ.get('PYPE_STATICS_SERVER', '')) # register action AppAction( - session, label, name, executable, variant, icon, description + session, label, name, executable, variant, + icon, description, preactions ).register() diff --git a/pype/ftrack/actions/action_component_open.py b/pype/ftrack/actions/action_component_open.py index c40a04b2fd..d3213c555a 100644 --- a/pype/ftrack/actions/action_component_open.py +++ b/pype/ftrack/actions/action_component_open.py @@ -1,8 +1,8 @@ +import os import sys import argparse import logging import subprocess -import os from pype.vendor import ftrack_api from pype.ftrack import BaseAction @@ -15,9 +15,8 @@ class ComponentOpen(BaseAction): # Action label label = 'Open File' # Action icon - icon = ( - 'https://cdn4.iconfinder.com/data/icons/rcons-application/32/' - 'application_go_run-256.png' + icon = '{}/ftrack/action_icons/ComponentOpen.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): diff --git a/pype/ftrack/actions/action_create_cust_attrs.py b/pype/ftrack/actions/action_create_cust_attrs.py index 9f2564406a..7dd8335ecc 100644 --- a/pype/ftrack/actions/action_create_cust_attrs.py +++ b/pype/ftrack/actions/action_create_cust_attrs.py @@ -114,10 +114,8 @@ class CustomAttributes(BaseAction): description = 'Creates Avalon/Mongo ID for double check' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = ( - 'https://cdn4.iconfinder.com/data/icons/' - 'ios-web-user-interface-multi-circle-flat-vol-4/512/' - 'Bullet_list_menu_lines_points_items_options-512.png' + icon = '{}/ftrack/action_icons/CustomAttributes.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def __init__(self, session): diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py index 4426fb9650..2a777911b4 100644 --- a/pype/ftrack/actions/action_create_folders.py +++ b/pype/ftrack/actions/action_create_folders.py @@ -3,11 +3,9 @@ import sys import logging import argparse import re -# import json from pype.vendor import ftrack_api from pype.ftrack import BaseAction -# from pype import api as pype, lib as pypelib from avalon import lib as avalonlib from avalon.tools.libraryloader.io_nonsingleton import DbConnector from pypeapp import config, Anatomy @@ -24,10 +22,10 @@ class CreateFolders(BaseAction): label = 'Create Folders' #: Action Icon. - icon = ( - 'https://cdn1.iconfinder.com/data/icons/hawcons/32/' - '698620-icon-105-folder-add-512.png' + icon = '{}/ftrack/action_icons/CreateFolders.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) + db = DbConnector() def discover(self, session, entities, event): @@ -239,17 +237,6 @@ class CreateFolders(BaseAction): output.extend(self.get_notask_children(child)) return output - # def get_presets(self): - # fpath_items = [pypelib.get_presets_path(), 'tools', 'sw_folders.json'] - # filepath = os.path.normpath(os.path.sep.join(fpath_items)) - # presets = dict() - # try: - # with open(filepath) as data_file: - # presets = json.load(data_file) - # except Exception as e: - # self.log.warning('Wasn\'t able to load presets') - # return dict(presets) - def template_format(self, template, data): partial_data = PartialDict(data) diff --git a/pype/ftrack/actions/action_create_project_folders.py b/pype/ftrack/actions/action_create_project_folders.py index 66e2e153e6..3ccdb08714 100644 --- a/pype/ftrack/actions/action_create_project_folders.py +++ b/pype/ftrack/actions/action_create_project_folders.py @@ -20,9 +20,8 @@ class CreateProjectFolders(BaseAction): description = 'Creates folder structure' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = ( - 'https://cdn2.iconfinder.com/data/icons/' - 'buttons-9/512/Button_Add-01.png' + icon = '{}/ftrack/action_icons/CreateProjectFolders.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) pattern_array = re.compile('\[.*\]') @@ -42,7 +41,7 @@ class CreateProjectFolders(BaseAction): else: project = entity['project'] - presets = config.load_presets()['tools']['project_folder_structure'] + presets = config.get_presets()['tools']['project_folder_structure'] try: # Get paths based on presets basic_paths = self.get_path_items(presets) @@ -142,28 +141,6 @@ class CreateProjectFolders(BaseAction): self.session.commit() return new_ent - # def load_presets(self): - # preset_items = [ - # pypelib.get_presets_path(), - # 'tools', - # 'project_folder_structure.json' - # ] - # filepath = os.path.sep.join(preset_items) - # - # # Load folder structure template from presets - # presets = dict() - # try: - # with open(filepath) as data_file: - # presets = json.load(data_file) - # except Exception as e: - # msg = 'Unable to load Folder structure preset' - # self.log.warning(msg) - # return { - # 'success': False, - # 'message': msg - # } - # return presets - def get_path_items(self, in_dict): output = [] for key, value in in_dict.items(): diff --git a/pype/ftrack/actions/action_delete_asset.py b/pype/ftrack/actions/action_delete_asset.py index 838a77570f..eabadecee6 100644 --- a/pype/ftrack/actions/action_delete_asset.py +++ b/pype/ftrack/actions/action_delete_asset.py @@ -1,3 +1,4 @@ +import os import sys import logging from bson.objectid import ObjectId @@ -16,10 +17,8 @@ class DeleteAsset(BaseAction): label = 'Delete Asset/Subsets' #: Action description. description = 'Removes from Avalon with all childs and asset from Ftrack' - icon = ( - 'https://cdn4.iconfinder.com/data/icons/' - 'ios-web-user-interface-multi-circle-flat-vol-5/512/' - 'Delete_dustbin_empty_recycle_recycling_remove_trash-512.png' + icon = '{}/ftrack/action_icons/DeleteAsset.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] diff --git a/pype/ftrack/actions/action_delete_asset_byname.py b/pype/ftrack/actions/action_delete_asset_byname.py index 9da60ce763..fa966096a8 100644 --- a/pype/ftrack/actions/action_delete_asset_byname.py +++ b/pype/ftrack/actions/action_delete_asset_byname.py @@ -1,3 +1,4 @@ +import os import sys import logging import argparse @@ -17,10 +18,8 @@ class AssetsRemover(BaseAction): description = 'Removes assets from Ftrack and Avalon db with all childs' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = ( - 'https://cdn4.iconfinder.com/data/icons/' - 'ios-web-user-interface-multi-circle-flat-vol-5/512/' - 'Clipboard_copy_delete_minus_paste_remove-512.png' + icon = '{}/ftrack/action_icons/AssetsRemover.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) #: Db db = DbConnector() diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index 942aa7a327..e0c0334e5f 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -16,16 +16,16 @@ class DJVViewAction(BaseAction): identifier = "djvview-launch-action" label = "DJV View" description = "DJV View Launcher" - icon = "http://a.fsdn.com/allura/p/djv/icon" + icon = '{}/app_icons/djvView.png'.format( + os.environ.get('PYPE_STATICS_SERVER', '') + ) type = 'Application' def __init__(self, session): '''Expects a ftrack_api.Session instance''' super().__init__(session) self.djv_path = None - self.config_data = None - # self.load_config_data() self.config_data = config.get_presets()['djv_view']['config'] self.set_djv_path() @@ -54,22 +54,6 @@ class DJVViewAction(BaseAction): return True return False - def load_config_data(self): - # path_items = [pypelib.get_presets_path(), 'djv_view', 'config.json'] - path_items = config.get_presets()['djv_view']['config'] - filepath = os.path.sep.join(path_items) - - data = dict() - try: - with open(filepath) as data_file: - data = json.load(data_file) - except Exception as e: - log.warning( - 'Failed to load data from DJV presets file ({})'.format(e) - ) - - self.config_data = data - def set_djv_path(self): for path in self.config_data.get("djv_paths", []): if os.path.exists(path): diff --git a/pype/ftrack/actions/action_job_killer.py b/pype/ftrack/actions/action_job_killer.py index 25c0c6a489..44acb24d55 100644 --- a/pype/ftrack/actions/action_job_killer.py +++ b/pype/ftrack/actions/action_job_killer.py @@ -1,6 +1,8 @@ +import os import sys import argparse import logging +import json from pype.vendor import ftrack_api from pype.ftrack import BaseAction @@ -17,9 +19,8 @@ class JobKiller(BaseAction): description = 'Killing selected running jobs' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = ( - 'https://cdn2.iconfinder.com/data/icons/new-year-resolutions/64/' - 'resolutions-23-512.png' + icon = '{}/ftrack/action_icons/JobKiller.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): @@ -37,14 +38,18 @@ class JobKiller(BaseAction): ).all() items = [] - import json + item_splitter = {'type': 'label', 'value': '---'} for job in jobs: - data = json.loads(job['data']) + try: + data = json.loads(job['data']) + desctiption = data['description'] + except Exception: + desctiption = '*No description*' user = job['user']['username'] created = job['created_at'].strftime('%d.%m.%Y %H:%M:%S') label = '{} - {} - {}'.format( - data['description'], created, user + desctiption, created, user ) item_label = { 'type': 'label', diff --git a/pype/ftrack/actions/action_multiple_notes.py b/pype/ftrack/actions/action_multiple_notes.py index 2d93f64242..338083fe47 100644 --- a/pype/ftrack/actions/action_multiple_notes.py +++ b/pype/ftrack/actions/action_multiple_notes.py @@ -1,3 +1,4 @@ +import os import sys import argparse import logging @@ -15,9 +16,8 @@ class MultipleNotes(BaseAction): label = 'Multiple Notes' #: Action description. description = 'Add same note to multiple Asset Versions' - icon = ( - 'https://cdn2.iconfinder.com/data/icons/' - 'mixed-rounded-flat-icon/512/note_1-512.png' + icon = '{}/ftrack/action_icons/MultipleNotes.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): diff --git a/pype/ftrack/actions/action_rv.py b/pype/ftrack/actions/action_rv.py index 15689ae811..3832dffae4 100644 --- a/pype/ftrack/actions/action_rv.py +++ b/pype/ftrack/actions/action_rv.py @@ -17,7 +17,9 @@ class RVAction(BaseAction): identifier = "rv.launch.action" label = "rv" description = "rv Launcher" - icon = "https://img.icons8.com/color/48/000000/circled-play.png" + icon = '{}/ftrack/action_icons/RV.png'.format( + os.environ.get('PYPE_STATICS_SERVER', '') + ) type = 'Application' def __init__(self, session): @@ -39,7 +41,6 @@ class RVAction(BaseAction): ) else: # if not, fallback to config file location - # self.load_config_data() self.config_data = config.get_presets()['djv_view']['config'] self.set_rv_path() @@ -61,21 +62,6 @@ class RVAction(BaseAction): return True return False - def load_config_data(self): - path_items = config.get_presets['rv']['config.json'] - filepath = os.path.sep.join(path_items) - - data = dict() - try: - with open(filepath) as data_file: - data = json.load(data_file) - except Exception as e: - log.warning( - 'Failed to load data from RV presets file ({})'.format(e) - ) - - self.config_data = data - def set_rv_path(self): self.rv_path = self.config_data.get("rv_path") diff --git a/pype/ftrack/actions/action_start_timer.py b/pype/ftrack/actions/action_start_timer.py new file mode 100644 index 0000000000..d27908541e --- /dev/null +++ b/pype/ftrack/actions/action_start_timer.py @@ -0,0 +1,79 @@ +from pype.vendor import ftrack_api +from pype.ftrack import BaseAction + + +class StartTimer(BaseAction): + '''Starts timer.''' + + identifier = 'start.timer' + label = 'Start timer' + description = 'Starts timer' + + def discover(self, session, entities, event): + return False + + def _handle_result(*arg): + return + + def launch(self, session, entities, event): + entity = entities[0] + if entity.entity_type.lower() != 'task': + return + self.start_ftrack_timer(entity) + try: + self.start_clockify_timer(entity) + except Exception: + self.log.warning( + 'Failed starting Clockify timer for task: ' + entity['name'] + ) + return + + def start_ftrack_timer(self, task): + user_query = 'User where username is "{}"'.format(self.session.api_user) + user = self.session.query(user_query).one() + self.log.info('Starting Ftrack timer for task: ' + task['name']) + user.start_timer(task, force=True) + self.session.commit() + + def start_clockify_timer(self, task): + # Validate Clockify settings if Clockify is required + clockify_timer = os.environ.get('CLOCKIFY_WORKSPACE', None) + if clockify_timer is None: + return + + from pype.clockify import ClockifyAPI + clockapi = ClockifyAPI() + if clockapi.verify_api() is False: + return + task_type = task['type']['name'] + project_name = task['project']['full_name'] + + def get_parents(entity): + output = [] + if entity.entity_type.lower() == 'project': + return output + output.extend(get_parents(entity['parent'])) + output.append(entity['name']) + + return output + + desc_items = get_parents(task['parent']) + desc_items.append(task['name']) + description = '/'.join(desc_items) + + project_id = clockapi.get_project_id(project_name) + tag_ids = [] + tag_ids.append(clockapi.get_tag_id(task_type)) + clockapi.start_time_entry( + description, project_id, tag_ids=tag_ids + ) + self.log.info('Starting Clockify timer for task: ' + task['name']) + + +def register(session, **kw): + '''Register plugin. Called when used as an plugin.''' + + if not isinstance(session, ftrack_api.session.Session): + return + + StartTimer(session).register() diff --git a/pype/ftrack/actions/action_sync_to_avalon_local.py b/pype/ftrack/actions/action_sync_to_avalon_local.py index 1056b5ee55..54fd0b47f8 100644 --- a/pype/ftrack/actions/action_sync_to_avalon_local.py +++ b/pype/ftrack/actions/action_sync_to_avalon_local.py @@ -50,9 +50,8 @@ class SyncToAvalon(BaseAction): #: Action description. description = 'Send data from Ftrack to Avalon' #: Action icon. - icon = ( - 'https://cdn1.iconfinder.com/data/icons/hawcons/32/' - '699650-icon-92-inbox-download-512.png' + icon = '{}/ftrack/action_icons/SyncToAvalon-local.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) #: roles that are allowed to register this action role_list = ['Pypeclub'] diff --git a/pype/ftrack/actions/action_test.py b/pype/ftrack/actions/action_test.py index 36adb99074..dcb9dd32d0 100644 --- a/pype/ftrack/actions/action_test.py +++ b/pype/ftrack/actions/action_test.py @@ -1,8 +1,8 @@ +import os import sys import argparse import logging import collections -import os import json import re @@ -27,9 +27,8 @@ class TestAction(BaseAction): priority = 10000 #: roles that are allowed to register this action role_list = ['Pypeclub'] - icon = ( - 'https://cdn4.iconfinder.com/data/icons/hospital-19/512/' - '8_hospital-512.png' + icon = '{}/ftrack/action_icons/TestAction.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): diff --git a/pype/ftrack/actions/action_thumbToChildern.py b/pype/ftrack/actions/action_thumbToChildern.py index 5b63ec264f..4e7f1298f5 100644 --- a/pype/ftrack/actions/action_thumbToChildern.py +++ b/pype/ftrack/actions/action_thumbToChildern.py @@ -1,3 +1,4 @@ +import os import sys import argparse import logging @@ -15,9 +16,8 @@ class ThumbToChildren(BaseAction): # Action label label = 'Thumbnail to Children' # Action icon - icon = ( - 'https://cdn3.iconfinder.com/data/icons/transfers/100/' - '239322-download_transfer-128.png' + icon = '{}/ftrack/action_icons/thumbToChildren.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): diff --git a/pype/ftrack/actions/action_thumbToParent.py b/pype/ftrack/actions/action_thumbToParent.py index eb5623328e..632d2a50b2 100644 --- a/pype/ftrack/actions/action_thumbToParent.py +++ b/pype/ftrack/actions/action_thumbToParent.py @@ -1,3 +1,4 @@ +import os import sys import argparse import logging @@ -14,9 +15,8 @@ class ThumbToParent(BaseAction): # Action label label = 'Thumbnail to Parent' # Action icon - icon = ( - "https://cdn3.iconfinder.com/data/icons/transfers/100/" - "239419-upload_transfer-512.png" + icon = '{}/ftrack/action_icons/thumbToParent.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): diff --git a/pype/ftrack/actions/event_collect_entities.py b/pype/ftrack/actions/event_collect_entities.py deleted file mode 100644 index 71f2d26ff3..0000000000 --- a/pype/ftrack/actions/event_collect_entities.py +++ /dev/null @@ -1,72 +0,0 @@ -from pype.vendor import ftrack_api -from pype.ftrack import BaseEvent - - -class CollectEntities(BaseEvent): - - priority = 1 - - def _launch(self, event): - entities = self.translate_event(event) - event['data']['entities_object'] = entities - - return - - def translate_event(self, event): - selection = event['data'].get('selection', []) - - entities = list() - for entity in selection: - ent = self.session.get( - self.get_entity_type(entity), - entity.get('entityId') - ) - entities.append(ent) - - return entities - - def get_entity_type(self, entity): - '''Return translated entity type tht can be used with API.''' - # Get entity type and make sure it is lower cased. Most places except - # the component tab in the Sidebar will use lower case notation. - entity_type = entity.get('entityType').replace('_', '').lower() - - for schema in self.session.schemas: - alias_for = schema.get('alias_for') - - if ( - alias_for and isinstance(alias_for, str) and - alias_for.lower() == entity_type - ): - return schema['id'] - - for schema in self.session.schemas: - if schema['id'].lower() == entity_type: - return schema['id'] - - raise ValueError( - 'Unable to translate entity type: {0}.'.format(entity_type) - ) - - def register(self): - self.session.event_hub.subscribe( - 'topic=ftrack.action.discover' - ' and source.user.username={0}'.format(self.session.api_user), - self._launch, - priority=self.priority - ) - - self.session.event_hub.subscribe( - 'topic=ftrack.action.launch' - ' and source.user.username={0}'.format(self.session.api_user), - self._launch, - priority=self.priority - ) - - -def register(session, **kw): - '''Register plugin. Called when used as an plugin.''' - if not isinstance(session, ftrack_api.session.Session): - return - - CollectEntities(session).register() diff --git a/pype/ftrack/credentials.py b/pype/ftrack/credentials.py index 30d503c534..836af73c61 100644 --- a/pype/ftrack/credentials.py +++ b/pype/ftrack/credentials.py @@ -1,6 +1,6 @@ import os import json -import ftrack_api +from pype.vendor import ftrack_api import appdirs diff --git a/pype/ftrack/events/action_sync_to_avalon.py b/pype/ftrack/events/action_sync_to_avalon.py index 22358cd775..8a5be1c100 100644 --- a/pype/ftrack/events/action_sync_to_avalon.py +++ b/pype/ftrack/events/action_sync_to_avalon.py @@ -2,8 +2,8 @@ import os import sys import argparse import logging -import ftrack_api import json +from pype.vendor import ftrack_api from pype.ftrack import BaseAction, lib @@ -49,9 +49,8 @@ class Sync_To_Avalon(BaseAction): #: Action description. description = 'Send data from Ftrack to Avalon' #: Action icon. - icon = ( - 'https://cdn1.iconfinder.com/data/icons/hawcons/32/' - '699650-icon-92-inbox-download-512.png' + icon = '{}/ftrack/action_icons/SyncToAvalon.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def register(self): 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 7659191637..f27a329429 100644 --- a/pype/ftrack/events/event_del_avalon_id_from_new.py +++ b/pype/ftrack/events/event_del_avalon_id_from_new.py @@ -1,4 +1,4 @@ -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseEvent, get_ca_mongoid from pype.ftrack.events.event_sync_to_avalon import Sync_to_Avalon diff --git a/pype/ftrack/events/event_next_task_update.py b/pype/ftrack/events/event_next_task_update.py index e677e53fb2..1ae06050bc 100644 --- a/pype/ftrack/events/event_next_task_update.py +++ b/pype/ftrack/events/event_next_task_update.py @@ -1,4 +1,4 @@ -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseEvent import operator diff --git a/pype/ftrack/events/event_radio_buttons.py b/pype/ftrack/events/event_radio_buttons.py index f96d90307d..769115f045 100644 --- a/pype/ftrack/events/event_radio_buttons.py +++ b/pype/ftrack/events/event_radio_buttons.py @@ -1,4 +1,4 @@ -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseEvent diff --git a/pype/ftrack/events/event_sync_to_avalon.py b/pype/ftrack/events/event_sync_to_avalon.py index 1deaa3d17e..9dd7355d5e 100644 --- a/pype/ftrack/events/event_sync_to_avalon.py +++ b/pype/ftrack/events/event_sync_to_avalon.py @@ -1,4 +1,4 @@ -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseEvent, lib diff --git a/pype/ftrack/events/event_test.py b/pype/ftrack/events/event_test.py index e4da4cd44e..f6746f2535 100644 --- a/pype/ftrack/events/event_test.py +++ b/pype/ftrack/events/event_test.py @@ -1,7 +1,7 @@ import os import sys import re -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseEvent diff --git a/pype/ftrack/events/event_thumbnail_updates.py b/pype/ftrack/events/event_thumbnail_updates.py index 50089e26b8..042f6cc600 100644 --- a/pype/ftrack/events/event_thumbnail_updates.py +++ b/pype/ftrack/events/event_thumbnail_updates.py @@ -1,4 +1,4 @@ -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseEvent diff --git a/pype/ftrack/events/event_version_to_task_statuses.py b/pype/ftrack/events/event_version_to_task_statuses.py index d1393e622e..8b14e025d3 100644 --- a/pype/ftrack/events/event_version_to_task_statuses.py +++ b/pype/ftrack/events/event_version_to_task_statuses.py @@ -1,4 +1,4 @@ -import ftrack_api +from pype.vendor import ftrack_api from pype.ftrack import BaseEvent diff --git a/pype/ftrack/ftrack_module.py b/pype/ftrack/ftrack_module.py index 127b39d2fc..fdce0535e8 100644 --- a/pype/ftrack/ftrack_module.py +++ b/pype/ftrack/ftrack_module.py @@ -153,6 +153,7 @@ class FtrackModule: parent_menu.addMenu(self.menu) + def tray_start(self): self.validate() # Definition of visibility of each menu actions diff --git a/pype/ftrack/lib/ftrack_action_handler.py b/pype/ftrack/lib/ftrack_action_handler.py index c6d6181c1f..7a25155718 100644 --- a/pype/ftrack/lib/ftrack_action_handler.py +++ b/pype/ftrack/lib/ftrack_action_handler.py @@ -66,6 +66,10 @@ class BaseAction(BaseHandler): self.session, event ) + preactions_launched = self._handle_preactions(self.session, event) + if preactions_launched is False: + return + interface = self._interface( self.session, *args ) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index e4075e9a19..3c2bc418a8 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -23,10 +23,11 @@ class AppAction(BaseHandler): ''' type = 'Application' + preactions = ['start.timer'] def __init__( self, session, label, name, executable, - variant=None, icon=None, description=None + variant=None, icon=None, description=None, preactions=[] ): super().__init__(session) '''Expects a ftrack_api.Session instance''' @@ -44,6 +45,7 @@ class AppAction(BaseHandler): self.variant = variant self.icon = icon self.description = description + self.preactions.extend(preactions) def register(self): '''Registers the action, subscribing the discover and launch topics.''' @@ -117,6 +119,12 @@ class AppAction(BaseHandler): self.session, event ) + preactions_launched = self._handle_preactions( + self.session, event + ) + if preactions_launched is False: + return + response = self.launch( self.session, *args ) @@ -148,25 +156,6 @@ class AppAction(BaseHandler): entity = entities[0] project_name = entity['project']['full_name'] - # Validate Clockify settings if Clockify is required - clockify_timer = os.environ.get('CLOCKIFY_WORKSPACE', None) - if clockify_timer is not None: - from pype.clockify import ClockifyAPI - clockapi = ClockifyAPI() - if clockapi.verify_api() is False: - title = 'Launch message' - header = '# You Can\'t launch **any Application**' - message = ( - '

You don\'t have set Clockify API' - ' key in Clockify settings

' - ) - items = [ - {'type': 'label', 'value': header}, - {'type': 'label', 'value': message} - ] - self.show_interface(event, items, title) - return False - database = pypelib.get_avalon_database() # Get current environments @@ -335,39 +324,6 @@ class AppAction(BaseHandler): } pass - # RUN TIMER IN FTRACK - username = event['source']['user']['username'] - user_query = 'User where username is "{}"'.format(username) - user = session.query(user_query).one() - task = session.query('Task where id is {}'.format(entity['id'])).one() - self.log.info('Starting timer for task: ' + task['name']) - user.start_timer(task, force=True) - - # RUN TIMER IN Clockify - if clockify_timer is not None: - task_type = task['type']['name'] - project_name = task['project']['full_name'] - - def get_parents(entity): - output = [] - if entity.entity_type.lower() == 'project': - return output - output.extend(get_parents(entity['parent'])) - output.append(entity['name']) - - return output - - desc_items = get_parents(task['parent']) - desc_items.append(task['name']) - description = '/'.join(desc_items) - - project_id = clockapi.get_project_id(project_name) - tag_ids = [] - tag_ids.append(clockapi.get_tag_id(task_type)) - clockapi.start_time_entry( - description, project_id, tag_ids=tag_ids - ) - # Change status of task to In progress config = get_config_data() diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py index 7a04ba329c..63d4ff0ce9 100644 --- a/pype/ftrack/lib/ftrack_base_handler.py +++ b/pype/ftrack/lib/ftrack_base_handler.py @@ -1,7 +1,8 @@ import functools import time from pype import api as pype -import ftrack_api +from pype.vendor import ftrack_api +from pype.vendor.ftrack_api import session as fa_session class MissingPermision(Exception): @@ -25,6 +26,7 @@ class BaseHandler(object): priority = 100 # Type is just for logging purpose (e.g.: Action, Event, Application,...) type = 'No-type' + preactions = [] def __init__(self, session): '''Expects a ftrack_api.Session instance''' @@ -46,18 +48,7 @@ class BaseHandler(object): else: label = '{} {}'.format(self.label, self.variant) try: - if hasattr(self, "role_list") and len(self.role_list) > 0: - username = self.session.api_user - user = self.session.query( - 'User where username is "{}"'.format(username) - ).one() - available = False - for role in user['user_security_roles']: - if role['security_role']['name'] in self.role_list: - available = True - break - if available is False: - raise MissingPermision + self._preregister() start_time = time.perf_counter() func(*args, **kwargs) @@ -119,6 +110,36 @@ class BaseHandler(object): def reset_session(self): self.session.reset() + def _preregister(self): + if hasattr(self, "role_list") and len(self.role_list) > 0: + username = self.session.api_user + user = self.session.query( + 'User where username is "{}"'.format(username) + ).one() + available = False + for role in user['user_security_roles']: + if role['security_role']['name'] in self.role_list: + available = True + break + if available is False: + raise MissingPermision + + # Custom validations + result = self.preregister() + if result is True: + return + msg = "Pre-register conditions were not met" + if isinstance(result, str): + msg = result + raise Exception(msg) + + def preregister(self): + ''' + Preregister conditions. + Registration continues if returns True. + ''' + return True + def register(self): ''' Registers the action, subscribing the discover and launch topics. @@ -176,7 +197,9 @@ class BaseHandler(object): _entities = event['data'].get('entities_object', None) if ( _entities is None or - _entities[0].get('link', None) == ftrack_api.symbol.NOT_SET + _entities[0].get( + 'link', None + ) == fa_session.ftrack_api.symbol.NOT_SET ): _entities = self._get_entities(event) @@ -227,6 +250,10 @@ class BaseHandler(object): self.session, event ) + preactions_launched = self._handle_preactions(self.session, event) + if preactions_launched is False: + return + interface = self._interface( self.session, *args ) @@ -263,6 +290,47 @@ class BaseHandler(object): ''' raise NotImplementedError() + def _handle_preactions(self, session, event): + # If preactions are not set + if len(self.preactions) == 0: + return True + # If no selection + selection = event.get('data', {}).get('selection', None) + if (selection is None): + return False + # If preactions were already started + if event['data'].get('preactions_launched', None) is True: + return True + + # Launch preactions + for preaction in self.preactions: + event = fa_session.ftrack_api.event.base.Event( + topic='ftrack.action.launch', + data=dict( + actionIdentifier=preaction, + selection=selection + ), + source=dict( + user=dict(username=session.api_user) + ) + ) + session.event_hub.publish(event, on_error='ignore') + # Relaunch this action + event = fa_session.ftrack_api.event.base.Event( + topic='ftrack.action.launch', + data=dict( + actionIdentifier=self.identifier, + selection=selection, + preactions_launched=True + ), + source=dict( + user=dict(username=session.api_user) + ) + ) + session.event_hub.publish(event, on_error='ignore') + + return False + def _interface(self, *args): interface = self.interface(*args) if interface: @@ -349,7 +417,7 @@ class BaseHandler(object): 'applicationId=ftrack.client.web and user.id="{0}"' ).format(user_id) self.session.event_hub.publish( - ftrack_api.event.base.Event( + fa_session.ftrack_api.event.base.Event( topic='ftrack.action.trigger-user-interface', data=dict( type='message', @@ -372,7 +440,7 @@ class BaseHandler(object): ).format(user_id) self.session.event_hub.publish( - ftrack_api.event.base.Event( + fa_session.ftrack_api.event.base.Event( topic='ftrack.action.trigger-user-interface', data=dict( type='widget', diff --git a/pype/ftrack/login_dialog.py b/pype/ftrack/login_dialog.py index 04ebd59ae4..5520087032 100644 --- a/pype/ftrack/login_dialog.py +++ b/pype/ftrack/login_dialog.py @@ -28,7 +28,7 @@ class Login_Dialog_ui(QtWidgets.QWidget): elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'): self.setWindowIcon(self.parent.parent.icon) else: - pype_setup = os.getenv('PYPE_SETUP_ROOT') + pype_setup = os.getenv('PYPE_ROOT') items = [pype_setup, "app", "resources", "icon.png"] fname = os.path.sep.join(items) icon = QtGui.QIcon(fname) diff --git a/pype/maya/__init__.py b/pype/maya/__init__.py index 6b971c8bca..8bfc4c8ee5 100644 --- a/pype/maya/__init__.py +++ b/pype/maya/__init__.py @@ -2,14 +2,15 @@ import os import logging import weakref -from maya import utils, cmds, mel +from maya import utils, cmds from avalon import api as avalon, pipeline, maya from avalon.maya.pipeline import IS_HEADLESS +from avalon.tools import workfiles from pyblish import api as pyblish +from pypeapp import config from ..lib import ( - update_task_from_path, any_outdated ) from . import menu @@ -107,19 +108,39 @@ def on_init(_): # Force load objExport plug-in (requested by artists) cmds.loadPlugin("objExport", quiet=True) - # Force load objExport plug-in (requested by artists) - cmds.loadPlugin("spore", quiet=True) - from .customize import ( override_component_mask_commands, override_toolbox_ui ) safe_deferred(override_component_mask_commands) + launch_workfiles = True + try: + presets = config.get_presets() + launch_workfiles = presets['tools']['workfiles']['start_on_app_launch'] + except KeyError: + log.info( + "Workfiles app start on launch configuration was not found." + " Defaulting to False." + ) + launch_workfiles = False + + if launch_workfiles: + safe_deferred(launch_workfiles_app) + if not IS_HEADLESS: safe_deferred(override_toolbox_ui) +def launch_workfiles_app(*args): + workfiles.show( + os.path.join( + cmds.workspace(query=True, rootDirectory=True), + cmds.workspace(fileRuleEntry="scene") + ) + ) + + def on_before_save(return_code, _): """Run validation for scene's FPS prior to saving""" return lib.validate_fps() diff --git a/pype/nuke/__init__.py b/pype/nuke/__init__.py index cd568ed8a2..376e8f95b8 100644 --- a/pype/nuke/__init__.py +++ b/pype/nuke/__init__.py @@ -6,6 +6,7 @@ from pyblish import api as pyblish from .. import api from pype.nuke import menu +import logging from .lib import ( create_write_node @@ -44,40 +45,40 @@ if os.getenv("PYBLISH_GUI", None): pyblish.register_gui(os.getenv("PYBLISH_GUI", None)) -# class NukeHandler(Logger.logging.Handler): -# ''' -# Nuke Handler - emits logs into nuke's script editor. -# warning will emit nuke.warning() -# critical and fatal would popup msg dialog to alert of the error. -# ''' -# -# def __init__(self): -# api.Logger.logging.Handler.__init__(self) -# self.set_name("Pype_Nuke_Handler") -# -# def emit(self, record): -# # Formated message: -# msg = self.format(record) -# -# if record.levelname.lower() in [ -# # "warning", -# "critical", -# "fatal", -# "error" -# ]: -# nuke.message(msg) +class NukeHandler(logging.Handler): + ''' + Nuke Handler - emits logs into nuke's script editor. + warning will emit nuke.warning() + critical and fatal would popup msg dialog to alert of the error. + ''' -# -# '''Adding Nuke Logging Handler''' -# nuke_handler = NukeHandler() -# if nuke_handler.get_name() \ -# not in [handler.get_name() -# for handler in Logger.logging.root.handlers[:]]: -# api.Logger.logging.getLogger().addHandler(nuke_handler) -# api.Logger.logging.getLogger().setLevel(Logger.logging.INFO) -# -# if not self.nLogger: -# self.nLogger = Logger + def __init__(self): + logging.Handler.__init__(self) + self.set_name("Pype_Nuke_Handler") + + def emit(self, record): + # Formated message: + msg = self.format(record) + + if record.levelname.lower() in [ + # "warning", + "critical", + "fatal", + "error" + ]: + nuke.message(msg) + + +'''Adding Nuke Logging Handler''' +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) + +if not self.nLogger: + self.nLogger = Logger def reload_config(): @@ -113,11 +114,11 @@ def install(): # api.set_avalon_workdir() # reload_config() - import sys + # import sys - for path in sys.path: - if path.startswith("C:\\Users\\Public"): - sys.path.remove(path) + # for path in sys.path: + # if path.startswith("C:\\Users\\Public"): + # sys.path.remove(path) log.info("Registering Nuke plug-ins..") pyblish.register_plugin_path(PUBLISH_PATH) @@ -139,7 +140,7 @@ def install(): menu.install() # load data from templates - # api.load_data_from_templates() + api.load_data_from_templates() def uninstall(): diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index c5cd224e10..46b1d6e4c8 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -58,7 +58,8 @@ def writes_version_sync(): node_new_file = node_file.replace(node_version, new_version) each['file'].setValue(node_new_file) except Exception as e: - log.debug("Write node: `{}` has no version in path: {}".format(each.name(), e)) + log.debug( + "Write node: `{}` has no version in path: {}".format(each.name(), e)) def version_up_script(): @@ -74,7 +75,7 @@ def get_render_path(node): data_preset = { "class": data['avalon']['family'], "preset": data['avalon']['families'] - } + } nuke_dataflow_writes = get_dataflow(**data_preset) nuke_colorspace_writes = get_colorspace(**data_preset) @@ -87,7 +88,8 @@ def get_render_path(node): }) anatomy_filled = format_anatomy(data) - return anatomy_filled.render.path.replace("\\", "/") + return anatomy_filled["render"]["path"].replace("\\", "/") + def format_anatomy(data): from .templates import ( @@ -95,28 +97,29 @@ def format_anatomy(data): ) anatomy = get_anatomy() - + log.info("__ anatomy.templates: {}".format(anatomy.templates)) # TODO: perhaps should be in try! - padding = anatomy.render.padding + padding = int(anatomy.templates['render']['padding']) version = data.get("version", None) if not version: file = script_name() data["version"] = pype.get_version_from_path(file) data.update({ + "root": api.Session["AVALON_PROJECTS"], "subset": data["avalon"]["subset"], "asset": data["avalon"]["asset"], "task": str(pype.get_task()).lower(), "family": data["avalon"]["family"], "project": {"name": pype.get_project_name(), "code": pype.get_project_code()}, - "representation": data["nuke_dataflow_writes"].file_type, + "representation": data["nuke_dataflow_writes"]["file_type"], "app": data["application"]["application_dir"], "hierarchy": pype.get_hierarchy(), "frame": "#" * padding, }) - - # log.info("format_anatomy:anatomy: {}".format(anatomy)) + log.info("__ data: {}".format(data)) + log.info("__ format_anatomy: {}".format(anatomy.format(data))) return anatomy.format(data) @@ -141,10 +144,8 @@ def create_write_node(name, data): except Exception as e: log.error("problem with resolving anatomy tepmlate: {}".format(e)) - log.debug("anatomy_filled.render: {}".format(anatomy_filled.render)) - _data = OrderedDict({ - "file": str(anatomy_filled.render.path).replace("\\", "/") + "file": str(anatomy_filled["render"]["path"]).replace("\\", "/") }) # adding dataflow template @@ -161,7 +162,7 @@ def create_write_node(name, data): log.debug(_data) _data["frame_range"] = data.get("frame_range", None) - + log.info("__ _data3: {}".format(_data)) instance = avalon.nuke.lib.add_write_node( name, **_data @@ -170,6 +171,7 @@ def create_write_node(name, data): add_rendering_knobs(instance) return instance + def add_rendering_knobs(node): if "render" not in node.knobs(): knob = nuke.Boolean_Knob("render", "Render") @@ -195,8 +197,8 @@ def set_viewers_colorspace(viewer): erased_viewers = [] for v in viewers: - v['viewerProcess'].setValue(str(viewer.viewerProcess)) - if str(viewer.viewerProcess) not in v['viewerProcess'].value(): + v['viewerProcess'].setValue(str(viewer["viewerProcess"])) + if str(viewer["viewerProcess"]) not in v['viewerProcess'].value(): copy_inputs = v.dependencies() copy_knobs = {k: v[k].value() for k in v.knobs() if k not in filter_knobs} @@ -218,7 +220,7 @@ def set_viewers_colorspace(viewer): nv[k].setValue(v) # set viewerProcess - nv['viewerProcess'].setValue(str(viewer.viewerProcess)) + nv['viewerProcess'].setValue(str(viewer["viewerProcess"])) if erased_viewers: log.warning( @@ -229,6 +231,17 @@ def set_viewers_colorspace(viewer): def set_root_colorspace(root_dict): assert isinstance(root_dict, dict), log.error( "set_root_colorspace(): argument should be dictionary") + + # first set OCIO + if nuke.root()["colorManagement"].value() not in str(root_dict["colorManagement"]): + nuke.root()["colorManagement"].setValue( + str(root_dict["colorManagement"])) + + # second set ocio version + if nuke.root()["OCIO_config"].value() not in str(root_dict["OCIO_config"]): + nuke.root()["OCIO_config"].setValue(str(root_dict["OCIO_config"])) + + # then set the rest for knob, value in root_dict.items(): if nuke.root()[knob].value() not in value: nuke.root()[knob].setValue(str(value)) @@ -244,20 +257,20 @@ def set_writes_colorspace(write_dict): def set_colorspace(): from pype import api as pype - nuke_colorspace = getattr(pype.Colorspace, "nuke", None) + nuke_colorspace = pype.Colorspace.get("nuke", None) try: - set_root_colorspace(nuke_colorspace.root) + set_root_colorspace(nuke_colorspace["root"]) except AttributeError: log.error( "set_colorspace(): missing `root` settings in template") try: - set_viewers_colorspace(nuke_colorspace.viewer) + set_viewers_colorspace(nuke_colorspace["viewer"]) except AttributeError: log.error( "set_colorspace(): missing `viewer` settings in template") try: - set_writes_colorspace(nuke_colorspace.write) + set_writes_colorspace(nuke_colorspace["write"]) except AttributeError: log.error( "set_colorspace(): missing `write` settings in template") @@ -322,7 +335,7 @@ def reset_resolution(): check_format = used_formats[-1] format_name = "{}_{}".format( project["name"], - int(used_formats[-1].name()[-1])+1 + int(used_formats[-1].name()[-1]) + 1 ) log.info( "Format exists: {}. " @@ -440,7 +453,7 @@ def get_additional_data(container): def get_write_node_template_attr(node): ''' Gets all defined data from presets - + ''' # get avalon data from node data = dict() @@ -448,7 +461,7 @@ def get_write_node_template_attr(node): data_preset = { "class": data['avalon']['family'], "preset": data['avalon']['families'] - } + } # get template data nuke_dataflow_writes = get_dataflow(**data_preset) diff --git a/pype/nuke/templates.py b/pype/nuke/templates.py index 4be350cbcb..b3de6970d0 100644 --- a/pype/nuke/templates.py +++ b/pype/nuke/templates.py @@ -15,10 +15,12 @@ def get_dataflow(**kwarg): assert any([host, cls]), log.error("nuke.templates.get_dataflow():" "Missing mandatory kwargs `host`, `cls`") - nuke_dataflow = getattr(pype.Dataflow, str(host), None) - nuke_dataflow_node = getattr(nuke_dataflow.nodes, str(cls), None) + nuke_dataflow = pype.Dataflow.get(str(host), None) + nuke_dataflow_nodes = nuke_dataflow.get('nodes', None) + nuke_dataflow_node = nuke_dataflow_nodes.get(str(cls), None) + if preset: - nuke_dataflow_node = getattr(nuke_dataflow_node, str(preset), None) + nuke_dataflow_node = nuke_dataflow_node.get(str(preset), None) log.info("Dataflow: {}".format(nuke_dataflow_node)) return nuke_dataflow_node @@ -32,10 +34,10 @@ def get_colorspace(**kwarg): assert any([host, cls]), log.error("nuke.templates.get_colorspace():" "Missing mandatory kwargs `host`, `cls`") - nuke_colorspace = getattr(pype.Colorspace, str(host), None) - nuke_colorspace_node = getattr(nuke_colorspace, str(cls), None) + nuke_colorspace = pype.Colorspace.get(str(host), None) + nuke_colorspace_node = nuke_colorspace.get(str(cls), None) if preset: - nuke_colorspace_node = getattr(nuke_colorspace_node, str(preset), None) + nuke_colorspace_node = nuke_colorspace_node.get(str(preset), None) log.info("Colorspace: {}".format(nuke_colorspace_node)) return nuke_colorspace_node diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py index e166af2954..75d9b6db15 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py @@ -26,15 +26,9 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): 'render': 'render', 'nukescript': 'comp', 'review': 'mov'} - exclude = [] def process(self, instance): - for ex in self.exclude: - if ex in instance.data['families']: - return - self.log.debug('instance {}'.format(instance)) - assumed_data = instance.data["assumedTemplateData"] assumed_version = assumed_data["version"] version_number = int(assumed_version) @@ -60,8 +54,6 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): self.log.debug('dest ext: ' + ext) thumbnail = False - - if ext in ['.mov']: if not instance.data.get('startFrameReview'): instance.data['startFrameReview'] = instance.data['startFrame'] @@ -70,12 +62,13 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): location = ft_session.query( 'Location where name is "ftrack.server"').one() component_data = { - "name": "ftrackreview-mp4", # Default component name is "main". + # Default component name is "main". + "name": "ftrackreview-mp4", "metadata": {'ftr_meta': json.dumps({ 'frameIn': int(instance.data['startFrameReview']), 'frameOut': int(instance.data['startFrameReview']), 'frameRate': 25})} - } + } elif ext in [".jpg", ".jpeg"]: component_data = { "name": "thumbnail" # Default component name is "main". diff --git a/pype/plugins/global/load/open_file.py b/pype/plugins/global/load/open_file.py new file mode 100644 index 0000000000..9425eaab04 --- /dev/null +++ b/pype/plugins/global/load/open_file.py @@ -0,0 +1,58 @@ +import sys +import os +import subprocess + +from avalon import api + + +def open(filepath): + """Open file with system default executable""" + if sys.platform.startswith('darwin'): + subprocess.call(('open', filepath)) + elif os.name == 'nt': + os.startfile(filepath) + elif os.name == 'posix': + subprocess.call(('xdg-open', filepath)) + + +class Openfile(api.Loader): + """Open Image Sequence with system default""" + + families = ["write"] + representations = ["*"] + + label = "Open" + order = -10 + icon = "play-circle" + color = "orange" + + def load(self, context, name, namespace, data): + from avalon.vendor import clique + + directory = os.path.dirname(self.fname) + pattern = clique.PATTERNS["frames"] + + files = os.listdir(directory) + representation = context["representation"] + + ext = representation["name"] + path = representation["data"]["path"] + + if ext in ["#"]: + collections, remainder = clique.assemble(files, + patterns=[pattern], + minimum_items=1) + + seqeunce = collections[0] + + first_image = list(seqeunce)[0] + filepath = os.path.normpath(os.path.join(directory, first_image)) + else: + file = [f for f in files + if ext in f + if "#" not in f][0] + filepath = os.path.normpath(os.path.join(directory, file)) + + self.log.info("Opening : {}".format(filepath)) + + open(filepath) diff --git a/pype/plugins/global/load/open_imagesequence.py b/pype/plugins/global/load/open_imagesequence.py deleted file mode 100644 index a910625733..0000000000 --- a/pype/plugins/global/load/open_imagesequence.py +++ /dev/null @@ -1,49 +0,0 @@ -import sys -import os -import subprocess - -from avalon import api - - -def open(filepath): - """Open file with system default executable""" - if sys.platform.startswith('darwin'): - subprocess.call(('open', filepath)) - elif os.name == 'nt': - os.startfile(filepath) - elif os.name == 'posix': - subprocess.call(('xdg-open', filepath)) - - -class PlayImageSequence(api.Loader): - """Open Image Sequence with system default""" - - families = ["write"] - representations = ["*"] - - label = "Play sequence" - order = -10 - icon = "play-circle" - color = "orange" - - def load(self, context, name, namespace, data): - - directory = self.fname - from avalon.vendor import clique - - pattern = clique.PATTERNS["frames"] - files = os.listdir(directory) - collections, remainder = clique.assemble(files, - patterns=[pattern], - minimum_items=1) - - assert not remainder, ("There shouldn't have been a remainder for " - "'%s': %s" % (directory, remainder)) - - seqeunce = collections[0] - first_image = list(seqeunce)[0] - filepath = os.path.normpath(os.path.join(directory, first_image)) - - self.log.info("Opening : {}".format(filepath)) - - open(filepath) diff --git a/pype/plugins/global/publish/collect_assumed_destination.py b/pype/plugins/global/publish/collect_assumed_destination.py index 7458db6aa7..fa6a3d9423 100644 --- a/pype/plugins/global/publish/collect_assumed_destination.py +++ b/pype/plugins/global/publish/collect_assumed_destination.py @@ -4,14 +4,18 @@ import pyblish.api from avalon import io, api -class CollectAssumedDestination(pyblish.api.InstancePlugin): +class CollectAssumedDestination(pyblish.api.ContextPlugin): """Generate the assumed destination path where the file will be stored""" label = "Collect Assumed Destination" order = pyblish.api.CollectorOrder + 0.498 exclude_families = ["clip"] - def process(self, instance): + def process(self, context): + for instance in context: + self.process_item(instance) + + def process_item(self, instance): if [ef for ef in self.exclude_families if instance.data["family"] in ef]: return @@ -19,7 +23,6 @@ class CollectAssumedDestination(pyblish.api.InstancePlugin): self.create_destination_template(instance) template_data = instance.data["assumedTemplateData"] - template = instance.data["template"] anatomy = instance.context.data['anatomy'] # self.log.info(anatomy.anatomy()) diff --git a/pype/plugins/global/publish/integrate_rendered_frames.py b/pype/plugins/global/publish/integrate_rendered_frames.py index 8e7e2a59c4..e814e31640 100644 --- a/pype/plugins/global/publish/integrate_rendered_frames.py +++ b/pype/plugins/global/publish/integrate_rendered_frames.py @@ -195,7 +195,7 @@ class IntegrateFrames(pyblish.api.InstancePlugin): template_data["frame"] = src_collection.format( "{padding}") % i anatomy_filled = anatomy.format(template_data) - test_dest_files.append(anatomy_filled.render.path) + test_dest_files.append(anatomy_filled["render"]["path"]) dst_collections, remainder = clique.assemble(test_dest_files) dst_collection = dst_collections[0] @@ -223,7 +223,6 @@ class IntegrateFrames(pyblish.api.InstancePlugin): # template_data.pop("frame", None) - anatomy.pop("frame", None) fname = files @@ -239,15 +238,21 @@ class IntegrateFrames(pyblish.api.InstancePlugin): src = os.path.join(stagingdir, fname) anatomy_filled = anatomy.format(template_data) - dst = anatomy_filled.render.path + dst = anatomy_filled["render"]["path"] instance.data["transfers"].append([src, dst]) - template_data["frame"] = "#" * anatomy.render.padding + if ext[1:] not in ["jpeg", "jpg", "mov", "mp4", "wav"]: + template_data["frame"] = "#" * int(anatomy_filled["render"]["padding"]) + anatomy_filled = anatomy.format(template_data) - path_to_save = anatomy_filled.render.path - template = anatomy.render.fullpath - self.log.debug('ext[1:]: {}'.format(ext[1:])) + path_to_save = anatomy_filled["render"]["path"] + template = anatomy.templates["render"]["path"] + + self.log.debug("path_to_save: {}".format(path_to_save)) + + + representation = { "schema": "pype:representation-2.0", diff --git a/pype/plugins/launcher/actions/AssetCreator.py b/pype/plugins/launcher/actions/AssetCreator.py index 579edebcea..9787aae002 100644 --- a/pype/plugins/launcher/actions/AssetCreator.py +++ b/pype/plugins/launcher/actions/AssetCreator.py @@ -1,9 +1,4 @@ -import os -import sys -import acre - from avalon import api, lib -from pype.tools import assetcreator from pype.api import Logger @@ -19,9 +14,23 @@ class AssetCreator(api.Action): def is_compatible(self, session): """Return whether the action is compatible with the session""" - if "AVALON_PROJECT" in session: - return True - return False + compatible = True + + # Check required modules. + module_names = [ + "ftrack_api", "ftrack_api_old", "pype.tools.assetcreator" + ] + for name in module_names: + try: + __import__(name) + except ImportError: + compatible = False + + # Check session environment. + if "AVALON_PROJECT" not in session: + compatible = False + + return compatible def process(self, session, **kwargs): asset = '' diff --git a/pype/plugins/nuke/create/create_write.py b/pype/plugins/nuke/create/create_write.py index 2c9ff42f98..b3c9117641 100644 --- a/pype/plugins/nuke/create/create_write.py +++ b/pype/plugins/nuke/create/create_write.py @@ -35,7 +35,7 @@ class CrateWriteRender(avalon.nuke.Creator): data = OrderedDict() - data["family"] = self.family + data["family"] = self.family.split("_")[-1] data["families"] = self.families {data.update({k: v}) for k, v in self.data.items() @@ -103,48 +103,51 @@ class CrateWritePrerender(avalon.nuke.Creator): create_write_node(self.data["subset"], write_data) return - - -class CrateWriteStill(avalon.nuke.Creator): - # change this to template preset - preset = "still" - - name = "WriteStill" - label = "Create Write Still" - hosts = ["nuke"] - family = "{}_write".format(preset) - families = preset - icon = "image" - - def __init__(self, *args, **kwargs): - super(CrateWriteStill, self).__init__(*args, **kwargs) - - data = OrderedDict() - - data["family"] = self.family - data["families"] = self.families - - {data.update({k: v}) for k, v in self.data.items() - if k not in data.keys()} - self.data = data - - def process(self): - self.name = self.data["subset"] - - instance = nuke.toNode(self.data["subset"]) - - family = self.family - node = 'write' - - if not instance: - write_data = { - "frame_range": [nuke.frame(), nuke.frame()], - "class": node, - "preset": self.preset, - "avalon": self.data - } - - nuke.createNode("FrameHold", "first_frame {}".format(nuke.frame())) - create_write_node(self.data["subset"], write_data) - - return +# +# +# class CrateWriteStill(avalon.nuke.Creator): +# # change this to template preset +# preset = "still" +# +# name = "WriteStill" +# label = "Create Write Still" +# hosts = ["nuke"] +# family = "{}_write".format(preset) +# families = preset +# icon = "image" +# +# def __init__(self, *args, **kwargs): +# super(CrateWriteStill, self).__init__(*args, **kwargs) +# +# data = OrderedDict() +# +# data["family"] = self.family.split("_")[-1] +# data["families"] = self.families +# +# {data.update({k: v}) for k, v in self.data.items() +# if k not in data.keys()} +# self.data = data +# +# def process(self): +# self.name = self.data["subset"] +# +# node_name = self.data["subset"].replace( +# "_", "_f{}_".format(nuke.frame())) +# instance = nuke.toNode(self.data["subset"]) +# self.data["subset"] = node_name +# +# family = self.family +# node = 'write' +# +# if not instance: +# write_data = { +# "frame_range": [nuke.frame(), nuke.frame()], +# "class": node, +# "preset": self.preset, +# "avalon": self.data +# } +# +# nuke.createNode("FrameHold", "first_frame {}".format(nuke.frame())) +# create_write_node(node_name, write_data) +# +# return diff --git a/pype/plugins/nuke/load/load_sequence.py b/pype/plugins/nuke/load/load_sequence.py index 45cd6e616e..b4e3cfb8b5 100644 --- a/pype/plugins/nuke/load/load_sequence.py +++ b/pype/plugins/nuke/load/load_sequence.py @@ -8,7 +8,7 @@ import avalon.io as io import nuke from pype.api import Logger -log = Logger.get_looger(__name__, "nuke") +log = Logger().get_logger(__name__, "nuke") @contextlib.contextmanager @@ -88,8 +88,6 @@ class LoadSequence(api.Loader): containerise, viewer_update_and_undo_stop ) - # for k, v in context.items(): - # log.info("key: `{}`, value: {}\n".format(k, v)) version = context['version'] version_data = version.get("data", {}) @@ -137,12 +135,14 @@ class LoadSequence(api.Loader): data_imprint.update({k: context["version"]['data'][k]}) data_imprint.update({"objectName": read_name}) + r["tile_color"].setValue(int("0x4ecd25ff", 16)) + return containerise(r, - name=name, - namespace=namespace, - context=context, - loader=self.__class__.__name__, - data=data_imprint) + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + data=data_imprint) def switch(self, container, representation): self.update(container, representation) @@ -150,18 +150,17 @@ class LoadSequence(api.Loader): def update(self, container, representation): """Update the Loader's path - Fusion automatically tries to reset some variables when changing + Nuke automatically tries to reset some variables when changing the loader's path to a new file. These automatic changes are to its inputs: """ from avalon.nuke import ( - viewer_update_and_undo_stop, ls_img_sequence, update_container ) - log.info("this i can see") + node = nuke.toNode(container['objectName']) # TODO: prepare also for other Read img/geo/camera assert node.Class() == "Read", "Must be Read" @@ -170,8 +169,19 @@ class LoadSequence(api.Loader): file = ls_img_sequence(os.path.dirname(root), one=True) # Get start frame from version data - version = io.find_one({"type": "version", - "_id": representation["parent"]}) + version = io.find_one({ + "type": "version", + "_id": representation["parent"] + }) + + # get all versions in list + versions = io.find({ + "type": "version", + "parent": version["parent"] + }).distinct('name') + + max_version = max(versions) + start = version["data"].get("startFrame") if start is None: log.warning("Missing start frame for updated version" @@ -179,24 +189,44 @@ class LoadSequence(api.Loader): "{} ({})".format(node['name'].value(), representation)) start = 0 - with viewer_update_and_undo_stop(): + # Update the loader's path whilst preserving some values + with preserve_trim(node): + node["file"].setValue(file["path"]) + log.info("__ node['file']: {}".format(node["file"])) - # Update the loader's path whilst preserving some values - with preserve_trim(node): - node["file"].setValue(file["path"]) + # Set the global in to the start frame of the sequence + global_in_changed = loader_shift(node, start, relative=False) + if global_in_changed: + # Log this change to the user + log.debug("Changed '{}' global in:" + " {:d}".format(node['name'].value(), start)) - # Set the global in to the start frame of the sequence - global_in_changed = loader_shift(node, start, relative=False) - if global_in_changed: - # Log this change to the user - log.debug("Changed '{}' global in:" - " {:d}".format(node['name'].value(), start)) + updated_dict = {} + updated_dict.update({ + "representation": str(representation["_id"]), + "startFrame": start, + "endFrame": version["data"].get("endFrame"), + "version": version.get("name"), + "colorspace": version["data"].get("colorspace"), + "source": version["data"].get("source"), + "handles": version["data"].get("handles"), + "fps": version["data"].get("fps"), + "author": version["data"].get("author"), + "outputDir": version["data"].get("outputDir"), + }) - # Update the imprinted representation - update_container( - node, - {"representation": str(representation["_id"])} - ) + # change color of node + if version.get("name") not in [max_version]: + node["tile_color"].setValue(int("0xd84f20ff", 16)) + else: + node["tile_color"].setValue(int("0x4ecd25ff", 16)) + + # Update the imprinted representation + update_container( + node, + updated_dict + ) + log.info("udated to version: {}".format(version.get("name"))) def remove(self, container): diff --git a/pype/plugins/nuke/publish/collect_families.py b/pype/plugins/nuke/publish/collect_families.py index 08ab90143d..d7515f91ca 100644 --- a/pype/plugins/nuke/publish/collect_families.py +++ b/pype/plugins/nuke/publish/collect_families.py @@ -18,7 +18,7 @@ class CollectInstanceFamilies(pyblish.api.InstancePlugin): families = [] if instance.data.get('families'): - families.append(instance.data['families']) + families += instance.data['families'] # set for ftrack to accept # instance.data["families"] = ["ftrack"] @@ -36,10 +36,8 @@ class CollectInstanceFamilies(pyblish.api.InstancePlugin): families.append('ftrack') - instance.data["families"] = families - # Sort/grouped by family (preserving local index) instance.context[:] = sorted(instance.context, key=self.sort_by_family) diff --git a/pype/plugins/nuke/publish/collect_writes.py b/pype/plugins/nuke/publish/collect_writes.py index 612c2a8775..ce37774ac9 100644 --- a/pype/plugins/nuke/publish/collect_writes.py +++ b/pype/plugins/nuke/publish/collect_writes.py @@ -1,13 +1,9 @@ import os -import tempfile import nuke import pyblish.api -import logging import pype.api as pype -log = logging.get_logger(__name__) - @pyblish.api.log class CollectNukeWrites(pyblish.api.ContextPlugin): @@ -65,9 +61,6 @@ class CollectNukeWrites(pyblish.api.ContextPlugin): int(last_frame) ) - # preredered frames - # collect frames by try - # collect families in next file if "files" not in instance.data: instance.data["files"] = list() try: @@ -89,8 +82,6 @@ class CollectNukeWrites(pyblish.api.ContextPlugin): "colorspace": node["colorspace"].value(), }) - - self.log.debug("instance.data: {}".format(instance.data)) self.log.debug("context: {}".format(context)) diff --git a/pype/plugins/nuke/publish/extract_render_local.py b/pype/plugins/nuke/publish/extract_render_local.py index 1f0a00273f..5ac1c77059 100644 --- a/pype/plugins/nuke/publish/extract_render_local.py +++ b/pype/plugins/nuke/publish/extract_render_local.py @@ -17,18 +17,11 @@ class NukeRenderLocal(pype.api.Extractor): order = pyblish.api.ExtractorOrder label = "Render Local" hosts = ["nuke"] - families = ["render.local", "prerender.local", "still.local"] + families = ["render.local"] def process(self, instance): node = instance[0] - # This should be a ContextPlugin, but this is a workaround - # for a bug in pyblish to run once for a family: issue #250 context = instance.context - key = "__hasRun{}".format(self.__class__.__name__) - if context.data.get(key, False): - return - else: - context.data[key] = True self.log.debug("instance collected: {}".format(instance.data)) @@ -70,8 +63,9 @@ class NukeRenderLocal(pype.api.Extractor): collections, remainder = clique.assemble(*instance.data['files']) self.log.info('collections: {}'.format(str(collections))) - collection = collections[0] - instance.data['collection'] = collection + if collections: + collection = collections[0] + instance.data['collection'] = collection self.log.info('Finished render') return diff --git a/pype/plugins/nuke/publish/extract_review.py b/pype/plugins/nuke/publish/extract_review.py index e85185e919..16fb07a3fc 100644 --- a/pype/plugins/nuke/publish/extract_review.py +++ b/pype/plugins/nuke/publish/extract_review.py @@ -28,21 +28,29 @@ class ExtractDataForReview(pype.api.Extractor): self.log.debug("creating staging dir:") self.staging_dir(instance) - self.render_review_representation(instance, - representation="mov") - self.log.debug("review mov:") - self.transcode_mov(instance) - self.render_review_representation(instance, - representation="jpeg") + self.log.debug("instance: {}".format(instance)) + self.log.debug("instance.data[families]: {}".format( + instance.data["families"])) + + if "still" not in instance.data["families"]: + self.render_review_representation(instance, + representation="mov") + self.log.debug("review mov:") + self.transcode_mov(instance) + self.log.debug("instance.data: {}".format(instance.data)) + self.render_review_representation(instance, + representation="jpeg") + else: + self.log.debug("instance: {}".format(instance)) + self.render_review_representation(instance, representation="jpeg") + # Restore selection [i["selected"].setValue(False) for i in nuke.allNodes()] [i["selected"].setValue(True) for i in selection] def transcode_mov(self, instance): - import subprocess - collection = instance.data["collection"] - staging_dir = instance.data["stagingDir"] + staging_dir = instance.data["stagingDir"].replace("\\", "/") file_name = collection.format("{head}mov") review_mov = os.path.join(staging_dir, file_name).replace("\\", "/") @@ -53,13 +61,16 @@ class ExtractDataForReview(pype.api.Extractor): out, err = ( ffmpeg .input(input_movie) - .output(review_mov, pix_fmt='yuv420p', crf=18, timecode="00:00:00:01") + .output( + review_mov, + pix_fmt='yuv420p', + crf=18, + timecode="00:00:00:01" + ) .overwrite_output() .run() ) - - self.log.debug("Removing `{0}`...".format( instance.data["baked_colorspace_movie"])) os.remove(instance.data["baked_colorspace_movie"]) @@ -72,23 +83,32 @@ class ExtractDataForReview(pype.api.Extractor): assert instance.data['files'], "Instance data files should't be empty!" - import clique import nuke temporary_nodes = [] - staging_dir = instance.data["stagingDir"] + staging_dir = instance.data["stagingDir"].replace("\\", "/") + self.log.debug("StagingDir `{0}`...".format(staging_dir)) collection = instance.data.get("collection", None) - # Create nodes - first_frame = min(collection.indexes) - last_frame = max(collection.indexes) + if collection: + # get path + fname = os.path.basename(collection.format( + "{head}{padding}{tail}")) + fhead = collection.format("{head}") + + # get first and last frame + first_frame = min(collection.indexes) + last_frame = max(collection.indexes) + else: + fname = os.path.basename(instance.data.get("path", None)) + fhead = os.path.splitext(fname)[0] + "." + first_frame = instance.data.get("startFrame", None) + last_frame = instance.data.get("endFrame", None) node = previous_node = nuke.createNode("Read") node["file"].setValue( - os.path.join(staging_dir, - os.path.basename(collection.format( - "{head}{padding}{tail}"))).replace("\\", "/")) + os.path.join(staging_dir, fname).replace("\\", "/")) node["first"].setValue(first_frame) node["origfirst"].setValue(first_frame) @@ -126,7 +146,7 @@ class ExtractDataForReview(pype.api.Extractor): write_node = nuke.createNode("Write") if representation in "mov": - file = collection.format("{head}baked.mov") + file = fhead + "baked.mov" path = os.path.join(staging_dir, file).replace("\\", "/") self.log.debug("Path: {}".format(path)) instance.data["baked_colorspace_movie"] = path @@ -137,7 +157,7 @@ class ExtractDataForReview(pype.api.Extractor): temporary_nodes.append(write_node) elif representation in "jpeg": - file = collection.format("{head}jpeg") + file = fhead + "jpeg" path = os.path.join(staging_dir, file).replace("\\", "/") instance.data["thumbnail"] = path write_node["file"].setValue(path) @@ -147,8 +167,8 @@ class ExtractDataForReview(pype.api.Extractor): temporary_nodes.append(write_node) # retime for - first_frame = int(last_frame)/2 - last_frame = int(last_frame)/2 + first_frame = int(last_frame) / 2 + last_frame = int(last_frame) / 2 # add into files for integration as representation instance.data["files"].append(file) diff --git a/pype/plugins/nuke/publish/validate_script.py b/pype/plugins/nuke/publish/validate_script.py index 5939083a61..ad4a83b32f 100644 --- a/pype/plugins/nuke/publish/validate_script.py +++ b/pype/plugins/nuke/publish/validate_script.py @@ -28,7 +28,7 @@ class ValidateScript(pyblish.api.InstancePlugin): ] # Value of these attributes can be found on parents - hierarchical_attributes = ["fps"] + hierarchical_attributes = ["fps", "resolution_width", "resolution_height", "pixel_aspect"] missing_attributes = [] asset_attributes = {} @@ -80,6 +80,7 @@ class ValidateScript(pyblish.api.InstancePlugin): # Compare asset's values Nukescript X Database not_matching = [] for attr in attributes: + self.log.debug("asset vs script attribute: {0}, {1}".format(asset_attributes[attr], script_attributes[attr])) if asset_attributes[attr] != script_attributes[attr]: not_matching.append(attr) diff --git a/pype/services/idle_manager/__init__.py b/pype/services/idle_manager/__init__.py index 7c07d3ebee..f1a87bef41 100644 --- a/pype/services/idle_manager/__init__.py +++ b/pype/services/idle_manager/__init__.py @@ -2,6 +2,4 @@ from .idle_manager import IdleManager def tray_init(tray_widget, main_widget): - manager = IdleManager() - manager.start() - return manager + return IdleManager() diff --git a/pype/services/idle_manager/idle_manager.py b/pype/services/idle_manager/idle_manager.py index e8ba246121..57b827a37e 100644 --- a/pype/services/idle_manager/idle_manager.py +++ b/pype/services/idle_manager/idle_manager.py @@ -17,8 +17,17 @@ class IdleManager(QtCore.QThread): super(IdleManager, self).__init__() self.log = Logger().get_logger(self.__class__.__name__) self.signal_reset_timer.connect(self._reset_time) + self.qaction = None + self.failed_icon = None self._is_running = False + def set_qaction(self, qaction, failed_icon): + self.qaction = qaction + self.failed_icon = failed_icon + + def tray_start(self): + self.start() + def add_time_signal(self, emit_time, signal): """ If any module want to use IdleManager, need to use add_time_signal :param emit_time: time when signal will be emitted @@ -47,19 +56,27 @@ class IdleManager(QtCore.QThread): thread_mouse.start() thread_keyboard = KeyboardThread(self.signal_reset_timer) thread_keyboard.start() - while self._is_running: - self.idle_time += 1 - if self.idle_time in self.time_signals: - for signal in self.time_signals[self.idle_time]: - signal.emit() - time.sleep(1) + try: + while self.is_running: + self.idle_time += 1 + if self.idle_time in self.time_signals: + for signal in self.time_signals[self.idle_time]: + signal.emit() + time.sleep(1) + except Exception: + self.log.warning( + 'Idle Manager service has failed', exc_info=True + ) + if self.qaction and self.failed_icon: + self.qaction.setIcon(self.failed_icon) thread_mouse.signal_stop.emit() thread_mouse.terminate() thread_mouse.wait() thread_keyboard.signal_stop.emit() thread_keyboard.terminate() thread_keyboard.wait() + self._is_running = False self.log.info('IdleManager has stopped') diff --git a/pype/services/statics_server/__init__.py b/pype/services/statics_server/__init__.py new file mode 100644 index 0000000000..4b2721b18b --- /dev/null +++ b/pype/services/statics_server/__init__.py @@ -0,0 +1,5 @@ +from .statics_server import StaticsServer + + +def tray_init(tray_widget, main_widget): + return StaticsServer() diff --git a/pype/services/statics_server/statics_server.py b/pype/services/statics_server/statics_server.py new file mode 100644 index 0000000000..8655cd9df9 --- /dev/null +++ b/pype/services/statics_server/statics_server.py @@ -0,0 +1,202 @@ +import os +import sys +import datetime +import socket +import http.server +from http import HTTPStatus +import urllib +import posixpath +import socketserver + +from Qt import QtCore +from pypeapp import config, Logger + + +DIRECTORY = os.path.sep.join([os.environ['PYPE_MODULE_ROOT'], 'res']) + + +class Handler(http.server.SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + py_version = sys.version.split('.') + # If python version is 3.7 or higher + if int(py_version[0]) >= 3 and int(py_version[1]) >= 7: + super().__init__(*args, directory=DIRECTORY, **kwargs) + else: + self.directory = DIRECTORY + super().__init__(*args, **kwargs) + + def send_head(self): + """Common code for GET and HEAD commands. + + This sends the response code and MIME headers. + + Return value is either a file object (which has to be copied + to the outputfile by the caller unless the command was HEAD, + and must be closed by the caller under all circumstances), or + None, in which case the caller has nothing further to do. + + """ + path = self.translate_path(self.path) + f = None + if os.path.isdir(path): + parts = urllib.parse.urlsplit(self.path) + if not parts.path.endswith('/'): + # redirect browser - doing basically what apache does + self.send_response(HTTPStatus.MOVED_PERMANENTLY) + new_parts = (parts[0], parts[1], parts[2] + '/', + parts[3], parts[4]) + new_url = urllib.parse.urlunsplit(new_parts) + self.send_header("Location", new_url) + self.end_headers() + return None + for index in "index.html", "index.htm": + index = os.path.join(path, index) + if os.path.exists(index): + path = index + break + else: + return self.list_directory(path) + ctype = self.guess_type(path) + try: + f = open(path, 'rb') + except OSError: + self.send_error(HTTPStatus.NOT_FOUND, "File not found") + return None + + try: + fs = os.fstat(f.fileno()) + # Use browser cache if possible + if ("If-Modified-Since" in self.headers + and "If-None-Match" not in self.headers): + # compare If-Modified-Since and time of last file modification + try: + ims = http.server.email.utils.parsedate_to_datetime( + self.headers["If-Modified-Since"]) + except (TypeError, IndexError, OverflowError, ValueError): + # ignore ill-formed values + pass + else: + if ims.tzinfo is None: + # obsolete format with no timezone, cf. + # https://tools.ietf.org/html/rfc7231#section-7.1.1.1 + ims = ims.replace(tzinfo=datetime.timezone.utc) + if ims.tzinfo is datetime.timezone.utc: + # compare to UTC datetime of last modification + last_modif = datetime.datetime.fromtimestamp( + fs.st_mtime, datetime.timezone.utc) + # remove microseconds, like in If-Modified-Since + last_modif = last_modif.replace(microsecond=0) + + if last_modif <= ims: + self.send_response(HTTPStatus.NOT_MODIFIED) + self.end_headers() + f.close() + return None + + self.send_response(HTTPStatus.OK) + self.send_header("Content-type", ctype) + self.send_header("Content-Length", str(fs[6])) + self.send_header("Last-Modified", + self.date_time_string(fs.st_mtime)) + self.end_headers() + return f + except: + f.close() + raise + + def translate_path(self, path): + """Translate a /-separated PATH to the local filename syntax. + + Components that mean special things to the local file system + (e.g. drive or directory names) are ignored. (XXX They should + probably be diagnosed.) + + """ + # abandon query parameters + path = path.split('?',1)[0] + path = path.split('#',1)[0] + # Don't forget explicit trailing slash when normalizing. Issue17324 + trailing_slash = path.rstrip().endswith('/') + try: + path = urllib.parse.unquote(path, errors='surrogatepass') + except UnicodeDecodeError: + path = urllib.parse.unquote(path) + path = posixpath.normpath(path) + words = path.split('/') + words = filter(None, words) + path = self.directory + for word in words: + if os.path.dirname(word) or word in (os.curdir, os.pardir): + # Ignore components that are not a simple file/directory name + continue + path = os.path.join(path, word) + if trailing_slash: + path += '/' + return path + + +class StaticsServer(QtCore.QThread): + """ Measure user's idle time in seconds. + Idle time resets on keyboard/mouse input. + Is able to emit signals at specific time idle. + """ + def __init__(self): + super(StaticsServer, self).__init__() + self.qaction = None + self.failed_icon = None + self._is_running = False + self.log = Logger().get_logger(self.__class__.__name__) + try: + self.presets = config.get_presets().get( + 'services', {}).get('statics_server') + except Exception: + self.presets = {'default_port': 8010, 'exclude_ports': []} + + self.port = self.find_port() + + def set_qaction(self, qaction, failed_icon): + self.qaction = qaction + self.failed_icon = failed_icon + + def tray_start(self): + self.start() + + @property + def is_running(self): + return self._is_running + + def stop(self): + self._is_running = False + + def run(self): + self._is_running = True + try: + with socketserver.TCPServer(("", self.port), Handler) as httpd: + while self._is_running: + httpd.handle_request() + except Exception: + self.log.warning( + 'Statics Server service has failed', exc_info=True + ) + self._is_running = False + if self.qaction and self.failed_icon: + self.qaction.setIcon(self.failed_icon) + + def find_port(self): + start_port = self.presets['default_port'] + exclude_ports = self.presets['exclude_ports'] + found_port = None + # port check takes time so it's lowered to 100 ports + for port in range(start_port, start_port+100): + if port in exclude_ports: + continue + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + result = sock.connect_ex(('localhost', port)) + if result != 0: + found_port = port + if found_port is not None: + break + if found_port is None: + return None + os.environ['PYPE_STATICS_SERVER'] = 'http://localhost:{}'.format(found_port) + return found_port diff --git a/pype/templates.py b/pype/templates.py index 071426859a..7d12801a00 100644 --- a/pype/templates.py +++ b/pype/templates.py @@ -4,7 +4,7 @@ import sys from avalon import io, api as avalon, lib as avalonlib from . import lib # from pypeapp.api import (Templates, Logger, format) -from pypeapp import Logger +from pypeapp import Logger, config, Anatomy log = Logger().get_logger(__name__, os.getenv("AVALON_APP", "pype-config")) @@ -19,7 +19,7 @@ def set_session(): def load_data_from_templates(): """ - Load Templates `contextual` data as singleton object + Load Presets and Anatomy `contextual` data as singleton object [info](https://en.wikipedia.org/wiki/Singleton_pattern) Returns: @@ -31,17 +31,29 @@ def load_data_from_templates(): if not any([ api.Dataflow, api.Anatomy, - api.Colorspace, - api.Metadata + api.Colorspace ] ): - # base = Templates() - t = Templates(type=["anatomy", "metadata", "dataflow", "colorspace"]) - api.Anatomy = t.anatomy - api.Metadata = t.metadata.format() - data = {"metadata": api.Metadata} - api.Dataflow = t.dataflow.format(data) - api.Colorspace = t.colorspace + presets = config.get_presets() + anatomy = Anatomy() + + try: + # try if it is not in projects custom directory + # `{PYPE_PROJECT_CONFIGS}/[PROJECT_NAME]/init.json` + # init.json define preset names to be used + p_init = presets["init"] + colorspace = presets["colorspace"][p_init["colorspace"]] + dataflow = presets["dataflow"][p_init["dataflow"]] + except KeyError: + log.warning("No projects custom preset available...") + colorspace = presets["colorspace"]["default"] + dataflow = presets["dataflow"]["default"] + log.info("Presets `colorspace` and `dataflow` loaded from `default`...") + + api.Anatomy = anatomy + api.Dataflow = dataflow + api.Colorspace = colorspace + log.info("Data from templates were Loaded...") @@ -59,7 +71,6 @@ def reset_data_from_templates(): api.Dataflow = None api.Anatomy = None api.Colorspace = None - api.Metadata = None log.info("Data from templates were Unloaded...") @@ -283,11 +294,12 @@ def get_workdir_template(data=None): load_data_from_templates() anatomy = api.Anatomy + anatomy_filled = anatomy.format(data or get_context_data()) try: - work = anatomy.work.format(data or get_context_data()) + work = anatomy_filled["work"] except Exception as e: log.error("{0} Error in " "get_workdir_template(): {1}".format(__name__, e)) - return os.path.join(work.root, work.folder) + return work["folder"] diff --git a/res/app_icons/Aport.png b/res/app_icons/Aport.png new file mode 100644 index 0000000000..0a6816513a Binary files /dev/null and b/res/app_icons/Aport.png differ diff --git a/res/app_icons/clockify-white.png b/res/app_icons/clockify-white.png new file mode 100644 index 0000000000..2803049fbe Binary files /dev/null and b/res/app_icons/clockify-white.png differ diff --git a/res/app_icons/clockify.png b/res/app_icons/clockify.png new file mode 100644 index 0000000000..ac4c44c763 Binary files /dev/null and b/res/app_icons/clockify.png differ diff --git a/res/app_icons/djvView.png b/res/app_icons/djvView.png new file mode 100644 index 0000000000..854604d57f Binary files /dev/null and b/res/app_icons/djvView.png differ diff --git a/res/app_icons/houdini.png b/res/app_icons/houdini.png new file mode 100644 index 0000000000..11cfa46dce Binary files /dev/null and b/res/app_icons/houdini.png differ diff --git a/res/app_icons/maya.png b/res/app_icons/maya.png new file mode 100644 index 0000000000..e84a6a3742 Binary files /dev/null and b/res/app_icons/maya.png differ diff --git a/res/app_icons/nuke.png b/res/app_icons/nuke.png new file mode 100644 index 0000000000..4234454096 Binary files /dev/null and b/res/app_icons/nuke.png differ diff --git a/res/app_icons/premiere.png b/res/app_icons/premiere.png new file mode 100644 index 0000000000..eb5b3d1ba2 Binary files /dev/null and b/res/app_icons/premiere.png differ diff --git a/res/app_icons/python.png b/res/app_icons/python.png new file mode 100644 index 0000000000..b3b5b2220a Binary files /dev/null and b/res/app_icons/python.png differ diff --git a/res/ftrack/action_icons/AssetsRemover.svg b/res/ftrack/action_icons/AssetsRemover.svg new file mode 100644 index 0000000000..e838ee9f28 --- /dev/null +++ b/res/ftrack/action_icons/AssetsRemover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/ftrack/action_icons/ComponentOpen.svg b/res/ftrack/action_icons/ComponentOpen.svg new file mode 100644 index 0000000000..6d4eba6839 --- /dev/null +++ b/res/ftrack/action_icons/ComponentOpen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/ftrack/action_icons/CreateFolders.svg b/res/ftrack/action_icons/CreateFolders.svg new file mode 100644 index 0000000000..c07e474e5c --- /dev/null +++ b/res/ftrack/action_icons/CreateFolders.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/CreateProjectFolders.svg b/res/ftrack/action_icons/CreateProjectFolders.svg new file mode 100644 index 0000000000..5fa653361e --- /dev/null +++ b/res/ftrack/action_icons/CreateProjectFolders.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/CustomAttributes.svg b/res/ftrack/action_icons/CustomAttributes.svg new file mode 100644 index 0000000000..6d73746ed0 --- /dev/null +++ b/res/ftrack/action_icons/CustomAttributes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/ftrack/action_icons/DeleteAsset.svg b/res/ftrack/action_icons/DeleteAsset.svg new file mode 100644 index 0000000000..a41ae31d12 --- /dev/null +++ b/res/ftrack/action_icons/DeleteAsset.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/ftrack/action_icons/JobKiller.svg b/res/ftrack/action_icons/JobKiller.svg new file mode 100644 index 0000000000..595c780a9b --- /dev/null +++ b/res/ftrack/action_icons/JobKiller.svg @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/MultipleNotes.svg b/res/ftrack/action_icons/MultipleNotes.svg new file mode 100644 index 0000000000..6ed916f1aa --- /dev/null +++ b/res/ftrack/action_icons/MultipleNotes.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/RV.png b/res/ftrack/action_icons/RV.png new file mode 100644 index 0000000000..741e7a9772 Binary files /dev/null and b/res/ftrack/action_icons/RV.png differ diff --git a/res/ftrack/action_icons/SyncToAvalon-local.svg b/res/ftrack/action_icons/SyncToAvalon-local.svg new file mode 100644 index 0000000000..bf4708e8a5 --- /dev/null +++ b/res/ftrack/action_icons/SyncToAvalon-local.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/ftrack/action_icons/SyncToAvalon.svg b/res/ftrack/action_icons/SyncToAvalon.svg new file mode 100644 index 0000000000..48071b2430 --- /dev/null +++ b/res/ftrack/action_icons/SyncToAvalon.svg @@ -0,0 +1,67 @@ + + + + + + diff --git a/res/ftrack/action_icons/TestAction.svg b/res/ftrack/action_icons/TestAction.svg new file mode 100644 index 0000000000..771644340e --- /dev/null +++ b/res/ftrack/action_icons/TestAction.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/thumbToChildren.svg b/res/ftrack/action_icons/thumbToChildren.svg new file mode 100644 index 0000000000..30b146803e --- /dev/null +++ b/res/ftrack/action_icons/thumbToChildren.svg @@ -0,0 +1,88 @@ + + + + + + diff --git a/res/ftrack/action_icons/thumbToParent.svg b/res/ftrack/action_icons/thumbToParent.svg new file mode 100644 index 0000000000..254b650306 --- /dev/null +++ b/res/ftrack/action_icons/thumbToParent.svg @@ -0,0 +1,95 @@ + + + + + +