class TimersManager(metaclass=Singleton): """ Handles about Timers. Should be able to start/stop all timers at once. If IdleManager is imported then is able to handle about stop timers when user idles for a long time (set in presets). """ # Presetable attributes # - when timer will stop if idle manager is running (minutes) full_time = 15 # - how many minutes before the timer is stopped will popup the message message_time = 0.5 def __init__(self, tray_widget, main_widget): self.log = Logger().get_logger(self.__class__.__name__) self.modules = [] self.is_running = False self.last_task = None self.tray_widget = tray_widget self.main_widget = main_widget self.idle_man = None self.signal_handler = None self.widget_user_idle = WidgetUserIdle(self, tray_widget) def set_signal_times(self): try: full_time = int(self.full_time * 60) message_time = int(self.message_time * 60) self.time_show_message = full_time - message_time self.time_stop_timer = full_time return True except Exception: self.log.error("Couldn't set timer signals.", exc_info=True) def add_module(self, module): """ Adds module to context Module must have implemented methods: - ``start_timer_manager(data)`` - ``stop_timer_manager()`` """ self.modules.append(module) def start_timers(self, data): ''' :param data: basic information needed to start any timer :type data: dict ..note:: Dictionary "data" should contain: - project_name(str) - Name of Project - hierarchy(list/tuple) - list of parents(except project) - task_type(str) - task_name(str) Example: - to run timers for task in 'C001_BackToPast/assets/characters/villian/Lookdev BG' - input data should contain: .. code-block:: Python data = { 'project_name': 'C001_BackToPast', 'hierarchy': ['assets', 'characters', 'villian'], 'task_type': 'lookdev', 'task_name': 'Lookdev BG' } ''' if len(data['hierarchy']) < 1: self.log.error(( 'Not allowed action in Pype!!' ' Timer has been launched on task which is child of Project.')) return self.last_task = data for module in self.modules: module.start_timer_manager(data) self.is_running = True def restart_timers(self): if self.last_task is not None: self.start_timers(self.last_task) def stop_timers(self): if self.is_running is False: return self.widget_user_idle.bool_not_stopped = False self.widget_user_idle.refresh_context() for module in self.modules: module.stop_timer_manager() self.is_running = False def process_modules(self, modules): """ Gives ability to connect with imported modules from TrayManager. :param modules: All imported modules from TrayManager :type modules: dict """ if 'IdleManager' in modules: self.signal_handler = SignalHandler(self) if self.set_signal_times() is True: self.register_to_idle_manager(modules['IdleManager']) def time_callback(self, int_def): if not self.signal_handler: return if int_def == 0: self.signal_handler.signal_show_message.emit() elif int_def == 1: self.signal_handler.signal_change_label.emit() elif int_def == 2: self.signal_handler.signal_stop_timers.emit() def register_to_idle_manager(self, man_obj): self.idle_man = man_obj # Time when message is shown self.idle_man.add_time_callback(self.time_show_message, lambda: self.time_callback(0)) # Times when idle is between show widget and stop timers show_to_stop_range = range(self.time_show_message - 1, self.time_stop_timer) for num in show_to_stop_range: self.idle_man.add_time_callback(num, lambda: self.time_callback(1)) # Times when widget is already shown and user restart idle shown_and_moved_range = range(self.time_stop_timer - self.time_show_message) for num in shown_and_moved_range: self.idle_man.add_time_callback(num, lambda: self.time_callback(1)) # Time when timers are stopped self.idle_man.add_time_callback(self.time_stop_timer, lambda: self.time_callback(2)) def change_label(self): if self.is_running is False: return if not self.idle_man or self.widget_user_idle.bool_is_showed is False: return if self.idle_man.idle_time > self.time_show_message: value = self.time_stop_timer - self.idle_man.idle_time else: value = 1 + (self.time_stop_timer - self.time_show_message - self.idle_man.idle_time) self.widget_user_idle.change_count_widget(value) def show_message(self): if self.is_running is False: return if self.widget_user_idle.bool_is_showed is False: self.widget_user_idle.show()
class SocketThread(threading.Thread): """Thread that checks suprocess of storer of processor of events""" MAX_TIMEOUT = int(os.environ.get("PYPE_FTRACK_SOCKET_TIMEOUT", 45)) def __init__(self, name, port, filepath, additional_args=[]): super(SocketThread, self).__init__() self.log = Logger().get_logger(self.__class__.__name__) self.setName(name) self.name = name self.port = port self.filepath = filepath self.additional_args = additional_args self.sock = None self.subproc = None self.connection = None self._is_running = False self.finished = False self.mongo_error = False self._temp_data = {} def stop(self): self._is_running = False def run(self): self._is_running = True time_socket = time.time() # Create a TCP/IP socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock = sock # Bind the socket to the port - skip already used ports while True: try: server_address = ("localhost", self.port) sock.bind(server_address) break except OSError: self.port += 1 self.log.debug( "Running Socked thread on {}:{}".format(*server_address)) env = os.environ.copy() env["PYPE_PROCESS_MONGO_ID"] = str(Logger.mongo_process_id) # Pype executable (with path to start script if not build) args = get_pype_execute_args( # Add `run` command "run", self.filepath, *self.additional_args, str(self.port)) self.subproc = subprocess.Popen(args, env=env, stdin=subprocess.PIPE) # Listen for incoming connections sock.listen(1) sock.settimeout(1.0) while True: if not self._is_running: break try: connection, client_address = sock.accept() time_socket = time.time() connection.settimeout(1.0) self.connection = connection except socket.timeout: if (time.time() - time_socket) > self.MAX_TIMEOUT: self.log.error("Connection timeout passed. Terminating.") self._is_running = False self.subproc.terminate() break continue try: time_con = time.time() # Receive the data in small chunks and retransmit it while True: try: if not self._is_running: break data = None try: data = self.get_data_from_con(connection) time_con = time.time() except socket.timeout: if (time.time() - time_con) > self.MAX_TIMEOUT: self.log.error( "Connection timeout passed. Terminating.") self._is_running = False self.subproc.terminate() break continue except ConnectionResetError: self._is_running = False break self._handle_data(connection, data) except Exception as exc: self.log.error("Event server process failed", exc_info=True) finally: # Clean up the connection connection.close() if self.subproc.poll() is None: self.subproc.terminate() self.finished = True def get_data_from_con(self, connection): return connection.recv(16) def _handle_data(self, connection, data): if not data: return if data == b"MongoError": self.mongo_error = True connection.sendall(data)
class UnrealPrelaunch(PypeHook): """ This hook will check if current workfile path has Unreal project inside. IF not, it initialize it and finally it pass path to the project by environment variable to Unreal launcher shell script. """ def __init__(self, logger=None): if not logger: self.log = Logger().get_logger(self.__class__.__name__) else: self.log = logger self.signature = "( {} )".format(self.__class__.__name__) def execute(self, *args, env: dict = None) -> bool: if not env: env = os.environ asset = env["AVALON_ASSET"] task = env["AVALON_TASK"] workdir = env["AVALON_WORKDIR"] engine_version = env["AVALON_APP_NAME"].split("_")[-1] project_name = f"{asset}_{task}" # Unreal is sensitive about project names longer then 20 chars if len(project_name) > 20: self.log.warning((f"Project name exceed 20 characters " f"({project_name})!")) # Unreal doesn't accept non alphabet characters at the start # of the project name. This is because project name is then used # in various places inside c++ code and there variable names cannot # start with non-alpha. We append 'P' before project name to solve it. # 😱 if not project_name[:1].isalpha(): self.log.warning(f"Project name doesn't start with alphabet " f"character ({project_name}). Appending 'P'") project_name = f"P{project_name}" project_path = os.path.join(workdir, project_name) self.log.info((f"{self.signature} requested UE4 version: " f"[ {engine_version} ]")) detected = unreal_lib.get_engine_versions() detected_str = ', '.join(detected.keys()) or 'none' self.log.info((f"{self.signature} detected UE4 versions: " f"[ {detected_str} ]")) del (detected_str) engine_version = ".".join(engine_version.split(".")[:2]) if engine_version not in detected.keys(): self.log.error((f"{self.signature} requested version not " f"detected [ {engine_version} ]")) return False os.makedirs(project_path, exist_ok=True) project_file = os.path.join(project_path, f"{project_name}.uproject") engine_path = detected[engine_version] if not os.path.isfile(project_file): self.log.info((f"{self.signature} creating unreal " f"project [ {project_name} ]")) if env.get("AVALON_UNREAL_PLUGIN"): os.environ["AVALON_UNREAL_PLUGIN"] = env.get( "AVALON_UNREAL_PLUGIN") # noqa: E501 unreal_lib.create_unreal_project(project_name, engine_version, project_path, engine_path=engine_path) env["PYPE_UNREAL_PROJECT_FILE"] = project_file env["AVALON_CURRENT_UNREAL_ENGINE"] = engine_path return True
class TrayManager: """Cares about context of application. Load submenus, actions, separators and modules into tray's context. """ available_sourcetypes = ["python", "file"] def __init__(self, tray_widget, main_window): self.tray_widget = tray_widget self.main_window = main_window self.log = Logger().get_logger(self.__class__.__name__) self.modules = {} self.services = {} self.services_submenu = None self.errors = [] CURRENT_DIR = os.path.dirname(__file__) self.modules_imports = config.load_json( os.path.join(CURRENT_DIR, "modules_imports.json")) presets = config.get_presets(first_run=True) menu_items = presets["tray"]["menu_items"] try: self.modules_usage = menu_items["item_usage"] except Exception: self.modules_usage = {} self.log.critical("Couldn't find modules usage data.") self.module_attributes = menu_items.get("attributes") or {} self.icon_run = QtGui.QIcon( resources.get_resource("icons", "circle_green.png")) self.icon_stay = QtGui.QIcon( resources.get_resource("icons", "circle_orange.png")) self.icon_failed = QtGui.QIcon( resources.get_resource("icons", "circle_red.png")) def process_presets(self): """Add modules to tray by presets. This is start up method for TrayManager. Loads presets and import modules described in "menu_items.json". In `item_usage` key you can specify by item's title or import path if you want to import it. Example of "menu_items.json" file: { "item_usage": { "Statics Server": false } } In this case `Statics Server` won't be used. """ items = [] # Get booleans is module should be used for item in self.modules_imports: import_path = item.get("import_path") title = item.get("title") item_usage = self.modules_usage.get(title) if item_usage is None: item_usage = self.modules_usage.get(import_path, True) if not item_usage: if not title: title = import_path self.log.info("{} - Module ignored".format(title)) continue _attributes = self.module_attributes.get(title) if _attributes is None: _attributes = self.module_attributes.get(import_path) if _attributes: item["attributes"] = _attributes items.append(item) if items: self.process_items(items, self.tray_widget.menu) # Add services if they are if self.services_submenu is not None: self.tray_widget.menu.addMenu(self.services_submenu) # Add separator if items and self.services_submenu is not None: self.add_separator(self.tray_widget.menu) self._add_version_item() # Add Exit action to menu aExit = QtWidgets.QAction("&Exit", self.tray_widget) aExit.triggered.connect(self.tray_widget.exit) self.tray_widget.menu.addAction(aExit) # Tell each module which modules were imported self.connect_modules() self.start_modules() def _add_version_item(self): config_file_path = os.path.join(os.environ["PYPE_SETUP_PATH"], "pypeapp", "config.ini") default_config = {} if os.path.exists(config_file_path): config = configparser.ConfigParser() config.read(config_file_path) try: default_config = config["CLIENT"] except Exception: pass subversion = default_config.get("subversion") client_name = default_config.get("client_name") version_string = pype.version.__version__ if subversion: version_string += " ({})".format(subversion) if client_name: version_string += ", {}".format(client_name) version_action = QtWidgets.QAction(version_string, self.tray_widget) self.tray_widget.menu.addAction(version_action) self.add_separator(self.tray_widget.menu) def process_items(self, items, parent_menu): """ Loop through items and add them to parent_menu. :param items: contains dictionary objects representing each item :type items: list :param parent_menu: menu where items will be add :type parent_menu: QtWidgets.QMenu """ for item in items: i_type = item.get('type', None) result = False if i_type is None: continue elif i_type == 'module': result = self.add_module(item, parent_menu) elif i_type == 'action': result = self.add_action(item, parent_menu) elif i_type == 'menu': result = self.add_menu(item, parent_menu) elif i_type == 'separator': result = self.add_separator(parent_menu) if result is False: self.errors.append(item) def add_module(self, item, parent_menu): """Inicialize object of module and add it to context. :param item: item from presets containing information about module :type item: dict :param parent_menu: menu where module's submenus/actions will be add :type parent_menu: QtWidgets.QMenu :returns: success of module implementation :rtype: bool REQUIRED KEYS (item): :import_path (*str*): - full import path as python's import - e.g. *"path.to.module"* :fromlist (*list*): - subparts of import_path (as from is used) - e.g. *["path", "to"]* OPTIONAL KEYS (item): :title (*str*): - represents label shown in services menu - import_path is used if title is not set - title is not used at all if module is not a service .. note:: Module is added as **service** if object does not have *tray_menu* method. """ import_path = item.get('import_path', None) title = item.get('title', import_path) fromlist = item.get('fromlist', []) attributes = item.get("attributes", {}) try: module = __import__("{}".format(import_path), fromlist=fromlist) klass = getattr(module, "CLASS_DEFINIION", None) if not klass and attributes: self.log.error( ("There are defined attributes for module \"{}\" but" "module does not have defined \"CLASS_DEFINIION\"." ).format(import_path)) elif klass and attributes: for key, value in attributes.items(): if hasattr(klass, key): setattr(klass, key, value) else: self.log.error( ("Module \"{}\" does not have attribute \"{}\"." " Check your settings please.").format( import_path, key)) obj = module.tray_init(self.tray_widget, self.main_window) name = obj.__class__.__name__ if hasattr(obj, 'tray_menu'): obj.tray_menu(parent_menu) else: if self.services_submenu is None: self.services_submenu = QtWidgets.QMenu( 'Services', self.tray_widget.menu) action = QtWidgets.QAction(title, self.services_submenu) action.setIcon(self.icon_run) self.services_submenu.addAction(action) if hasattr(obj, 'set_qaction'): obj.set_qaction(action, self.icon_failed) self.modules[name] = obj self.log.info("{} - Module imported".format(title)) except Exception as exc: if self.services_submenu is None: self.services_submenu = QtWidgets.QMenu( 'Services', self.tray_widget.menu) action = QtWidgets.QAction(title, self.services_submenu) action.setIcon(self.icon_failed) self.services_submenu.addAction(action) self.log.warning("{} - Module import Error: {}".format( title, str(exc)), exc_info=True) return False return True def add_action(self, item, parent_menu): """Adds action to parent_menu. :param item: item from presets containing information about action :type item: dictionary :param parent_menu: menu where action will be added :type parent_menu: QtWidgets.QMenu :returns: success of adding item to parent_menu :rtype: bool REQUIRED KEYS (item): :title (*str*): - represents label shown in menu :sourcetype (*str*): - type of action *enum["file", "python"]* :command (*str*): - filepath to script *(sourcetype=="file")* - python code as string *(sourcetype=="python")* OPTIONAL KEYS (item): :tooltip (*str*): - will be shown when hover over action """ sourcetype = item.get('sourcetype', None) command = item.get('command', None) title = item.get('title', '*ERROR*') tooltip = item.get('tooltip', None) if sourcetype not in self.available_sourcetypes: self.log.error('item "{}" has invalid sourcetype'.format(title)) return False if command is None or command.strip() == '': self.log.error('item "{}" has invalid command'.format(title)) return False new_action = QtWidgets.QAction(title, parent_menu) if tooltip is not None and tooltip.strip() != '': new_action.setToolTip(tooltip) if sourcetype == 'python': new_action.triggered.connect(lambda: exec(command)) elif sourcetype == 'file': command = os.path.normpath(command) if '$' in command: command_items = command.split(os.path.sep) for i in range(len(command_items)): if command_items[i].startswith('$'): # TODO: raise error if environment was not found? command_items[i] = os.environ.get( command_items[i].replace('$', ''), command_items[i]) command = os.path.sep.join(command_items) new_action.triggered.connect( lambda: exec(open(command).read(), globals())) parent_menu.addAction(new_action) def add_menu(self, item, parent_menu): """ Adds submenu to parent_menu. :param item: item from presets containing information about menu :type item: dictionary :param parent_menu: menu where submenu will be added :type parent_menu: QtWidgets.QMenu :returns: success of adding item to parent_menu :rtype: bool REQUIRED KEYS (item): :title (*str*): - represents label shown in menu :items (*list*): - list of submenus / actions / separators / modules *(dict)* """ try: title = item.get('title', None) if title is None or title.strip() == '': self.log.error('Missing title in menu from presets') return False new_menu = QtWidgets.QMenu(title, parent_menu) new_menu.setProperty('submenu', 'on') parent_menu.addMenu(new_menu) self.process_items(item.get('items', []), new_menu) return True except Exception: return False def add_separator(self, parent_menu): """ Adds separator to parent_menu. :param parent_menu: menu where submenu will be added :type parent_menu: QtWidgets.QMenu :returns: success of adding item to parent_menu :rtype: bool """ try: parent_menu.addSeparator() return True except Exception: return False def connect_modules(self): """Sends all imported modules to imported modules which have process_modules method. """ for obj in self.modules.values(): if hasattr(obj, 'process_modules'): obj.process_modules(self.modules) def start_modules(self): """Modules which can be modified by another modules and must be launched after *connect_modules* should have tray_start to start their process afterwards. (e.g. Ftrack actions) """ for obj in self.modules.values(): if hasattr(obj, 'tray_start'): obj.tray_start() def on_exit(self): for obj in self.modules.values(): if hasattr(obj, 'tray_exit'): try: obj.tray_exit() except Exception: self.log.error("Failed to exit module {}".format( obj.__class__.__name__))
class BaseHandler(object): '''Custom Action base class <label> - a descriptive string identifing your action. <varaint> - To group actions together, give them the same label and specify a unique variant per action. <identifier> - a unique identifier for app. <description> - a verbose descriptive text for you action <icon> - icon in ftrack ''' # Default priority is 100 priority = 100 # Type is just for logging purpose (e.g.: Action, Event, Application,...) type = 'No-type' ignore_me = False preactions = [] def __init__(self, session, plugins_presets=None): '''Expects a ftrack_api.Session instance''' self.log = Logger().get_logger(self.__class__.__name__) if not (isinstance(session, ftrack_api.session.Session) or isinstance(session, SocketSession)): raise Exception( ("Session object entered with args is instance of \"{}\"" " but expected instances are \"{}\" and \"{}\"").format( str(type(session)), str(ftrack_api.session.Session), str(SocketSession))) self._session = session # Using decorator self.register = self.register_decorator(self.register) self.launch = self.launch_log(self.launch) if plugins_presets is None: plugins_presets = {} 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: label = self.label else: label = '{} {}'.format(self.label, self.variant) try: self._preregister() start_time = time.perf_counter() func(*args, **kwargs) end_time = time.perf_counter() run_time = end_time - start_time self.log.info( ('{} "{}" - Registered successfully ({:.4f}sec)').format( self.type, label, run_time)) except MissingPermision as MPE: self.log.info( ('!{} "{}" - You\'re missing required {} permissions' ).format(self.type, label, str(MPE))) except AssertionError as ae: self.log.warning( ('!{} "{}" - {}').format(self.type, label, str(ae))) except NotImplementedError: self.log.error( ('{} "{}" - Register method is not implemented').format( self.type, label)) except PreregisterException as exc: self.log.warning( ('{} "{}" - {}').format(self.type, label, str(exc))) except Exception as e: self.log.error('{} "{}" - Registration failed ({})'.format( self.type, label, str(e))) return wrapper_register # Decorator def launch_log(self, func): @functools.wraps(func) def wrapper_launch(*args, **kwargs): label = self.__class__.__name__ if hasattr(self, 'label'): label = self.label if hasattr(self, 'variant'): if self.variant is not None: label = '{} {}'.format(self.label, self.variant) self.log.info(('{} "{}": Launched').format(self.type, label)) try: return func(*args, **kwargs) except Exception as exc: self.session.rollback() msg = '{} "{}": Failed ({})'.format(self.type, label, str(exc)) self.log.error(msg, exc_info=True) return {'success': False, 'message': msg} finally: self.log.info(('{} "{}": Finished').format(self.type, label)) return wrapper_launch @property def session(self): '''Return current session.''' return self._session 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 lowercase_rolelist = [x.lower() for x in self.role_list] for role in user['user_security_roles']: if role['security_role']['name'].lower() in lowercase_rolelist: available = True break if available is False: raise MissingPermision # Custom validations result = self.preregister() if result is None: self.log.debug( ("\"{}\" 'preregister' method returned 'None'. Expected it" " didn't fail and continue as preregister returned True." ).format(self.__class__.__name__)) return if result is True: return msg = None if isinstance(result, str): msg = result raise PreregisterException(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. Is decorated by register_log ''' raise NotImplementedError() def _translate_event(self, event, session=None): '''Return *event* translated structure to be used with the API.''' if session is None: session = self.session _entities = event['data'].get('entities_object', None) if (_entities is None or _entities[0].get('link', None) == ftrack_api.symbol.NOT_SET): _entities = self._get_entities(event) event['data']['entities_object'] = _entities return _entities def _get_entities(self, event, session=None, ignore=None): entities = [] selection = event['data'].get('selection') if not selection: return entities if ignore is None: ignore = [] elif isinstance(ignore, str): ignore = [ignore] filtered_selection = [] for entity in selection: if entity['entityType'] not in ignore: filtered_selection.append(entity) if not filtered_selection: return entities if session is None: session = self.session session._local_cache.clear() for entity in filtered_selection: entities.append( session.get(self._get_entity_type(entity, session), entity.get('entityId'))) return entities def _get_entity_type(self, entity, session=None): '''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() if session is None: session = self.session 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 _launch(self, event): self.session.rollback() self.session._local_cache.clear() self.launch(self.session, event) def launch(self, session, event): '''Callback method for the custom action. return either a bool ( True if successful or False if the action failed ) or a dictionary with they keys `message` and `success`, the message should be a string and will be displayed as feedback to the user, success should be a bool, True if successful or False if the action failed. *session* is a `ftrack_api.Session` instance *entities* is a list of tuples each containing the entity type and the entity id. If the entity is a hierarchical you will always get the entity type TypedContext, once retrieved through a get operation you will have the "real" entity type ie. example Shot, Sequence or Asset Build. *event* the unmodified original event ''' 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: self.trigger_action(preaction, event) # Relaunch this action additional_data = {"preactions_launched": True} self.trigger_action(self.identifier, event, additional_event_data=additional_data) return False def _handle_result(self, result): '''Validate the returned result from the action callback''' if isinstance(result, bool): if result is True: result = { 'success': result, 'message': ('{0} launched successfully.'.format(self.label)) } else: result = { 'success': result, 'message': ('{0} launch failed.'.format(self.label)) } elif isinstance(result, dict): items = 'items' in result if items is False: for key in ('success', 'message'): if key in result: continue raise KeyError('Missing required key: {0}.'.format(key)) return result def show_message(self, event, input_message, result=False): """ Shows message to user who triggered event - event - just source of user id - input_message - message that is shown to user - result - changes color of message (based on ftrack settings) - True = Violet - False = Red """ if not isinstance(result, bool): result = False try: message = str(input_message) except Exception: return user_id = event['source']['user']['id'] target = ('applicationId=ftrack.client.web and user.id="{0}"' ).format(user_id) self.session.event_hub.publish(ftrack_api.event.base.Event( topic='ftrack.action.trigger-user-interface', data=dict(type='message', success=result, message=message), target=target), on_error='ignore') def show_interface(self, items, title='', event=None, user=None, username=None, user_id=None): """ Shows interface to user - to identify user must be entered one of args: event, user, username, user_id - 'items' must be list containing Ftrack interface items """ if not any([event, user, username, user_id]): raise TypeError( ('Missing argument `show_interface` requires one of args:' ' event (ftrack_api Event object),' ' user (ftrack_api User object)' ' username (string) or user_id (string)')) if event: user_id = event['source']['user']['id'] elif user: user_id = user['id'] else: if user_id: key = 'id' value = user_id else: key = 'username' value = username user = self.session.query('User where {} is "{}"'.format( key, value)).first() if not user: raise TypeError( ('Ftrack user with {} "{}" was not found!').format( key, value)) user_id = user['id'] target = ('applicationId=ftrack.client.web and user.id="{0}"' ).format(user_id) self.session.event_hub.publish(ftrack_api.event.base.Event( topic='ftrack.action.trigger-user-interface', data=dict(type='widget', items=items, title=title), target=target), on_error='ignore') def show_interface_from_dict(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 = [] splitter = {'type': 'label', 'value': '---'} first = True for key, value in messages.items(): if not first: items.append(splitter) else: first = False subtitle = {'type': 'label', 'value': '<h3>{}</h3>'.format(key)} items.append(subtitle) if isinstance(value, list): for item in value: message = { 'type': 'label', 'value': '<p>{}</p>'.format(item) } items.append(message) else: message = {'type': 'label', 'value': '<p>{}</p>'.format(value)} items.append(message) self.show_interface(items, title, event, user, username, user_id) def trigger_action(self, action_name, event=None, session=None, selection=None, user_data=None, topic="ftrack.action.launch", additional_event_data={}, on_error="ignore"): self.log.debug("Triggering action \"{}\" Begins".format(action_name)) if not session: session = self.session # Getting selection and user data _selection = None _user_data = None if event: _selection = event.get("data", {}).get("selection") _user_data = event.get("source", {}).get("user") if selection is not None: _selection = selection if user_data is not None: _user_data = user_data # Without selection and user data skip triggering msg = "Can't trigger \"{}\" action without {}." if _selection is None: self.log.error(msg.format(action_name, "selection")) return if _user_data is None: self.log.error(msg.format(action_name, "user data")) return _event_data = { "actionIdentifier": action_name, "selection": _selection } # Add additional data if additional_event_data: _event_data.update(additional_event_data) # Create and trigger event session.event_hub.publish(ftrack_api.event.base.Event( topic=topic, data=_event_data, source=dict(user=_user_data)), on_error=on_error) self.log.debug( "Action \"{}\" Triggered successfully".format(action_name)) def trigger_event(self, topic, event_data={}, session=None, source=None, event=None, on_error="ignore"): if session is None: session = self.session if not source and event: source = event.get("source") # Create and trigger event event = ftrack_api.event.base.Event(topic=topic, data=event_data, source=source) session.event_hub.publish(event, on_error=on_error) self.log.debug(("Publishing event: {}").format(str(event.__dict__))) def get_project_from_entity(self, entity): low_entity_type = entity.entity_type.lower() if low_entity_type == "project": return entity if "project" in entity: # reviewsession, task(Task, Shot, Sequence,...) return entity["project"] if low_entity_type == "filecomponent": entity = entity["version"] low_entity_type = entity.entity_type.lower() if low_entity_type == "assetversion": asset = entity["asset"] if asset: parent = asset["parent"] if parent: return parent["project"] project_data = entity["link"][0] return self.session.query("Project where id is {}".format( project_data["id"])).one()
class TrayManager: """Cares about context of application. Load submenus, actions, separators and modules into tray's context. """ modules = {} services = {} services_submenu = None errors = [] items = (config.get_presets(first_run=True).get('tray', {}).get('menu_items', [])) available_sourcetypes = ['python', 'file'] def __init__(self, tray_widget, main_window): self.tray_widget = tray_widget self.main_window = main_window self.log = Logger().get_logger(self.__class__.__name__) self.icon_run = QtGui.QIcon(get_resource('circle_green.png')) self.icon_stay = QtGui.QIcon(get_resource('circle_orange.png')) self.icon_failed = QtGui.QIcon(get_resource('circle_red.png')) self.services_thread = None def process_presets(self): """Add modules to tray by presets. This is start up method for TrayManager. Loads presets and import modules described in "menu_items.json". In `item_usage` key you can specify by item's title or import path if you want to import it. Example of "menu_items.json" file: { "item_usage": { "Statics Server": false } }, { "item_import": [{ "title": "Ftrack", "type": "module", "import_path": "pype.ftrack.tray", "fromlist": ["pype", "ftrack"] }, { "title": "Statics Server", "type": "module", "import_path": "pype.services.statics_server", "fromlist": ["pype","services"] }] } In this case `Statics Server` won't be used. """ # Backwards compatible presets loading if isinstance(self.items, list): items = self.items else: items = [] # Get booleans is module should be used usages = self.items.get("item_usage") or {} for item in self.items.get("item_import", []): import_path = item.get("import_path") title = item.get("title") item_usage = usages.get(title) if item_usage is None: item_usage = usages.get(import_path, True) if item_usage: items.append(item) else: if not title: title = import_path self.log.debug("{} - Module ignored".format(title)) if items: self.process_items(items, self.tray_widget.menu) # Add services if they are if self.services_submenu is not None: self.tray_widget.menu.addMenu(self.services_submenu) # Add separator if items and self.services_submenu is not None: self.add_separator(self.tray_widget.menu) # Add Exit action to menu aExit = QtWidgets.QAction("&Exit", self.tray_widget) aExit.triggered.connect(self.tray_widget.exit) self.tray_widget.menu.addAction(aExit) # Tell each module which modules were imported self.connect_modules() self.start_modules() def process_items(self, items, parent_menu): """ Loop through items and add them to parent_menu. :param items: contains dictionary objects representing each item :type items: list :param parent_menu: menu where items will be add :type parent_menu: QtWidgets.QMenu """ for item in items: i_type = item.get('type', None) result = False if i_type is None: continue elif i_type == 'module': result = self.add_module(item, parent_menu) elif i_type == 'action': result = self.add_action(item, parent_menu) elif i_type == 'menu': result = self.add_menu(item, parent_menu) elif i_type == 'separator': result = self.add_separator(parent_menu) if result is False: self.errors.append(item) def add_module(self, item, parent_menu): """Inicialize object of module and add it to context. :param item: item from presets containing information about module :type item: dict :param parent_menu: menu where module's submenus/actions will be add :type parent_menu: QtWidgets.QMenu :returns: success of module implementation :rtype: bool REQUIRED KEYS (item): :import_path (*str*): - full import path as python's import - e.g. *"path.to.module"* :fromlist (*list*): - subparts of import_path (as from is used) - e.g. *["path", "to"]* OPTIONAL KEYS (item): :title (*str*): - represents label shown in services menu - import_path is used if title is not set - title is not used at all if module is not a service .. note:: Module is added as **service** if object does not have *tray_menu* method. """ import_path = item.get('import_path', None) title = item.get('title', import_path) fromlist = item.get('fromlist', []) try: module = __import__("{}".format(import_path), fromlist=fromlist) obj = module.tray_init(self.tray_widget, self.main_window) name = obj.__class__.__name__ if hasattr(obj, 'tray_menu'): obj.tray_menu(parent_menu) else: if self.services_submenu is None: self.services_submenu = QtWidgets.QMenu( 'Services', self.tray_widget.menu) action = QtWidgets.QAction(title, self.services_submenu) action.setIcon(self.icon_run) self.services_submenu.addAction(action) if hasattr(obj, 'set_qaction'): obj.set_qaction(action, self.icon_failed) self.modules[name] = obj self.log.info("{} - Module imported".format(title)) except ImportError as ie: if self.services_submenu is None: self.services_submenu = QtWidgets.QMenu( 'Services', self.tray_widget.menu) action = QtWidgets.QAction(title, self.services_submenu) action.setIcon(self.icon_failed) self.services_submenu.addAction(action) self.log.warning("{} - Module import Error: {}".format( title, str(ie)), exc_info=True) return False return True def add_action(self, item, parent_menu): """Adds action to parent_menu. :param item: item from presets containing information about action :type item: dictionary :param parent_menu: menu where action will be added :type parent_menu: QtWidgets.QMenu :returns: success of adding item to parent_menu :rtype: bool REQUIRED KEYS (item): :title (*str*): - represents label shown in menu :sourcetype (*str*): - type of action *enum["file", "python"]* :command (*str*): - filepath to script *(sourcetype=="file")* - python code as string *(sourcetype=="python")* OPTIONAL KEYS (item): :tooltip (*str*): - will be shown when hover over action """ sourcetype = item.get('sourcetype', None) command = item.get('command', None) title = item.get('title', '*ERROR*') tooltip = item.get('tooltip', None) if sourcetype not in self.available_sourcetypes: self.log.error('item "{}" has invalid sourcetype'.format(title)) return False if command is None or command.strip() == '': self.log.error('item "{}" has invalid command'.format(title)) return False new_action = QtWidgets.QAction(title, parent_menu) if tooltip is not None and tooltip.strip() != '': new_action.setToolTip(tooltip) if sourcetype == 'python': new_action.triggered.connect(lambda: exec(command)) elif sourcetype == 'file': command = os.path.normpath(command) if '$' in command: command_items = command.split(os.path.sep) for i in range(len(command_items)): if command_items[i].startswith('$'): # TODO: raise error if environment was not found? command_items[i] = os.environ.get( command_items[i].replace('$', ''), command_items[i]) command = os.path.sep.join(command_items) new_action.triggered.connect( lambda: exec(open(command).read(), globals())) parent_menu.addAction(new_action) def add_menu(self, item, parent_menu): """ Adds submenu to parent_menu. :param item: item from presets containing information about menu :type item: dictionary :param parent_menu: menu where submenu will be added :type parent_menu: QtWidgets.QMenu :returns: success of adding item to parent_menu :rtype: bool REQUIRED KEYS (item): :title (*str*): - represents label shown in menu :items (*list*): - list of submenus / actions / separators / modules *(dict)* """ try: title = item.get('title', None) if title is None or title.strip() == '': self.log.error('Missing title in menu from presets') return False new_menu = QtWidgets.QMenu(title, parent_menu) new_menu.setProperty('submenu', 'on') parent_menu.addMenu(new_menu) self.process_items(item.get('items', []), new_menu) return True except Exception: return False def add_separator(self, parent_menu): """ Adds separator to parent_menu. :param parent_menu: menu where submenu will be added :type parent_menu: QtWidgets.QMenu :returns: success of adding item to parent_menu :rtype: bool """ try: parent_menu.addSeparator() return True except Exception: return False def connect_modules(self): """Sends all imported modules to imported modules which have process_modules method. """ for obj in self.modules.values(): if hasattr(obj, 'process_modules'): obj.process_modules(self.modules) def start_modules(self): """Modules which can be modified by another modules and must be launched after *connect_modules* should have tray_start to start their process afterwards. (e.g. Ftrack actions) """ for obj in self.modules.values(): if hasattr(obj, 'tray_start'): obj.tray_start() def on_exit(self): for obj in self.modules.values(): if hasattr(obj, 'tray_exit'): try: obj.tray_exit() except Exception: self.log.error("Failed to exit module {}".format( obj.__class__.__name__))