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 PremierePrelaunch(PypeHook): """ This hook will check if current workfile path has Adobe Premiere project inside. IF not, it initialize it and finally it pass path to the project by environment variable to Premiere launcher shell script. """ project_code = None 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 # initialize self._S = api.Session # get context variables self._S["AVALON_PROJECT"] = env["AVALON_PROJECT"] self._S["AVALON_ASSET"] = env["AVALON_ASSET"] task = self._S["AVALON_TASK"] = env["AVALON_TASK"] # get workfile path anatomy_filled = self.get_anatomy_filled() # if anatomy template should have different root for particular task # just add for example > work[conforming]: workfile_search_key = f"work[{task.lower()}]" workfile_key = anatomy_filled.get(workfile_search_key, anatomy_filled.get("work")) workdir = env["AVALON_WORKDIR"] = workfile_key["folder"] # create workdir if doesn't exist os.makedirs(workdir, exist_ok=True) self.log.info(f"Work dir is: `{workdir}`") # adding project code to env env["AVALON_PROJECT_CODE"] = self.project_code try: __import__("pype.hosts.premiere") __import__("pyblish") except ImportError as e: print(traceback.format_exc()) print("pyblish: Could not load integration: %s " % e) else: # Premiere Setup integration prlib.setup(env) return True def get_anatomy_filled(self): root_path = api.registered_root() project_name = self._S["AVALON_PROJECT"] asset_name = self._S["AVALON_ASSET"] io.install() project_entity = io.find_one({"type": "project", "name": project_name}) assert project_entity, ( "Project '{0}' was not found.").format(project_name) self.log.debug("Collected Project \"{}\"".format(project_entity)) asset_entity = io.find_one({ "type": "asset", "name": asset_name, "parent": project_entity["_id"] }) assert asset_entity, ( "No asset found by the name '{0}' in project '{1}'").format( asset_name, project_name) project_name = project_entity["name"] self.project_code = project_entity["data"].get("code") self.log.info("Anatomy object collected for project \"{}\".".format( project_name)) hierarchy_items = asset_entity["data"]["parents"] hierarchy = "" if hierarchy_items: hierarchy = os.path.join(*hierarchy_items) template_data = { "root": root_path, "project": { "name": project_name, "code": self.project_code }, "asset": asset_entity["name"], "hierarchy": hierarchy.replace("\\", "/"), "task": self._S["AVALON_TASK"], "ext": "ppro", "version": 1, "username": os.getenv("PYPE_USERNAME", "").strip() } avalon_app_name = os.environ.get("AVALON_APP_NAME") if avalon_app_name: application_def = lib.get_application(avalon_app_name) app_dir = application_def.get("application_dir") if app_dir: template_data["app"] = app_dir anatomy = Anatomy(project_name) anatomy_filled = anatomy.format_all(template_data).get_solved() return anatomy_filled
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 CelactionPrelaunchHook(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. """ workfile_ext = "scn" 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 # initialize self._S = api.Session # get publish version of celaction app = "celaction_publish" # get context variables project = self._S["AVALON_PROJECT"] = env["AVALON_PROJECT"] asset = self._S["AVALON_ASSET"] = env["AVALON_ASSET"] task = self._S["AVALON_TASK"] = env["AVALON_TASK"] workdir = self._S["AVALON_WORKDIR"] = env["AVALON_WORKDIR"] # get workfile path anatomy_filled = self.get_anatomy_filled() workfile = anatomy_filled["work"]["file"] version = anatomy_filled["version"] # create workdir if doesn't exist os.makedirs(workdir, exist_ok=True) self.log.info(f"Work dir is: `{workdir}`") # get last version of workfile workfile_last = env.get("AVALON_LAST_WORKFILE") self.log.debug(f"_ workfile_last: `{workfile_last}`") if workfile_last: workfile = workfile_last workfile_path = os.path.join(workdir, workfile) # copy workfile from template if doesnt exist any on path if not os.path.isfile(workfile_path): # try to get path from environment or use default # from `pype.celation` dir template_path = env.get("CELACTION_TEMPLATE") or os.path.join( env.get("PYPE_MODULE_ROOT"), "pype/hosts/celaction/celaction_template_scene.scn") self.log.info( f"Creating workfile from template: `{template_path}`") shutil.copy2(os.path.normpath(template_path), os.path.normpath(workfile_path)) self.log.info(f"Workfile to open: `{workfile_path}`") # adding compulsory environment var for openting file env["PYPE_CELACTION_PROJECT_FILE"] = workfile_path # setting output parameters path = r"Software\CelAction\CelAction2D\User Settings" winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) hKey = winreg.OpenKey( winreg.HKEY_CURRENT_USER, "Software\\CelAction\\CelAction2D\\User Settings", 0, winreg.KEY_ALL_ACCESS) # TODO: change to root path and pyblish standalone to premiere way pype_root_path = os.getenv("PYPE_SETUP_PATH") path = os.path.join(pype_root_path, "pype.bat") winreg.SetValueEx(hKey, "SubmitAppTitle", 0, winreg.REG_SZ, path) parameters = [ "launch", f"--app {app}", f"--project {project}", f"--asset {asset}", f"--task {task}", "--currentFile \\\"\"*SCENE*\"\\\"", "--chunk 10", "--frameStart *START*", "--frameEnd *END*", "--resolutionWidth *X*", "--resolutionHeight *Y*", # "--programDir \"'*PROGPATH*'\"" ] winreg.SetValueEx(hKey, "SubmitParametersTitle", 0, winreg.REG_SZ, " ".join(parameters)) # setting resolution parameters path = r"Software\CelAction\CelAction2D\User Settings\Dialogs" path += r"\SubmitOutput" winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, winreg.KEY_ALL_ACCESS) winreg.SetValueEx(hKey, "SaveScene", 0, winreg.REG_DWORD, 1) winreg.SetValueEx(hKey, "CustomX", 0, winreg.REG_DWORD, 1920) winreg.SetValueEx(hKey, "CustomY", 0, winreg.REG_DWORD, 1080) # making sure message dialogs don't appear when overwriting path = r"Software\CelAction\CelAction2D\User Settings\Messages" path += r"\OverwriteScene" winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, winreg.KEY_ALL_ACCESS) winreg.SetValueEx(hKey, "Result", 0, winreg.REG_DWORD, 6) winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1) path = r"Software\CelAction\CelAction2D\User Settings\Messages" path += r"\SceneSaved" winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, winreg.KEY_ALL_ACCESS) winreg.SetValueEx(hKey, "Result", 0, winreg.REG_DWORD, 1) winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1) return True def get_anatomy_filled(self): root_path = api.registered_root() project_name = self._S["AVALON_PROJECT"] asset_name = self._S["AVALON_ASSET"] io.install() project_entity = io.find_one({"type": "project", "name": project_name}) assert project_entity, ( "Project '{0}' was not found.").format(project_name) log.debug("Collected Project \"{}\"".format(project_entity)) asset_entity = io.find_one({ "type": "asset", "name": asset_name, "parent": project_entity["_id"] }) assert asset_entity, ( "No asset found by the name '{0}' in project '{1}'").format( asset_name, project_name) project_name = project_entity["name"] log.info("Anatomy object collected for project \"{}\".".format( project_name)) hierarchy_items = asset_entity["data"]["parents"] hierarchy = "" if hierarchy_items: hierarchy = os.path.join(*hierarchy_items) template_data = { "root": root_path, "project": { "name": project_name, "code": project_entity["data"].get("code") }, "asset": asset_entity["name"], "hierarchy": hierarchy.replace("\\", "/"), "task": self._S["AVALON_TASK"], "ext": self.workfile_ext, "version": 1, "username": os.getenv("PYPE_USERNAME", "").strip() } avalon_app_name = os.environ.get("AVALON_APP_NAME") if avalon_app_name: application_def = lib.get_application(avalon_app_name) app_dir = application_def.get("application_dir") if app_dir: template_data["app"] = app_dir anatomy = Anatomy(project_name) anatomy_filled = anatomy.format_all(template_data).get_solved() return anatomy_filled
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__))