class AssetsRemover(BaseAction): '''Edit meta data action.''' #: Action identifier. identifier = 'remove.assets' #: Action label. label = "Pype Admin" variant = '- Delete Assets by Name' #: Action description. description = 'Removes assets from Ftrack and Avalon db with all childs' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '')) #: Db db = DbConnector() def discover(self, session, entities, event): ''' Validation ''' if len(entities) != 1: return False valid = ["show", "task"] entityType = event["data"]["selection"][0].get("entityType", "") if entityType.lower() not in valid: return False return True def interface(self, session, entities, event): if not event['data'].get('values', {}): title = 'Enter Asset names to delete' items = [] for i in range(15): item = { 'label': 'Asset {}'.format(i + 1), 'name': 'asset_{}'.format(i + 1), 'type': 'text', 'value': '' } items.append(item) return {'items': items, 'title': title} def launch(self, session, entities, event): entity = entities[0] if entity.entity_type.lower() != 'Project': project = entity['project'] else: project = entity if 'values' not in event['data']: return values = event['data']['values'] if len(values) <= 0: return {'success': True, 'message': 'No Assets to delete!'} asset_names = [] for k, v in values.items(): if v.replace(' ', '') != '': asset_names.append(v) self.db.install() self.db.Session['AVALON_PROJECT'] = project["full_name"] assets = self.find_assets(asset_names) all_ids = [] for asset in assets: all_ids.append(asset['_id']) all_ids.extend(self.find_child(asset)) if len(all_ids) == 0: self.db.uninstall() return {'success': True, 'message': 'None of assets'} or_subquery = [] for id in all_ids: or_subquery.append({'_id': id}) delete_query = {'$or': or_subquery} self.db.delete_many(delete_query) self.db.uninstall() return {'success': True, 'message': 'All assets were deleted!'} def find_child(self, entity): output = [] id = entity['_id'] visuals = [x for x in self.db.find({'data.visualParent': id})] assert len(visuals) == 0, 'This asset has another asset as child' childs = self.db.find({'parent': id}) for child in childs: output.append(child['_id']) output.extend(self.find_child(child)) return output def find_assets(self, asset_names): assets = [] for name in asset_names: entity = self.db.find_one({'type': 'asset', 'name': name}) if entity is not None and entity not in assets: assets.append(entity) return assets
class PypeUpdateFromV2_2_0(BaseAction): """This action is to remove silo field from database and changes asset schema to newer version WARNING: it is NOT for situations when you want to switch from avalon-core to Pype's avalon-core!!! """ #: Action identifier. identifier = "silos.doctor" #: Action label. label = "Pype Update" variant = "- v2.2.0 to v2.3.0 or higher" #: Action description. description = "Use when Pype was updated from v2.2.0 to v2.3.0 or higher" #: roles that are allowed to register this action role_list = ["Pypeclub", "Administrator"] icon = "{}/ftrack/action_icons/PypeUpdate.svg".format( os.environ.get("PYPE_STATICS_SERVER", "")) # connector to MongoDB (Avalon mongo) db_con = DbConnector() def discover(self, session, entities, event): """ Validation """ if len(entities) != 1: return False if entities[0].entity_type.lower() != "project": return False return True def interface(self, session, entities, event): if event['data'].get('values', {}): return items = [] item_splitter = {'type': 'label', 'value': '---'} title = "Updated Pype from v 2.2.0 to v2.3.0 or higher" items.append({ "type": "label", "value": ("NOTE: This doctor action should be used ONLY when Pype" " was updated from v2.2.0 to v2.3.0 or higher.<br><br><br>") }) items.append({ "type": "label", "value": ("Select if want to process <b>all synchronized projects</b>" " or <b>selection</b>.") }) items.append({ "type": "enumerator", "name": "__process_all__", "data": [{ "label": "All synchronized projects", "value": True }, { "label": "Selection", "value": False }], "value": False }) items.append({ "type": "label", "value": ("<br/><br/><h2>Synchronized projects:</h2>" "<i>(ignore if <strong>\"ALL projects\"</strong> selected)</i>") }) self.log.debug("Getting all Ftrack projects") # Get all Ftrack projects all_ftrack_projects = [ project["full_name"] for project in session.query("Project").all() ] self.log.debug("Getting Avalon projects that are also in the Ftrack") # Get Avalon projects that are in Ftrack self.db_con.install() possible_projects = [ project["name"] for project in self.db_con.projects() if project["name"] in all_ftrack_projects ] for project in possible_projects: item_label = {"type": "label", "value": project} item = { "label": "- process", "name": project, "type": 'boolean', "value": False } items.append(item_splitter) items.append(item_label) items.append(item) if len(possible_projects) == 0: return { "success": False, "message": ("Nothing to process." " There are not projects synchronized to avalon.") } else: return {"items": items, "title": title} def launch(self, session, entities, event): if 'values' not in event['data']: return projects_selection = {True: [], False: []} process_all = None values = event['data']['values'] for key, value in values.items(): if key == "__process_all__": process_all = value continue projects_selection[value].append(key) # Skip if process_all value is not boolean # - may happen when user delete string line in combobox if not isinstance(process_all, bool): self.log.warning( "Nothing was processed. User didn't select if want to process" " selection or all projects!") return { "success": False, "message": ("Nothing was processed. You must select if want to process" " \"selection\" or \"all projects\"!") } projects_to_process = projects_selection[True] if process_all: projects_to_process.extend(projects_selection[False]) self.db_con.install() for project in projects_to_process: self.log.debug("Processing project \"{}\"".format(project)) self.db_con.Session["AVALON_PROJECT"] = project self.log.debug("- Unsetting silos on assets") self.db_con.update_many({"type": "asset"}, {"$unset": { "silo": "" }}) self.log.debug("- setting schema of assets to v.3") self.db_con.update_many( {"type": "asset"}, {"$set": { "schema": "avalon-core:asset-3.0" }}) return True
class StoreThumbnailsToAvalon(BaseAction): # Action identifier identifier = "store.thubmnail.to.avalon" # Action label label = "Pype Admin" # Action variant variant = "- Store Thumbnails to avalon" # Action description description = 'Test action' # roles that are allowed to register this action role_list = ["Pypeclub", "Administrator", "Project Manager"] icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '') ) thumbnail_key = "AVALON_THUMBNAIL_ROOT" db_con = DbConnector() def discover(self, session, entities, event): for entity in entities: if entity.entity_type.lower() == "assetversion": return True return False def launch(self, session, entities, event): # DEBUG LINE # root_path = r"C:\Users\jakub.trllo\Desktop\Tests\ftrack_thumbnails" user = session.query( "User where username is '{0}'".format(session.api_user) ).one() action_job = session.create("Job", { "user": user, "status": "running", "data": json.dumps({ "description": "Storing thumbnails to avalon." }) }) session.commit() project = self.get_project_from_entity(entities[0]) project_name = project["full_name"] anatomy = Anatomy(project_name) if "publish" not in anatomy.templates: msg = "Anatomy does not have set publish key!" action_job["status"] = "failed" session.commit() self.log.warning(msg) return { "success": False, "message": msg } if "thumbnail" not in anatomy.templates["publish"]: msg = ( "There is not set \"thumbnail\"" " template in Antomy for project \"{}\"" ).format(project_name) action_job["status"] = "failed" session.commit() self.log.warning(msg) return { "success": False, "message": msg } thumbnail_roots = os.environ.get(self.thumbnail_key) if ( "{thumbnail_root}" in anatomy.templates["publish"]["thumbnail"] and not thumbnail_roots ): msg = "`{}` environment is not set".format(self.thumbnail_key) action_job["status"] = "failed" session.commit() self.log.warning(msg) return { "success": False, "message": msg } existing_thumbnail_root = None for path in thumbnail_roots.split(os.pathsep): if os.path.exists(path): existing_thumbnail_root = path break if existing_thumbnail_root is None: msg = ( "Can't access paths, set in `{}` ({})" ).format(self.thumbnail_key, thumbnail_roots) action_job["status"] = "failed" session.commit() self.log.warning(msg) return { "success": False, "message": msg } example_template_data = { "_id": "ID", "thumbnail_root": "THUBMNAIL_ROOT", "thumbnail_type": "THUMBNAIL_TYPE", "ext": ".EXT", "project": { "name": "PROJECT_NAME", "code": "PROJECT_CODE" }, "asset": "ASSET_NAME", "subset": "SUBSET_NAME", "version": "VERSION_NAME", "hierarchy": "HIERARCHY" } tmp_filled = anatomy.format_all(example_template_data) thumbnail_result = tmp_filled["publish"]["thumbnail"] if not thumbnail_result.solved: missing_keys = thumbnail_result.missing_keys invalid_types = thumbnail_result.invalid_types submsg = "" if missing_keys: submsg += "Missing keys: {}".format(", ".join( ["\"{}\"".format(key) for key in missing_keys] )) if invalid_types: items = [] for key, value in invalid_types.items(): items.append("{}{}".format(str(key), str(value))) submsg += "Invalid types: {}".format(", ".join(items)) msg = ( "Thumbnail Anatomy template expects more keys than action" " can offer. {}" ).format(submsg) action_job["status"] = "failed" session.commit() self.log.warning(msg) return { "success": False, "message": msg } thumbnail_template = anatomy.templates["publish"]["thumbnail"] self.db_con.install() for entity in entities: # Skip if entity is not AssetVersion (never should happend, but..) if entity.entity_type.lower() != "assetversion": continue # Skip if AssetVersion don't have thumbnail thumbnail_ent = entity["thumbnail"] if thumbnail_ent is None: self.log.debug(( "Skipping. AssetVersion don't " "have set thumbnail. {}" ).format(entity["id"])) continue avalon_ents_result = self.get_avalon_entities_for_assetversion( entity, self.db_con ) version_full_path = ( "Asset: \"{project_name}/{asset_path}\"" " | Subset: \"{subset_name}\"" " | Version: \"{version_name}\"" ).format(**avalon_ents_result) version = avalon_ents_result["version"] if not version: self.log.warning(( "AssetVersion does not have version in avalon. {}" ).format(version_full_path)) continue thumbnail_id = version["data"].get("thumbnail_id") if thumbnail_id: self.log.info(( "AssetVersion skipped, already has thubmanil set. {}" ).format(version_full_path)) continue # Get thumbnail extension file_ext = thumbnail_ent["file_type"] if not file_ext.startswith("."): file_ext = ".{}".format(file_ext) avalon_project = avalon_ents_result["project"] avalon_asset = avalon_ents_result["asset"] hierarchy = "" parents = avalon_asset["data"].get("parents") or [] if parents: hierarchy = "/".join(parents) # Prepare anatomy template fill data # 1. Create new id for thumbnail entity thumbnail_id = ObjectId() template_data = { "_id": str(thumbnail_id), "thumbnail_root": existing_thumbnail_root, "thumbnail_type": "thumbnail", "ext": file_ext, "project": { "name": avalon_project["name"], "code": avalon_project["data"].get("code") }, "asset": avalon_ents_result["asset_name"], "subset": avalon_ents_result["subset_name"], "version": avalon_ents_result["version_name"], "hierarchy": hierarchy } anatomy_filled = anatomy.format(template_data) thumbnail_path = anatomy_filled["publish"]["thumbnail"] thumbnail_path = thumbnail_path.replace("..", ".") thumbnail_path = os.path.normpath(thumbnail_path) downloaded = False for loc in (thumbnail_ent.get("component_locations") or []): res_id = loc.get("resource_identifier") if not res_id: continue thubmnail_url = self.get_thumbnail_url(res_id) if self.download_file(thubmnail_url, thumbnail_path): downloaded = True break if not downloaded: self.log.warning( "Could not download thumbnail for {}".format( version_full_path ) ) continue # Clean template data from keys that are dynamic template_data.pop("_id") template_data.pop("thumbnail_root") thumbnail_entity = { "_id": thumbnail_id, "type": "thumbnail", "schema": "pype:thumbnail-1.0", "data": { "template": thumbnail_template, "template_data": template_data } } # Create thumbnail entity self.db_con.insert_one(thumbnail_entity) self.log.debug( "Creating entity in database {}".format(str(thumbnail_entity)) ) # Set thumbnail id for version self.db_con.update_one( {"_id": version["_id"]}, {"$set": {"data.thumbnail_id": thumbnail_id}} ) self.db_con.update_one( {"_id": avalon_asset["_id"]}, {"$set": {"data.thumbnail_id": thumbnail_id}} ) action_job["status"] = "done" session.commit() return True def get_thumbnail_url(self, resource_identifier, size=None): # TODO use ftrack_api method rather (find way how to use it) url_string = ( u'{url}/component/thumbnail?id={id}&username={username}' u'&apiKey={apiKey}' ) url = url_string.format( url=self.session.server_url, id=resource_identifier, username=self.session.api_user, apiKey=self.session.api_key ) if size: url += u'&size={0}'.format(size) return url def download_file(self, source_url, dst_file_path): dir_path = os.path.dirname(dst_file_path) try: os.makedirs(dir_path) except OSError as exc: if exc.errno != errno.EEXIST: self.log.warning( "Could not create folder: \"{}\"".format(dir_path) ) return False self.log.debug( "Downloading file \"{}\" -> \"{}\"".format( source_url, dst_file_path ) ) file_open = open(dst_file_path, "wb") try: file_open.write(requests.get(source_url).content) except Exception: self.log.warning( "Download of image `{}` failed.".format(source_url) ) return False finally: file_open.close() return True def get_avalon_entities_for_assetversion(self, asset_version, db_con): output = { "success": True, "message": None, "project": None, "project_name": None, "asset": None, "asset_name": None, "asset_path": None, "subset": None, "subset_name": None, "version": None, "version_name": None, "representations": None } db_con.install() ft_asset = asset_version["asset"] subset_name = ft_asset["name"] version = asset_version["version"] parent = ft_asset["parent"] ent_path = "/".join( [ent["name"] for ent in parent["link"]] ) project = self.get_project_from_entity(asset_version) project_name = project["full_name"] output["project_name"] = project_name output["asset_name"] = parent["name"] output["asset_path"] = ent_path output["subset_name"] = subset_name output["version_name"] = version db_con.Session["AVALON_PROJECT"] = project_name avalon_project = db_con.find_one({"type": "project"}) output["project"] = avalon_project if not avalon_project: output["success"] = False output["message"] = ( "Project not synchronized to avalon `{}`".format(project_name) ) return output asset_ent = None asset_mongo_id = parent["custom_attributes"].get(CustAttrIdKey) if asset_mongo_id: try: asset_mongo_id = ObjectId(asset_mongo_id) asset_ent = db_con.find_one({ "type": "asset", "_id": asset_mongo_id }) except Exception: pass if not asset_ent: asset_ent = db_con.find_one({ "type": "asset", "data.ftrackId": parent["id"] }) output["asset"] = asset_ent if not asset_ent: output["success"] = False output["message"] = ( "Not synchronized entity to avalon `{}`".format(ent_path) ) return output asset_mongo_id = asset_ent["_id"] subset_ent = db_con.find_one({ "type": "subset", "parent": asset_mongo_id, "name": subset_name }) output["subset"] = subset_ent if not subset_ent: output["success"] = False output["message"] = ( "Subset `{}` does not exist under Asset `{}`" ).format(subset_name, ent_path) return output version_ent = db_con.find_one({ "type": "version", "name": version, "parent": subset_ent["_id"] }) output["version"] = version_ent if not version_ent: output["success"] = False output["message"] = ( "Version `{}` does not exist under Subset `{}` | Asset `{}`" ).format(version, subset_name, ent_path) return output repre_ents = list(db_con.find({ "type": "representation", "parent": version_ent["_id"] })) output["representations"] = repre_ents return output
class AttributesRemapper(BaseAction): '''Edit meta data action.''' ignore_me = True #: Action identifier. identifier = 'attributes.remapper' #: Action label. label = "Pype Doctor" variant = '- Attributes Remapper' #: Action description. description = 'Remaps attributes in avalon DB' #: roles that are allowed to register this action role_list = ["Pypeclub", "Administrator"] icon = '{}/ftrack/action_icons/PypeDoctor.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '')) db_con = DbConnector() keys_to_change = { "fstart": "frameStart", "startFrame": "frameStart", "edit_in": "frameStart", "fend": "frameEnd", "endFrame": "frameEnd", "edit_out": "frameEnd", "handle_start": "handleStart", "handle_end": "handleEnd", "handles": ["handleEnd", "handleStart"], "frameRate": "fps", "framerate": "fps", "resolution_width": "resolutionWidth", "resolution_height": "resolutionHeight", "pixel_aspect": "pixelAspect" } def discover(self, session, entities, event): ''' Validation ''' return True def interface(self, session, entities, event): if event['data'].get('values', {}): return title = 'Select Projects where attributes should be remapped' items = [] selection_enum = { 'label': 'Process type', 'type': 'enumerator', 'name': 'process_type', 'data': [{ 'label': 'Selection', 'value': 'selection' }, { 'label': 'Inverted selection', 'value': 'except' }], 'value': 'selection' } selection_label = { 'type': 'label', 'value': ('Selection based variants:<br/>' '- `Selection` - ' 'NOTHING is processed when nothing is selected<br/>' '- `Inverted selection` - ' 'ALL Projects are processed when nothing is selected') } items.append(selection_enum) items.append(selection_label) item_splitter = {'type': 'label', 'value': '---'} all_projects = session.query('Project').all() for project in all_projects: item_label = { 'type': 'label', 'value': '{} (<i>{}</i>)'.format(project['full_name'], project['name']) } item = {'name': project['id'], 'type': 'boolean', 'value': False} if len(items) > 0: items.append(item_splitter) items.append(item_label) items.append(item) if len(items) == 0: return {'success': False, 'message': 'Didn\'t found any projects'} else: return {'items': items, 'title': title} def launch(self, session, entities, event): if 'values' not in event['data']: return values = event['data']['values'] process_type = values.pop('process_type') selection = True if process_type == 'except': selection = False interface_messages = {} projects_to_update = [] for project_id, update_bool in values.items(): if not update_bool and selection: continue if update_bool and not selection: continue project = session.query( 'Project where id is "{}"'.format(project_id)).one() projects_to_update.append(project) if not projects_to_update: self.log.debug('Nothing to update') return {'success': True, 'message': 'Nothing to update'} self.db_con.install() relevant_types = ["project", "asset", "version"] for ft_project in projects_to_update: self.log.debug("Processing project \"{}\"".format( ft_project["full_name"])) self.db_con.Session["AVALON_PROJECT"] = ft_project["full_name"] project = self.db_con.find_one({'type': 'project'}) if not project: key = "Projects not synchronized to db" if key not in interface_messages: interface_messages[key] = [] interface_messages[key].append(ft_project["full_name"]) continue # Get all entities in project collection from MongoDB _entities = self.db_con.find({}) for _entity in _entities: ent_t = _entity.get("type", "*unknown type") name = _entity.get("name", "*unknown name") self.log.debug("- {} ({})".format(name, ent_t)) # Skip types that do not store keys to change if ent_t.lower() not in relevant_types: self.log.debug("-- skipping - type is not relevant") continue # Get data which will change updating_data = {} source_data = _entity["data"] for key_from, key_to in self.keys_to_change.items(): # continue if final key already exists if type(key_to) == list: for key in key_to: # continue if final key was set in update_data if key in updating_data: continue # continue if source key not exist or value is None value = source_data.get(key_from) if value is None: continue self.log.debug("-- changing key {} to {}".format( key_from, key)) updating_data[key] = value else: if key_to in source_data: continue # continue if final key was set in update_data if key_to in updating_data: continue # continue if source key not exist or value is None value = source_data.get(key_from) if value is None: continue self.log.debug("-- changing key {} to {}".format( key_from, key_to)) updating_data[key_to] = value # Pop out old keys from entity is_obsolete = False for key in self.keys_to_change: if key not in source_data: continue is_obsolete = True source_data.pop(key) # continue if there is nothing to change if not is_obsolete and not updating_data: self.log.debug("-- nothing to change") continue source_data.update(updating_data) self.db_con.update_many({"_id": _entity["_id"]}, {"$set": { "data": source_data }}) self.db_con.uninstall() if interface_messages: self.show_interface_from_dict( messages=interface_messages, title="Errors during remapping attributes", event=event) return True def show_interface_from_dict(self, event, messages, title=""): items = [] for key, value in messages.items(): if not value: continue subtitle = {'type': 'label', 'value': '# {}'.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=items, title=title, event=event)
class UserAssigmentEvent(BaseEvent): """ This script will intercept user assigment / de-assigment event and run shell script, providing as much context as possible. It expects configuration file ``presets/ftrack/user_assigment_event.json``. In it, you define paths to scripts to be run for user assigment event and for user-deassigment:: { "add": [ "/path/to/script1", "/path/to/script2" ], "remove": [ "/path/to/script3", "/path/to/script4" ] } Those scripts are executed in shell. Three arguments will be passed to to them: 1) user name of user (de)assigned 2) path to workfiles of task user was (de)assigned to 3) path to publish files of task user was (de)assigned to """ db_con = DbConnector() def error(self, *err): for e in err: self.log.error(e) def _run_script(self, script, args): """ Run shell script with arguments as subprocess :param script: script path :type script: str :param args: list of arguments passed to script :type args: list :returns: return code :rtype: int """ p = subprocess.call([script, args], shell=True) return p def _get_task_and_user(self, session, action, changes): """ Get Task and User entities from Ftrack session :param session: ftrack session :type session: ftrack_api.session :param action: event action :type action: str :param changes: what was changed by event :type changes: dict :returns: User and Task entities :rtype: tuple """ if not changes: return None, None if action == 'add': task_id = changes.get('context_id', {}).get('new') user_id = changes.get('resource_id', {}).get('new') elif action == 'remove': task_id = changes.get('context_id', {}).get('old') user_id = changes.get('resource_id', {}).get('old') if not task_id: return None, None if not user_id: return None, None task = session.query('Task where id is "{}"'.format(task_id)).one() user = session.query('User where id is "{}"'.format(user_id)).one() return task, user def _get_asset(self, task): """ Get asset from task entity :param task: Task entity :type task: dict :returns: Asset entity :rtype: dict """ parent = task['parent'] self.db_con.install() self.db_con.Session['AVALON_PROJECT'] = task['project']['full_name'] avalon_entity = None parent_id = parent['custom_attributes'].get(CustAttrIdKey) if parent_id: parent_id = ObjectId(parent_id) avalon_entity = self.db_con.find_one({ '_id': parent_id, 'type': 'asset' }) if not avalon_entity: avalon_entity = self.db_con.find_one({ 'type': 'asset', 'name': parent['name'] }) if not avalon_entity: self.db_con.uninstall() msg = 'Entity "{}" not found in avalon database'.format( parent['name']) self.error(msg) return {'success': False, 'message': msg} self.db_con.uninstall() return avalon_entity def _get_hierarchy(self, asset): """ Get hierarchy from Asset entity :param asset: Asset entity :type asset: dict :returns: hierarchy string :rtype: str """ return asset['data']['hierarchy'] def _get_template_data(self, task): """ Get data to fill template from task .. seealso:: :mod:`pypeapp.Anatomy` :param task: Task entity :type task: dict :returns: data for anatomy template :rtype: dict """ project_name = task['project']['full_name'] project_code = task['project']['name'] try: root = os.environ['PYPE_STUDIO_PROJECTS_PATH'] except KeyError: msg = 'Project ({}) root not set'.format(project_name) self.log.error(msg) return {'success': False, 'message': msg} # fill in template data asset = self._get_asset(task) t_data = { 'root': root, 'project': { 'name': project_name, 'code': project_code }, 'asset': asset['name'], 'task': task['name'], 'hierarchy': self._get_hierarchy(asset) } return t_data def launch(self, session, event): # load shell scripts presets presets = config.get_presets()['ftrack'].get("user_assigment_event") if not presets: return for entity in event.get('data', {}).get('entities', []): if entity.get('entity_type') != 'Appointment': continue task, user = self._get_task_and_user(session, entity.get('action'), entity.get('changes')) if not task or not user: self.log.error('Task or User was not found.') continue data = self._get_template_data(task) # format directories to pass to shell script anatomy = Anatomy(data["project"]["name"]) # formatting work dir is easiest part as we can use whole path work_dir = anatomy.format(data)['avalon']['work'] # we also need publish but not whole filled_all = anatomy.format_all(data) publish = filled_all['avalon']['publish'] # now find path to {asset} m = re.search("(^.+?{})".format(data['asset']), publish) if not m: msg = 'Cannot get part of publish path {}'.format(publish) self.log.error(msg) return {'success': False, 'message': msg} publish_dir = m.group(1) for script in presets.get(entity.get('action')): self.log.info('[{}] : running script for user {}'.format( entity.get('action'), user["username"])) self._run_script(script, [user["username"], work_dir, publish_dir]) return True
class SyncHierarchicalAttrs(BaseAction): db_con = DbConnector() ca_mongoid = lib.get_ca_mongoid() #: Action identifier. identifier = 'sync.hierarchical.attrs' #: Action label. label = "Pype Admin" variant = '- Sync Hier Attrs (Server)' #: Action description. description = 'Synchronize hierarchical attributes' #: Icon icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get( 'PYPE_STATICS_SERVER', 'http://localhost:{}'.format(config.get_presets().get( 'services', {}).get('statics_server', {}).get('default_port', 8021)))) def register(self): self.session.event_hub.subscribe('topic=ftrack.action.discover', self._discover) self.session.event_hub.subscribe( 'topic=ftrack.action.launch and data.actionIdentifier={}'.format( self.identifier), self._launch) def discover(self, session, entities, event): ''' Validation ''' role_check = False discover = False role_list = ['Pypeclub', 'Administrator', 'Project Manager'] user = session.query('User where id is "{}"'.format( event['source']['user']['id'])).one() for role in user['user_security_roles']: if role['security_role']['name'] in role_list: role_check = True break if role_check is True: for entity in entities: context_type = entity.get('context_type', '').lower() if (context_type in ('show', 'task') and entity.entity_type.lower() != 'task'): discover = True break return discover def launch(self, session, entities, event): self.interface_messages = {} user = session.query('User where id is "{}"'.format( event['source']['user']['id'])).one() job = session.create( 'Job', { 'user': user, 'status': 'running', 'data': json.dumps( {'description': 'Sync Hierachical attributes'}) }) session.commit() self.log.debug('Job with id "{}" created'.format(job['id'])) process_session = ftrack_api.Session(server_url=session.server_url, api_key=session.api_key, api_user=session.api_user, auto_connect_event_hub=True) try: # Collect hierarchical attrs self.log.debug('Collecting Hierarchical custom attributes started') custom_attributes = {} all_avalon_attr = process_session.query( 'CustomAttributeGroup where name is "avalon"').one() error_key = ( 'Hierarchical attributes with set "default" value (not allowed)' ) for cust_attr in all_avalon_attr[ 'custom_attribute_configurations']: if 'avalon_' in cust_attr['key']: continue if not cust_attr['is_hierarchical']: continue if cust_attr['default']: if error_key not in self.interface_messages: self.interface_messages[error_key] = [] self.interface_messages[error_key].append( cust_attr['label']) self.log.warning( ('Custom attribute "{}" has set default value.' ' This attribute can\'t be synchronized').format( cust_attr['label'])) continue custom_attributes[cust_attr['key']] = cust_attr self.log.debug( 'Collecting Hierarchical custom attributes has finished') if not custom_attributes: msg = 'No hierarchical attributes to sync.' self.log.debug(msg) return {'success': True, 'message': msg} entity = entities[0] if entity.entity_type.lower() == 'project': project_name = entity['full_name'] else: project_name = entity['project']['full_name'] self.db_con.install() self.db_con.Session['AVALON_PROJECT'] = project_name _entities = self._get_entities(event, process_session) for entity in _entities: self.log.debug(30 * '-') self.log.debug('Processing entity "{}"'.format( entity.get('name', entity))) ent_name = entity.get('name', entity) if entity.entity_type.lower() == 'project': ent_name = entity['full_name'] for key in custom_attributes: self.log.debug(30 * '*') self.log.debug( 'Processing Custom attribute key "{}"'.format(key)) # check if entity has that attribute if key not in entity['custom_attributes']: error_key = 'Missing key on entities' if error_key not in self.interface_messages: self.interface_messages[error_key] = [] self.interface_messages[error_key].append( '- key: "{}" - entity: "{}"'.format(key, ent_name)) self.log.error(('- key "{}" not found on "{}"').format( key, entity.get('name', entity))) continue value = self.get_hierarchical_value(key, entity) if value is None: error_key = ( 'Missing value for key on entity' ' and its parents (synchronization was skipped)') if error_key not in self.interface_messages: self.interface_messages[error_key] = [] self.interface_messages[error_key].append( '- key: "{}" - entity: "{}"'.format(key, ent_name)) self.log.warning( ('- key "{}" not set on "{}" or its parents' ).format(key, ent_name)) continue self.update_hierarchical_attribute(entity, key, value) job['status'] = 'done' session.commit() except Exception: self.log.error('Action "{}" failed'.format(self.label), exc_info=True) finally: self.db_con.uninstall() if job['status'] in ('queued', 'running'): job['status'] = 'failed' session.commit() if self.interface_messages: self.show_interface_from_dict(messages=self.interface_messages, title="something went wrong", event=event) return True def get_hierarchical_value(self, key, entity): value = entity['custom_attributes'][key] if (value is not None or entity.entity_type.lower() == 'project'): return value return self.get_hierarchical_value(key, entity['parent']) def update_hierarchical_attribute(self, entity, key, value): if (entity['context_type'].lower() not in ('show', 'task') or entity.entity_type.lower() == 'task'): return ent_name = entity.get('name', entity) if entity.entity_type.lower() == 'project': ent_name = entity['full_name'] hierarchy = '/'.join([a['name'] for a in entity.get('ancestors', [])]) if hierarchy: hierarchy = '/'.join( [entity['project']['full_name'], hierarchy, entity['name']]) elif entity.entity_type.lower() == 'project': hierarchy = entity['full_name'] else: hierarchy = '/'.join( [entity['project']['full_name'], entity['name']]) self.log.debug('- updating entity "{}"'.format(hierarchy)) # collect entity's custom attributes custom_attributes = entity.get('custom_attributes') if not custom_attributes: return mongoid = custom_attributes.get(self.ca_mongoid) if not mongoid: error_key = 'Missing MongoID on entities (try SyncToAvalon first)' if error_key not in self.interface_messages: self.interface_messages[error_key] = [] if ent_name not in self.interface_messages[error_key]: self.interface_messages[error_key].append(ent_name) self.log.warning( '-- entity "{}" is not synchronized to avalon. Skipping'. format(ent_name)) return try: mongoid = ObjectId(mongoid) except Exception: error_key = 'Invalid MongoID on entities (try SyncToAvalon)' if error_key not in self.interface_messages: self.interface_messages[error_key] = [] if ent_name not in self.interface_messages[error_key]: self.interface_messages[error_key].append(ent_name) self.log.warning( '-- entity "{}" has stored invalid MongoID. Skipping'.format( ent_name)) return # Find entity in Mongo DB mongo_entity = self.db_con.find_one({'_id': mongoid}) if not mongo_entity: error_key = 'Entities not found in Avalon DB (try SyncToAvalon)' if error_key not in self.interface_messages: self.interface_messages[error_key] = [] if ent_name not in self.interface_messages[error_key]: self.interface_messages[error_key].append(ent_name) self.log.warning( '-- entity "{}" was not found in DB by id "{}". Skipping'. format(ent_name, str(mongoid))) return # Change value if entity has set it's own entity_value = custom_attributes[key] if entity_value is not None: value = entity_value data = mongo_entity.get('data') or {} data[key] = value self.db_con.update_many({'_id': mongoid}, {'$set': {'data': data}}) for child in entity.get('children', []): self.update_hierarchical_attribute(child, key, value)
class SyncHierarchicalAttrs(BaseEvent): # After sync to avalon event! priority = 101 db_con = DbConnector() ca_mongoid = lib.get_ca_mongoid() def launch(self, session, event): # Filter entities and changed values if it makes sence to run script processable = [] processable_ent = {} for ent in event['data']['entities']: # Ignore entities that are not tasks or projects if ent['entityType'].lower() not in ['task', 'show']: continue action = ent.get("action") # skip if remove (Entity does not exist in Ftrack) if action == "remove": continue # When entity was add we don't care about keys if action != "add": keys = ent.get('keys') if not keys: continue entity = session.get(self._get_entity_type(ent), ent['entityId']) processable.append(ent) processable_ent[ent['entityId']] = { "entity": entity, "action": action, "link": entity["link"] } if not processable: return True # Find project of entities ft_project = None for entity_dict in processable_ent.values(): try: base_proj = entity_dict['link'][0] except Exception: continue ft_project = session.get(base_proj['type'], base_proj['id']) break # check if project is set to auto-sync if (ft_project is None or 'avalon_auto_sync' not in ft_project['custom_attributes'] or ft_project['custom_attributes']['avalon_auto_sync'] is False): return True # Get hierarchical custom attributes from "avalon" group custom_attributes = {} query = 'CustomAttributeGroup where name is "avalon"' all_avalon_attr = session.query(query).one() for cust_attr in all_avalon_attr['custom_attribute_configurations']: if 'avalon_' in cust_attr['key']: continue if not cust_attr['is_hierarchical']: continue custom_attributes[cust_attr['key']] = cust_attr if not custom_attributes: return True self.db_con.install() self.db_con.Session['AVALON_PROJECT'] = ft_project['full_name'] for ent in processable: entity_dict = processable_ent[ent['entityId']] entity = entity_dict["entity"] ent_path = "/".join([ent["name"] for ent in entity_dict['link']]) action = entity_dict["action"] keys_to_process = {} if action == "add": # Store all custom attributes when entity was added for key in custom_attributes: keys_to_process[key] = entity['custom_attributes'][key] else: # Update only updated keys for key in ent['keys']: if key in custom_attributes: keys_to_process[key] = entity['custom_attributes'][key] processed_keys = self.get_hierarchical_values( keys_to_process, entity) # Do the processing of values self.update_hierarchical_attribute(entity, processed_keys, ent_path) self.db_con.uninstall() return True def get_hierarchical_values(self, keys_dict, entity): # check already set values _set_keys = [] for key, value in keys_dict.items(): if value is not None: _set_keys.append(key) # pop set values from keys_dict set_keys = {} for key in _set_keys: set_keys[key] = keys_dict.pop(key) # find if entity has set values and pop them out keys_to_pop = [] for key in keys_dict.keys(): _val = entity["custom_attributes"][key] if _val: keys_to_pop.append(key) set_keys[key] = _val for key in keys_to_pop: keys_dict.pop(key) # if there are not keys to find value return found if not keys_dict: return set_keys # end recursion if entity is project if entity.entity_type.lower() == "project": for key, value in keys_dict.items(): set_keys[key] = value else: result = self.get_hierarchical_values(keys_dict, entity["parent"]) for key, value in result.items(): set_keys[key] = value return set_keys def update_hierarchical_attribute(self, entity, keys_dict, ent_path): # TODO store all keys at once for entity custom_attributes = entity.get('custom_attributes') if not custom_attributes: return mongoid = custom_attributes.get(self.ca_mongoid) if not mongoid: return try: mongoid = ObjectId(mongoid) except Exception: return mongo_entity = self.db_con.find_one({'_id': mongoid}) if not mongo_entity: return changed_keys = {} data = mongo_entity.get('data') or {} for key, value in keys_dict.items(): cur_value = data.get(key) if cur_value: if cur_value == value: continue changed_keys[key] = value data[key] = value if not changed_keys: return self.log.debug("{} - updated hierarchical attributes: {}".format( ent_path, str(changed_keys))) self.db_con.update_many({'_id': mongoid}, {'$set': {'data': data}}) for child in entity.get('children', []): _keys_dict = {} for key, value in keys_dict.items(): if key not in child.get('custom_attributes', {}): continue child_value = child['custom_attributes'][key] if child_value is not None: continue _keys_dict[key] = value if not _keys_dict: continue child_path = "/".join([ent["name"] for ent in child['link']]) self.update_hierarchical_attribute(child, _keys_dict, child_path)
class CreateFolders(BaseAction): '''Custom action.''' #: Action identifier. identifier = 'create.folders' #: Action label. label = 'Create Folders' #: Action Icon. icon = '{}/ftrack/action_icons/CreateFolders.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '')) db = DbConnector() def discover(self, session, entities, event): ''' Validation ''' if len(entities) != 1: return False not_allowed = ['assetversion', 'project'] if entities[0].entity_type.lower() in not_allowed: return False return True def interface(self, session, entities, event): if event['data'].get('values', {}): return entity = entities[0] without_interface = True for child in entity['children']: if child['object_type']['name'].lower() != 'task': without_interface = False break self.without_interface = without_interface if without_interface: return title = 'Create folders' entity_name = entity['name'] msg = ('<h2>Do you want create folders also' ' for all children of "{}"?</h2>') if entity.entity_type.lower() == 'project': entity_name = entity['full_name'] msg = msg.replace(' also', '') msg += '<h3>(Project root won\'t be created if not checked)</h3>' items = [] item_msg = {'type': 'label', 'value': msg.format(entity_name)} item_label = {'type': 'label', 'value': 'With all chilren entities'} item = {'name': 'children_included', 'type': 'boolean', 'value': False} items.append(item_msg) items.append(item_label) items.append(item) if len(items) == 0: return { 'success': False, 'message': 'Didn\'t found any running jobs' } else: return {'items': items, 'title': title} def launch(self, session, entities, event): '''Callback method for custom action.''' with_childrens = True if self.without_interface is False: if 'values' not in event['data']: return with_childrens = event['data']['values']['children_included'] entity = entities[0] if entity.entity_type.lower() == 'project': proj = entity else: proj = entity['project'] project_name = proj['full_name'] project_code = proj['name'] if entity.entity_type.lower() == 'project' and with_childrens == False: return {'success': True, 'message': 'Nothing was created'} data = { "root": os.environ["AVALON_PROJECTS"], "project": { "name": project_name, "code": project_code } } all_entities = [] all_entities.append(entity) if with_childrens: all_entities = self.get_notask_children(entity) av_project = None try: self.db.install() self.db.Session['AVALON_PROJECT'] = project_name av_project = self.db.find_one({'type': 'project'}) template_work = av_project['config']['template']['work'] template_publish = av_project['config']['template']['publish'] self.db.uninstall() except Exception: templates = Anatomy().templates template_work = templates["avalon"]["work"] template_publish = templates["avalon"]["publish"] collected_paths = [] presets = config.get_presets()['tools']['sw_folders'] for entity in all_entities: if entity.entity_type.lower() == 'project': continue ent_data = data.copy() asset_name = entity['name'] ent_data['asset'] = asset_name parents = entity['link'] hierarchy_names = [p['name'] for p in parents[1:-1]] hierarchy = '' if hierarchy_names: hierarchy = os.path.sep.join(hierarchy_names) ent_data['hierarchy'] = hierarchy tasks_created = False if entity['children']: for child in entity['children']: if child['object_type']['name'].lower() != 'task': continue tasks_created = True task_type_name = child['type']['name'].lower() task_data = ent_data.copy() task_data['task'] = child['name'] possible_apps = presets.get(task_type_name, []) template_work_created = False template_publish_created = False apps = [] for app in possible_apps: try: app_data = avalonlib.get_application(app) app_dir = app_data['application_dir'] except ValueError: app_dir = app apps.append(app_dir) # Template wok if '{app}' in template_work: for app in apps: template_work_created = True app_data = task_data.copy() app_data['app'] = app collected_paths.append( self.compute_template(template_work, app_data)) if template_work_created is False: collected_paths.append( self.compute_template(template_work, task_data)) # Template publish if '{app}' in template_publish: for app in apps: template_publish_created = True app_data = task_data.copy() app_data['app'] = app collected_paths.append( self.compute_template(template_publish, app_data, True)) if template_publish_created is False: collected_paths.append( self.compute_template(template_publish, task_data, True)) if not tasks_created: # create path for entity collected_paths.append( self.compute_template(template_work, ent_data)) collected_paths.append( self.compute_template(template_publish, ent_data)) if len(collected_paths) > 0: self.log.info('Creating folders:') for path in set(collected_paths): self.log.info(path) if not os.path.exists(path): os.makedirs(path) return {'success': True, 'message': 'Created Folders Successfully!'} def get_notask_children(self, entity): output = [] if entity.get('object_type', {}).get('name', entity.entity_type).lower() == 'task': return output else: output.append(entity) if entity['children']: for child in entity['children']: output.extend(self.get_notask_children(child)) return output def template_format(self, template, data): partial_data = PartialDict(data) # remove subdict items from string (like 'project[name]') subdict = PartialDict() count = 1 store_pattern = 5 * '_' + '{:0>3}' regex_patern = "\{\w*\[[^\}]*\]\}" matches = re.findall(regex_patern, template) for match in matches: key = store_pattern.format(count) subdict[key] = match template = template.replace(match, '{' + key + '}') count += 1 # solve fillind keys with optional keys solved = self._solve_with_optional(template, partial_data) # try to solve subdict and replace them back to string for k, v in subdict.items(): try: v = v.format_map(data) except (KeyError, TypeError): pass subdict[k] = v return solved.format_map(subdict) def _solve_with_optional(self, template, data): # Remove optional missing keys pattern = re.compile(r"(<.*?[^{0]*>)[^0-9]*?") invalid_optionals = [] for group in pattern.findall(template): try: group.format(**data) except KeyError: invalid_optionals.append(group) for group in invalid_optionals: template = template.replace(group, "") solved = template.format_map(data) # solving after format optional in second round for catch in re.compile(r"(<.*?[^{0]*>)[^0-9]*?").findall(solved): if "{" in catch: # remove all optional solved = solved.replace(catch, "") else: # Remove optional symbols solved = solved.replace(catch, catch[1:-1]) return solved def compute_template(self, str, data, task=False): first_result = self.template_format(str, data) if first_result == first_result.split('{')[0]: return os.path.normpath(first_result) if task: return os.path.normpath(first_result.split('{')[0]) index = first_result.index('{') regex = '\{\w*[^\}]*\}' match = re.findall(regex, first_result[index:])[0] without_missing = str.split(match)[0].split('}') output_items = [] for part in without_missing: if '{' in part: output_items.append(part + '}') return os.path.normpath( self.template_format(''.join(output_items), data))
class DeleteOldVersions(BaseAction): identifier = "delete.old.versions" label = "Pype Admin" variant = "- Delete old versions" description = ("Delete files from older publishes so project can be" " archived with only lates versions.") role_list = ["Pypeclub", "Project Manager", "Administrator"] icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format( os.environ.get('PYPE_STATICS_SERVER', '')) dbcon = DbConnector() inteface_title = "Choose your preferences" splitter_item = {"type": "label", "value": "---"} sequence_splitter = "__sequence_splitter__" def discover(self, session, entities, event): ''' Validation ''' selection = event["data"].get("selection") or [] for entity in selection: entity_type = (entity.get("entityType") or "").lower() if entity_type == "assetversion": return True return False def interface(self, session, entities, event): items = [] root = os.environ.get("AVALON_PROJECTS") if not root: msg = "Root path to projects is not set." items.append({ "type": "label", "value": "<i><b>ERROR:</b> {}</i>".format(msg) }) self.show_interface(items=items, title=self.inteface_title, event=event) return {"success": False, "message": msg} if not os.path.exists(root): msg = "Root path does not exists \"{}\".".format(str(root)) items.append({ "type": "label", "value": "<i><b>ERROR:</b> {}</i>".format(msg) }) self.show_interface(items=items, title=self.inteface_title, event=event) return {"success": False, "message": msg} values = event["data"].get("values") if values: versions_count = int(values["last_versions_count"]) if versions_count >= 1: return items.append({ "type": "label", "value": ("# You have to keep at least 1 version!") }) items.append({ "type": "label", "value": ("<i><b>WARNING:</b> This will remove published files of older" " versions from disk so we don't recommend use" " this action on \"live\" project.</i>") }) items.append(self.splitter_item) # How many versions to keep items.append({ "type": "label", "value": "## Choose how many versions you want to keep:" }) items.append({ "type": "label", "value": ("<i><b>NOTE:</b> We do recommend to keep 2 versions.</i>") }) items.append({ "type": "number", "name": "last_versions_count", "label": "Versions", "value": 2 }) items.append(self.splitter_item) items.append({ "type": "label", "value": ("## Remove publish folder even if there" " are other than published files:") }) items.append({ "type": "label", "value": ("<i><b>WARNING:</b> This may remove more than you want.</i>") }) items.append({ "type": "boolean", "name": "force_delete_publish_folder", "label": "Are You sure?", "value": False }) return {"items": items, "title": self.inteface_title} def launch(self, session, entities, event): values = event["data"].get("values") if not values: return versions_count = int(values["last_versions_count"]) force_to_remove = values["force_delete_publish_folder"] _val1 = "OFF" if force_to_remove: _val1 = "ON" _val3 = "s" if versions_count == 1: _val3 = "" self.log.debug( ("Process started. Force to delete publish folder is set to [{0}]" " and will keep {1} latest version{2}.").format( _val1, versions_count, _val3)) self.dbcon.install() project = None avalon_asset_names = [] asset_versions_by_parent_id = collections.defaultdict(list) subset_names_by_asset_name = collections.defaultdict(list) ftrack_assets_by_name = {} for entity in entities: ftrack_asset = entity["asset"] parent_ent = ftrack_asset["parent"] parent_ftrack_id = parent_ent["id"] parent_name = parent_ent["name"] if parent_name not in avalon_asset_names: avalon_asset_names.append(parent_name) # Group asset versions by parent entity asset_versions_by_parent_id[parent_ftrack_id].append(entity) # Get project if project is None: project = parent_ent["project"] # Collect subset names per asset subset_name = ftrack_asset["name"] subset_names_by_asset_name[parent_name].append(subset_name) if subset_name not in ftrack_assets_by_name: ftrack_assets_by_name[subset_name] = ftrack_asset # Set Mongo collection project_name = project["full_name"] self.dbcon.Session["AVALON_PROJECT"] = project_name self.log.debug("Project is set to {}".format(project_name)) # Get Assets from avalon database assets = list( self.dbcon.find({ "type": "asset", "name": { "$in": avalon_asset_names } })) asset_id_to_name_map = { asset["_id"]: asset["name"] for asset in assets } asset_ids = list(asset_id_to_name_map.keys()) self.log.debug("Collected assets ({})".format(len(asset_ids))) # Get Subsets subsets = list( self.dbcon.find({ "type": "subset", "parent": { "$in": asset_ids } })) subsets_by_id = {} subset_ids = [] for subset in subsets: asset_id = subset["parent"] asset_name = asset_id_to_name_map[asset_id] available_subsets = subset_names_by_asset_name[asset_name] if subset["name"] not in available_subsets: continue subset_ids.append(subset["_id"]) subsets_by_id[subset["_id"]] = subset self.log.debug("Collected subsets ({})".format(len(subset_ids))) # Get Versions versions = list( self.dbcon.find({ "type": "version", "parent": { "$in": subset_ids } })) versions_by_parent = collections.defaultdict(list) for ent in versions: versions_by_parent[ent["parent"]].append(ent) def sort_func(ent): return int(ent["name"]) all_last_versions = [] for parent_id, _versions in versions_by_parent.items(): for idx, version in enumerate( sorted(_versions, key=sort_func, reverse=True)): if idx >= versions_count: break all_last_versions.append(version) self.log.debug("Collected versions ({})".format(len(versions))) # Filter latest versions for version in all_last_versions: versions.remove(version) # Update versions_by_parent without filtered versions versions_by_parent = collections.defaultdict(list) for ent in versions: versions_by_parent[ent["parent"]].append(ent) # Filter already deleted versions versions_to_pop = [] for version in versions: version_tags = version["data"].get("tags") if version_tags and "deleted" in version_tags: versions_to_pop.append(version) for version in versions_to_pop: subset = subsets_by_id[version["parent"]] asset_id = subset["parent"] asset_name = asset_id_to_name_map[asset_id] msg = "Asset: \"{}\" | Subset: \"{}\" | Version: \"{}\"".format( asset_name, subset["name"], version["name"]) self.log.warning( ("Skipping version. Already tagged as `deleted`. < {} >" ).format(msg)) versions.remove(version) version_ids = [ent["_id"] for ent in versions] self.log.debug("Filtered versions to delete ({})".format( len(version_ids))) if not version_ids: msg = "Skipping processing. Nothing to delete." self.log.debug(msg) return {"success": True, "message": msg} repres = list( self.dbcon.find({ "type": "representation", "parent": { "$in": version_ids } })) self.log.debug("Collected representations to remove ({})".format( len(repres))) dir_paths = {} file_paths_by_dir = collections.defaultdict(list) for repre in repres: file_path, seq_path = self.path_from_represenation(repre) if file_path is None: self.log.warning( ("Could not format path for represenation \"{}\"").format( str(repre))) continue dir_path = os.path.dirname(file_path) dir_id = None for _dir_id, _dir_path in dir_paths.items(): if _dir_path == dir_path: dir_id = _dir_id break if dir_id is None: dir_id = uuid.uuid4() dir_paths[dir_id] = dir_path file_paths_by_dir[dir_id].append([file_path, seq_path]) dir_ids_to_pop = [] for dir_id, dir_path in dir_paths.items(): if os.path.exists(dir_path): continue dir_ids_to_pop.append(dir_id) # Pop dirs from both dictionaries for dir_id in dir_ids_to_pop: dir_paths.pop(dir_id) paths = file_paths_by_dir.pop(dir_id) # TODO report of missing directories? paths_msg = ", ".join( ["'{}'".format(path[0].replace("\\", "/")) for path in paths]) self.log.warning( ("Folder does not exist. Deleting it's files skipped: {}" ).format(paths_msg)) if force_to_remove: self.delete_whole_dir_paths(dir_paths.values()) else: self.delete_only_repre_files(dir_paths, file_paths_by_dir) mongo_changes_bulk = [] for version in versions: orig_version_tags = version["data"].get("tags") or [] version_tags = [tag for tag in orig_version_tags] if "deleted" not in version_tags: version_tags.append("deleted") if version_tags == orig_version_tags: continue update_query = {"_id": version["_id"]} update_data = {"$set": {"data.tags": version_tags}} mongo_changes_bulk.append(UpdateOne(update_query, update_data)) if mongo_changes_bulk: self.dbcon.bulk_write(mongo_changes_bulk) self.dbcon.uninstall() # Set attribute `is_published` to `False` on ftrack AssetVersions for subset_id, _versions in versions_by_parent.items(): subset_name = None for subset in subsets: if subset["_id"] == subset_id: subset_name = subset["name"] break if subset_name is None: self.log.warning("Subset with ID `{}` was not found.".format( str(subset_id))) continue ftrack_asset = ftrack_assets_by_name.get(subset_name) if not ftrack_asset: self.log.warning(("Could not find Ftrack asset with name `{}`" ).format(subset_name)) continue version_numbers = [int(ver["name"]) for ver in _versions] for version in ftrack_asset["versions"]: if int(version["version"]) in version_numbers: version["is_published"] = False try: session.commit() except Exception: msg = ("Could not set `is_published` attribute to `False`" " for selected AssetVersions.") self.log.warning(msg, exc_info=True) return {"success": False, "message": msg} return True def delete_whole_dir_paths(self, dir_paths): for dir_path in dir_paths: # Delete all files and fodlers in dir path for root, dirs, files in os.walk(dir_path, topdown=False): for name in files: os.remove(os.path.join(root, name)) for name in dirs: os.rmdir(os.path.join(root, name)) # Delete even the folder and it's parents folders if they are empty while True: if not os.path.exists(dir_path): dir_path = os.path.dirname(dir_path) continue if len(os.listdir(dir_path)) != 0: break os.rmdir(os.path.join(dir_path)) def delete_only_repre_files(self, dir_paths, file_paths): for dir_id, dir_path in dir_paths.items(): dir_files = os.listdir(dir_path) collections, remainders = clique.assemble(dir_files) for file_path, seq_path in file_paths[dir_id]: file_path_base = os.path.split(file_path)[1] # Just remove file if `frame` key was not in context or # filled path is in remainders (single file sequence) if not seq_path or file_path_base in remainders: if not os.path.exists(file_path): self.log.warning( "File was not found: {}".format(file_path)) continue os.remove(file_path) self.log.debug("Removed file: {}".format(file_path)) remainders.remove(file_path_base) continue seq_path_base = os.path.split(seq_path)[1] head, tail = seq_path_base.split(self.sequence_splitter) final_col = None for collection in collections: if head != collection.head or tail != collection.tail: continue final_col = collection break if final_col is not None: # Fill full path to head final_col.head = os.path.join(dir_path, final_col.head) for _file_path in final_col: if os.path.exists(_file_path): os.remove(_file_path) _seq_path = final_col.format("{head}{padding}{tail}") self.log.debug("Removed files: {}".format(_seq_path)) collections.remove(final_col) elif os.path.exists(file_path): os.remove(file_path) self.log.debug("Removed file: {}".format(file_path)) else: self.log.warning( "File was not found: {}".format(file_path)) # Delete as much as possible parent folders for dir_path in dir_paths.values(): while True: if not os.path.exists(dir_path): dir_path = os.path.dirname(dir_path) continue if len(os.listdir(dir_path)) != 0: break self.log.debug("Removed folder: {}".format(dir_path)) os.rmdir(dir_path) def path_from_represenation(self, representation): try: template = representation["data"]["template"] except KeyError: return (None, None) root = os.environ["AVALON_PROJECTS"] if not root: return (None, None) sequence_path = None try: context = representation["context"] context["root"] = root path = avalon.pipeline.format_template_with_optional_keys( context, template) if "frame" in context: context["frame"] = self.sequence_splitter sequence_path = os.path.normpath( avalon.pipeline.format_template_with_optional_keys( context, template)) except KeyError: # Template references unavailable data return (None, None) return (os.path.normpath(path), sequence_path)
class DeleteAssetSubset(BaseAction): '''Edit meta data action.''' #: Action identifier. identifier = "delete.asset.subset" #: Action label. label = "Delete Asset/Subsets" #: Action description. description = "Removes from Avalon with all childs and asset from Ftrack" 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", "Project Manager"] #: Db connection dbcon = DbConnector() splitter = {"type": "label", "value": "---"} action_data_by_id = {} asset_prefix = "asset:" subset_prefix = "subset:" def discover(self, session, entities, event): """ Validation """ task_ids = [] for ent_info in event["data"]["selection"]: entType = ent_info.get("entityType", "") if entType == "task": task_ids.append(ent_info["entityId"]) for entity in entities: ftrack_id = entity["id"] if ftrack_id not in task_ids: continue if entity.entity_type.lower() != "task": return True return False def _launch(self, event): try: args = self._translate_event(self.session, event) if "values" not in event["data"]: self.dbcon.install() return self._interface(self.session, *args) confirmation = self.confirm_delete(*args) if confirmation: return confirmation self.dbcon.install() response = self.launch(self.session, *args) finally: self.dbcon.uninstall() return self._handle_result(self.session, response, *args) def interface(self, session, entities, event): self.show_message(event, "Preparing data...", True) items = [] title = "Choose items to delete" # Filter selection and get ftrack ids selection = event["data"].get("selection") or [] ftrack_ids = [] project_in_selection = False for entity in selection: entity_type = (entity.get("entityType") or "").lower() if entity_type != "task": if entity_type == "show": project_in_selection = True continue ftrack_id = entity.get("entityId") if not ftrack_id: continue ftrack_ids.append(ftrack_id) if project_in_selection: msg = "It is not possible to use this action on project entity." self.show_message(event, msg, True) # Filter event even more (skip task entities) # - task entities are not relevant for avalon entity_mapping = {} for entity in entities: ftrack_id = entity["id"] if ftrack_id not in ftrack_ids: continue if entity.entity_type.lower() == "task": ftrack_ids.remove(ftrack_id) entity_mapping[ftrack_id] = entity if not ftrack_ids: # It is bug if this happens! return { "success": False, "message": "Invalid selection for this action (Bug)" } if entities[0].entity_type.lower() == "project": project = entities[0] else: project = entities[0]["project"] project_name = project["full_name"] self.dbcon.Session["AVALON_PROJECT"] = project_name selected_av_entities = list( self.dbcon.find({ "type": "asset", "data.ftrackId": { "$in": ftrack_ids } })) found_without_ftrack_id = {} if len(selected_av_entities) != len(ftrack_ids): found_ftrack_ids = [ ent["data"]["ftrackId"] for ent in selected_av_entities ] for ftrack_id, entity in entity_mapping.items(): if ftrack_id in found_ftrack_ids: continue av_ents_by_name = list( self.dbcon.find({ "type": "asset", "name": entity["name"] })) if not av_ents_by_name: continue ent_path_items = [ent["name"] for ent in entity["link"]] parents = ent_path_items[1:len(ent_path_items) - 1:] # TODO we should say to user that # few of them are missing in avalon for av_ent in av_ents_by_name: if av_ent["data"]["parents"] != parents: continue # TODO we should say to user that found entity # with same name does not match same ftrack id? if "ftrackId" not in av_ent["data"]: selected_av_entities.append(av_ent) found_without_ftrack_id[str(av_ent["_id"])] = ftrack_id break if not selected_av_entities: return { "success": False, "message": "Didn't found entities in avalon" } # Remove cached action older than 2 minutes old_action_ids = [] for id, data in self.action_data_by_id.items(): created_at = data.get("created_at") if not created_at: old_action_ids.append(id) continue cur_time = datetime.now() existing_in_sec = (created_at - cur_time).total_seconds() if existing_in_sec > 60 * 2: old_action_ids.append(id) for id in old_action_ids: self.action_data_by_id.pop(id, None) # Store data for action id action_id = str(uuid.uuid1()) self.action_data_by_id[action_id] = { "attempt": 1, "created_at": datetime.now(), "project_name": project_name, "subset_ids_by_name": {}, "subset_ids_by_parent": {}, "without_ftrack_id": found_without_ftrack_id } id_item = {"type": "hidden", "name": "action_id", "value": action_id} items.append(id_item) asset_ids = [ent["_id"] for ent in selected_av_entities] subsets_for_selection = self.dbcon.find({ "type": "subset", "parent": { "$in": asset_ids } }) asset_ending = "" if len(selected_av_entities) > 1: asset_ending = "s" asset_title = { "type": "label", "value": "# Delete asset{}:".format(asset_ending) } asset_note = { "type": "label", "value": ("<p><i>NOTE: Action will delete checked entities" " in Ftrack and Avalon with all children entities and" " published content.</i></p>") } items.append(asset_title) items.append(asset_note) asset_items = collections.defaultdict(list) for asset in selected_av_entities: ent_path_items = [project_name] ent_path_items.extend(asset.get("data", {}).get("parents") or []) ent_path_to_parent = "/".join(ent_path_items) + "/" asset_items[ent_path_to_parent].append(asset) for asset_parent_path, assets in sorted(asset_items.items()): items.append({ "type": "label", "value": "## <b>- {}</b>".format(asset_parent_path) }) for asset in assets: items.append({ "label": asset["name"], "name": "{}{}".format(self.asset_prefix, str(asset["_id"])), "type": 'boolean', "value": False }) subset_ids_by_name = collections.defaultdict(list) subset_ids_by_parent = collections.defaultdict(list) for subset in subsets_for_selection: subset_id = subset["_id"] name = subset["name"] parent_id = subset["parent"] subset_ids_by_name[name].append(subset_id) subset_ids_by_parent[parent_id].append(subset_id) if not subset_ids_by_name: return {"items": items, "title": title} subset_ending = "" if len(subset_ids_by_name.keys()) > 1: subset_ending = "s" subset_title = { "type": "label", "value": "# Subset{} to delete:".format(subset_ending) } subset_note = { "type": "label", "value": ("<p><i>WARNING: Subset{} will be removed" " for all <b>selected</b> entities.</i></p>" ).format(subset_ending) } items.append(self.splitter) items.append(subset_title) items.append(subset_note) for name in subset_ids_by_name: items.append({ "label": "<b>{}</b>".format(name), "name": "{}{}".format(self.subset_prefix, name), "type": "boolean", "value": False }) self.action_data_by_id[action_id]["subset_ids_by_parent"] = ( subset_ids_by_parent) self.action_data_by_id[action_id]["subset_ids_by_name"] = ( subset_ids_by_name) return {"items": items, "title": title} def confirm_delete(self, entities, event): values = event["data"]["values"] action_id = values.get("action_id") spec_data = self.action_data_by_id.get(action_id) if not spec_data: # it is a bug if this happens! return { "success": False, "message": "Something bad has happened. Please try again." } # Process Delete confirmation delete_key = values.get("delete_key") if delete_key: delete_key = delete_key.lower().strip() # Go to launch part if user entered `delete` if delete_key == "delete": return # Skip whole process if user didn't enter any text elif delete_key == "": self.action_data_by_id.pop(action_id, None) return { "success": True, "message": "Deleting cancelled (delete entry was empty)" } # Get data to show again to_delete = spec_data["to_delete"] else: to_delete = collections.defaultdict(list) for key, value in values.items(): if not value: continue if key.startswith(self.asset_prefix): _key = key.replace(self.asset_prefix, "") to_delete["assets"].append(_key) elif key.startswith(self.subset_prefix): _key = key.replace(self.subset_prefix, "") to_delete["subsets"].append(_key) self.action_data_by_id[action_id]["to_delete"] = to_delete asset_to_delete = len(to_delete.get("assets") or []) > 0 subset_to_delete = len(to_delete.get("subsets") or []) > 0 if not asset_to_delete and not subset_to_delete: self.action_data_by_id.pop(action_id, None) return { "success": True, "message": "Nothing was selected to delete" } attempt = spec_data["attempt"] if attempt > 3: self.action_data_by_id.pop(action_id, None) return { "success": False, "message": "You didn't enter \"DELETE\" properly 3 times!" } self.action_data_by_id[action_id]["attempt"] += 1 title = "Confirmation of deleting" if asset_to_delete: asset_len = len(to_delete["assets"]) asset_ending = "" if asset_len > 1: asset_ending = "s" title += " {} Asset{}".format(asset_len, asset_ending) if subset_to_delete: title += " and" if subset_to_delete: sub_len = len(to_delete["subsets"]) type_ending = "" sub_ending = "" if sub_len == 1: subset_ids_by_name = spec_data["subset_ids_by_name"] if len(subset_ids_by_name[to_delete["subsets"][0]]) > 1: sub_ending = "s" elif sub_len > 1: type_ending = "s" sub_ending = "s" title += " {} type{} of subset{}".format(sub_len, type_ending, sub_ending) items = [] id_item = {"type": "hidden", "name": "action_id", "value": action_id} delete_label = { 'type': 'label', 'value': '# Please enter "DELETE" to confirm #' } delete_item = { "name": "delete_key", "type": "text", "value": "", "empty_text": "Type Delete here..." } items.append(id_item) items.append(delete_label) items.append(delete_item) return {"items": items, "title": title} def launch(self, session, entities, event): self.show_message(event, "Processing...", True) values = event["data"]["values"] action_id = values.get("action_id") spec_data = self.action_data_by_id.get(action_id) if not spec_data: # it is a bug if this happens! return { "success": False, "message": "Something bad has happened. Please try again." } report_messages = collections.defaultdict(list) project_name = spec_data["project_name"] to_delete = spec_data["to_delete"] self.dbcon.Session["AVALON_PROJECT"] = project_name assets_to_delete = to_delete.get("assets") or [] subsets_to_delete = to_delete.get("subsets") or [] # Convert asset ids to ObjectId obj assets_to_delete = [ObjectId(id) for id in assets_to_delete if id] subset_ids_by_parent = spec_data["subset_ids_by_parent"] subset_ids_by_name = spec_data["subset_ids_by_name"] subset_ids_to_archive = [] asset_ids_to_archive = [] ftrack_ids_to_delete = [] if len(assets_to_delete) > 0: map_av_ftrack_id = spec_data["without_ftrack_id"] # Prepare data when deleting whole avalon asset avalon_assets = self.dbcon.find({"type": "asset"}) avalon_assets_by_parent = collections.defaultdict(list) for asset in avalon_assets: asset_id = asset["_id"] parent_id = asset["data"]["visualParent"] avalon_assets_by_parent[parent_id].append(asset) if asset_id in assets_to_delete: ftrack_id = map_av_ftrack_id.get(str(asset_id)) if not ftrack_id: ftrack_id = asset["data"].get("ftrackId") if not ftrack_id: continue ftrack_ids_to_delete.append(ftrack_id) children_queue = Queue() for mongo_id in assets_to_delete: children_queue.put(mongo_id) while not children_queue.empty(): mongo_id = children_queue.get() if mongo_id in asset_ids_to_archive: continue asset_ids_to_archive.append(mongo_id) for subset_id in subset_ids_by_parent.get(mongo_id, []): if subset_id not in subset_ids_to_archive: subset_ids_to_archive.append(subset_id) children = avalon_assets_by_parent.get(mongo_id) if not children: continue for child in children: child_id = child["_id"] if child_id not in asset_ids_to_archive: children_queue.put(child_id) # Prepare names of assets in ftrack and ids of subsets in mongo asset_names_to_delete = [] if len(subsets_to_delete) > 0: for name in subsets_to_delete: asset_names_to_delete.append(name) for subset_id in subset_ids_by_name[name]: if subset_id in subset_ids_to_archive: continue subset_ids_to_archive.append(subset_id) # Get ftrack ids of entities where will be delete only asset not_deleted_entities_id = [] ftrack_id_name_map = {} if asset_names_to_delete: for entity in entities: ftrack_id = entity["id"] ftrack_id_name_map[ftrack_id] = entity["name"] if ftrack_id in ftrack_ids_to_delete: continue not_deleted_entities_id.append(ftrack_id) mongo_proc_txt = "MongoProcessing: " ftrack_proc_txt = "Ftrack processing: " if asset_ids_to_archive: self.log.debug("{}Archivation of assets <{}>".format( mongo_proc_txt, ", ".join([str(id) for id in asset_ids_to_archive]))) self.dbcon.update_many( { "_id": { "$in": asset_ids_to_archive }, "type": "asset" }, {"$set": { "type": "archived_asset" }}) if subset_ids_to_archive: self.log.debug("{}Archivation of subsets <{}>".format( mongo_proc_txt, ", ".join([str(id) for id in subset_ids_to_archive]))) self.dbcon.update_many( { "_id": { "$in": subset_ids_to_archive }, "type": "subset" }, {"$set": { "type": "archived_subset" }}) if ftrack_ids_to_delete: self.log.debug("{}Deleting Ftrack Entities <{}>".format( ftrack_proc_txt, ", ".join(ftrack_ids_to_delete))) joined_ids_to_delete = ", ".join( ["\"{}\"".format(id) for id in ftrack_ids_to_delete]) ftrack_ents_to_delete = self.session.query( "select id, link from TypedContext where id in ({})".format( joined_ids_to_delete)).all() for entity in ftrack_ents_to_delete: self.session.delete(entity) try: self.session.commit() except Exception: ent_path = "/".join( [ent["name"] for ent in entity["link"]]) msg = "Failed to delete entity" report_messages[msg].append(ent_path) self.session.rollback() self.log.warning("{} <{}>".format(msg, ent_path), exc_info=True) if not_deleted_entities_id: joined_not_deleted = ", ".join([ "\"{}\"".format(ftrack_id) for ftrack_id in not_deleted_entities_id ]) joined_asset_names = ", ".join( ["\"{}\"".format(name) for name in asset_names_to_delete]) # Find assets of selected entities with names of checked subsets assets = self.session.query( ("select id from Asset where" " context_id in ({}) and name in ({})").format( joined_not_deleted, joined_asset_names)).all() self.log.debug("{}Deleting Ftrack Assets <{}>".format( ftrack_proc_txt, ", ".join([asset["id"] for asset in assets]))) for asset in assets: self.session.delete(asset) try: self.session.commit() except Exception: self.session.rollback() msg = "Failed to delete asset" report_messages[msg].append(asset["id"]) self.log.warning("{} <{}>".format(asset["id"]), exc_info=True) return self.report_handle(report_messages, project_name, event) def report_handle(self, report_messages, project_name, event): if not report_messages: return {"success": True, "message": "Deletion was successful!"} title = "Delete report ({}):".format(project_name) items = [] items.append({ "type": "label", "value": "# Deleting was not completely successful" }) items.append({ "type": "label", "value": "<p><i>Check logs for more information</i></p>" }) for msg, _items in report_messages.items(): if not _items or not msg: continue items.append({"type": "label", "value": "# {}".format(msg)}) if isinstance(_items, str): _items = [_items] items.append({ "type": "label", "value": '<p>{}</p>'.format("<br>".join(_items)) }) items.append(self.splitter) self.show_interface(items, title, event) return { "success": False, "message": "Deleting finished. Read report messages." }
class DeleteAsset(BaseAction): '''Edit meta data action.''' #: Action identifier. identifier = 'delete.asset' #: Action label. label = 'Delete Asset/Subsets' #: Action description. description = 'Removes from Avalon with all childs and asset from Ftrack' 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'] #: Db db = DbConnector() value = None def discover(self, session, entities, event): ''' Validation ''' if len(entities) != 1: return False valid = ["task"] entityType = event["data"]["selection"][0].get("entityType", "") if entityType.lower() not in valid: return False return True def _launch(self, event): self.reset_session() try: self.db.install() args = self._translate_event(self.session, event) interface = self._interface(self.session, *args) confirmation = self.confirm_delete(True, *args) if interface: return interface if confirmation: return confirmation response = self.launch(self.session, *args) finally: self.db.uninstall() return self._handle_result(self.session, response, *args) def interface(self, session, entities, event): if not event['data'].get('values', {}): self.attempt = 1 items = [] entity = entities[0] title = 'Choose items to delete from "{}"'.format(entity['name']) project = entity['project'] self.db.Session['AVALON_PROJECT'] = project["full_name"] av_entity = self.db.find_one({ 'type': 'asset', 'name': entity['name'] }) if av_entity is None: return { 'success': False, 'message': 'Didn\'t found assets in avalon' } asset_label = { 'type': 'label', 'value': '## Delete whole asset: ##' } asset_item = { 'label': av_entity['name'], 'name': 'whole_asset', 'type': 'boolean', 'value': False } splitter = {'type': 'label', 'value': '{}'.format(200 * "-")} subset_label = {'type': 'label', 'value': '## Subsets: ##'} if av_entity is not None: items.append(asset_label) items.append(asset_item) items.append(splitter) all_subsets = self.db.find({ 'type': 'subset', 'parent': av_entity['_id'] }) subset_items = [] for subset in all_subsets: item = { 'label': subset['name'], 'name': str(subset['_id']), 'type': 'boolean', 'value': False } subset_items.append(item) if len(subset_items) > 0: items.append(subset_label) items.extend(subset_items) else: return { 'success': False, 'message': 'Didn\'t found assets in avalon' } return {'items': items, 'title': title} def confirm_delete(self, first_attempt, entities, event): if first_attempt is True: if 'values' not in event['data']: return values = event['data']['values'] if len(values) <= 0: return if 'whole_asset' not in values: return else: values = self.values title = 'Confirmation of deleting {}' if values['whole_asset'] is True: title = title.format('whole asset {}'.format(entities[0]['name'])) else: subsets = [] for key, value in values.items(): if value is True: subsets.append(key) len_subsets = len(subsets) if len_subsets == 0: return { 'success': True, 'message': 'Nothing was selected to delete' } elif len_subsets == 1: title = title.format('{} subset'.format(len_subsets)) else: title = title.format('{} subsets'.format(len_subsets)) self.values = values items = [] delete_label = { 'type': 'label', 'value': '# Please enter "DELETE" to confirm #' } delete_item = { 'name': 'delete_key', 'type': 'text', 'value': '', 'empty_text': 'Type Delete here...' } items.append(delete_label) items.append(delete_item) return {'items': items, 'title': title} def launch(self, session, entities, event): if 'values' not in event['data']: return values = event['data']['values'] if len(values) <= 0: return if 'delete_key' not in values: return if values['delete_key'].lower() != 'delete': if values['delete_key'].lower() == '': return {'success': False, 'message': 'Deleting cancelled'} if self.attempt < 3: self.attempt += 1 return_dict = self.confirm_delete(False, entities, event) return_dict['title'] = '{} ({} attempt)'.format( return_dict['title'], self.attempt) return return_dict return { 'success': False, 'message': 'You didn\'t enter "DELETE" properly 3 times!' } entity = entities[0] project = entity['project'] self.db.Session['AVALON_PROJECT'] = project["full_name"] all_ids = [] if self.values.get('whole_asset', False) is True: av_entity = self.db.find_one({ 'type': 'asset', 'name': entity['name'] }) if av_entity is not None: all_ids.append(av_entity['_id']) all_ids.extend(self.find_child(av_entity)) session.delete(entity) session.commit() else: subset_names = [] for key, value in self.values.items(): if key == 'delete_key' or value is False: continue entity_id = ObjectId(key) av_entity = self.db.find_one({'_id': entity_id}) subset_names.append(av_entity['name']) if av_entity is None: continue all_ids.append(entity_id) all_ids.extend(self.find_child(av_entity)) for ft_asset in entity['assets']: if ft_asset['name'] in subset_names: session.delete(ft_asset) session.commit() if len(all_ids) == 0: return { 'success': True, 'message': 'No entities to delete in avalon' } or_subquery = [] for id in all_ids: or_subquery.append({'_id': id}) delete_query = {'$or': or_subquery} self.db.delete_many(delete_query) return {'success': True, 'message': 'All assets were deleted!'} def find_child(self, entity): output = [] id = entity['_id'] visuals = [x for x in self.db.find({'data.visualParent': id})] assert len(visuals) == 0, 'This asset has another asset as child' childs = self.db.find({'parent': id}) for child in childs: output.append(child['_id']) output.extend(self.find_child(child)) return output def find_assets(self, asset_names): assets = [] for name in asset_names: entity = self.db.find_one({'type': 'asset', 'name': name}) if entity is not None and entity not in assets: assets.append(entity) return assets