Exemplo n.º 1
0
    def run_interactive(self, log, args):
        """
        Tank command accessor

        :param log: std python logger
        :param args: command line args
        """

        log.info("Welcome to the folder sync upgrade command!")
        log.info("")
        log.info(
            "Projects created with Toolkit v0.14 and earlier do not automatically synchronize "
            "their folders on disk with Shotgun. You can use this command to turn on that folder "
            "sync.")
        log.info("")

        if self.tk.pipeline_configuration.get_shotgun_path_cache_enabled():
            log.info("Looks like syncing is already turned on! Nothing to do!")
            return

        log.info(
            "Turning on folder sync will first do a full synchronization of the "
            "existing folders. After that, syncing will happen incrementally in the "
            "background.")
        log.info("")
        log.info(
            "Note! If you have any cloned pipeline configurations for this project, you must run "
            "'tank upgrade_folders' for each one of them in order for them to pick up folders "
            "from Shotgun.")
        log.info("")
        val = input(
            "Turn on syncing for this pipeline configuration (Yes/No) ? [Yes]: "
        )
        if val != "" and not val.lower().startswith("y"):
            log.info("Exiting! Syncing will not be turned on.")
            return

        # first load up the current path cache file and make sure
        # shotgun has got all those entries present as FilesystemLocations.
        log.info("")
        log.info(
            "Phase 1/3: Pushing data from the current path cache to Shotgun..."
        )
        curr_pc = path_cache.PathCache(self.tk)
        try:
            curr_pc.ensure_all_entries_are_in_shotgun()
        finally:
            curr_pc.close()

        # now turn on the cloud based path cache. This means that from now on, a new path
        # cache, stored on the local disk, will be used instead of the previous (shared) one.
        log.info("")
        log.info("Phase 2/3: Switching on the Shotgun Path Cache...")
        self.tk.pipeline_configuration.turn_on_shotgun_path_cache()

        # and synchronize path cache
        log.info("")
        log.info("Phase 3/3: Synchronizing your local machine with Shotgun...")
        pc = path_cache.PathCache(self.tk)
        try:
            pc.synchronize(full_sync=True)
        finally:
            pc.close()

        log.info("")
        log.info(
            "All done! This project and pipeline configuration is now synchronizing its folders with Shotgun."
        )
        log.info("")
        log.info(
            "Once all pipeline configurations for this project have been upgraded, the previous path cache "
            "file, located in PROJECT_ROOT/tank/cache, is no longer needed and can be removed."
        )
        log.info("")
        log.info("")
Exemplo n.º 2
0
    def run_interactive(self, log, args):

        if self.tk.pipeline_configuration.is_unmanaged():
            log.error("Cannot move unmanaged configurations!")
            return

        pipeline_config_id = self.tk.pipeline_configuration.get_shotgun_id()
        current_paths = self.tk.pipeline_configuration.get_all_os_paths()

        if len(args) != 3:
            log.info(
                "Syntax: move_configuration linux_path windows_path mac_path")
            log.info("")
            log.info(
                "This will move the location of the given pipeline configuation."
            )
            log.info(
                "You can also use this command to add a new platform to the pipeline configuration."
            )
            log.info("")
            log.info("Current Paths")
            log.info(
                "--------------------------------------------------------------"
            )
            log.info("")
            log.info("Current Linux Path:   '%s'" % current_paths.linux)
            log.info("Current Windows Path: '%s'" % current_paths.windows)
            log.info("Current Mac Path:     '%s'" % current_paths.macosx)
            log.info("")
            log.info("")
            log.info("You typically need to quote your paths, like this:")
            log.info("")
            log.info(
                '> tank move_configuration "/linux_root/my_config" "p:\\configs\\my_config" "/mac_root/my_config"'
            )
            log.info("")
            log.info(
                "If you want to leave a platform blank, just just empty quotes. For example, "
                "if you want a configuration which only works on windows, do like this: "
            )
            log.info("")
            log.info(
                '> tank move_configuration "" "p:\\configs\\my_config" ""')
            log.info("")
            log.info("")
            raise TankError("Please specify three target locations!")

        linux_path = args[0]
        windows_path = args[1]
        mac_path = args[2]
        new_paths = {
            "darwin": mac_path,
            "win32": windows_path,
            "linux2": linux_path
        }

        # check which paths are different
        modifications = {
            "darwin": (current_paths.macosx != mac_path),
            "win32": (current_paths.windows != windows_path),
            "linux2": (current_paths.linux != linux_path),
        }

        log.info("")
        log.info("Current Paths")
        log.info(
            "--------------------------------------------------------------")
        log.info("Current Linux Path:   '%s'" % current_paths.linux)
        log.info("Current Windows Path: '%s'" % current_paths.windows)
        log.info("Current Mac Path:     '%s'" % current_paths.macosx)
        log.info("")
        log.info("New Paths")
        log.info(
            "--------------------------------------------------------------")
        if modifications["linux2"]:
            log.info("New Linux Path:   '%s'" % linux_path)
        else:
            log.info("New Linux Path:   No change")

        if modifications["win32"]:
            log.info("New Windows Path: '%s'" % windows_path)
        else:
            log.info("New Windows Path: No change")

        if modifications["darwin"]:
            log.info("New Mac Path:     '%s'" % mac_path)
        else:
            log.info("New Mac Path:     No change")

        log.info("")
        log.info("")

        if modifications[sgsix.platform]:
            copy_files = True
            log.info(
                "The configuration will be moved to reflect the specified path changes."
            )
        else:
            copy_files = False
            # we are not modifying current OS
            log.info(
                "Looks like you are not modifying the location for this operating system. Therefore, "
                "no files will be moved around, only configuration files will be updated."
            )

        log.info("")
        log.info(
            "Note for advanced users: If your configuration is localized and you have other projects which "
            "are linked to the core API embedded in this configuration, these links must be manually "
            "updated after the move operation.")

        log.info("")
        val = input(
            "Are you sure you want to move your configuration? [Yes/No] ")
        if not val.lower().startswith("y"):
            raise TankError("Aborted by User.")

        # ok let's do it!
        local_source_path = self.tk.pipeline_configuration.get_path()
        local_target_path = new_paths[sgsix.platform]

        if copy_files:

            # check that files exists and that we can carry out the copy etc.
            if not os.path.exists(local_source_path):
                raise TankError("The path %s does not exist on disk!" %
                                local_source_path)
            if os.path.exists(local_target_path):
                raise TankError("The path %s already exists on disk!" %
                                local_target_path)

            # sanity check target folder
            parent_target = os.path.dirname(local_target_path)
            if not os.path.exists(parent_target):
                raise TankError("The path '%s' does not exist!" %
                                parent_target)
            if not os.access(parent_target, os.W_OK | os.R_OK | os.X_OK):
                raise TankError(
                    "The permissions setting for '%s' is too strict. The current user "
                    "cannot create folders in this location." % parent_target)

        # first copy the data across
        old_umask = os.umask(0)
        try:

            # first copy the files - this is where things can go wrong so start with this
            if copy_files:
                log.info("Copying '%s' -> '%s'" %
                         (local_source_path, local_target_path))
                self._copy_folder(log, 0, local_source_path, local_target_path)
                sg_code_location = os.path.join(local_target_path, "config",
                                                "core", "install_location.yml")
            else:
                sg_code_location = os.path.join(local_source_path, "config",
                                                "core", "install_location.yml")

            # now updated config files
            log.info("Updating cached locations in %s..." % sg_code_location)
            os.chmod(sg_code_location, 0o666)
            fh = open(sg_code_location, "wt")
            fh.write("# SG Pipeline Toolkit configuration file\n")
            fh.write(
                "# This file reflects the paths in the pipeline configuration\n"
            )
            fh.write("# entity which is associated with this location\n")
            fh.write("\n")
            fh.write("Windows: '%s'\n" % windows_path)
            fh.write("Darwin: '%s'\n" % mac_path)
            fh.write("Linux: '%s'\n" % linux_path)
            fh.write("\n")
            fh.write("# End of file.\n")
            fh.close()

        except Exception as e:
            raise TankError(
                "Could not copy configuration! This may be because of system "
                "permissions or system setup. This configuration will "
                "still be functional, however data may have been partially copied "
                "to '%s' so we recommend that that location is cleaned up. "
                "Error Details: %s" % (local_target_path, e))
        finally:
            os.umask(old_umask)

        log.info("Updating SG Configuration Record...")
        self.tk.shotgun.update(
            constants.PIPELINE_CONFIGURATION_ENTITY,
            pipeline_config_id,
            {
                "mac_path": new_paths["darwin"],
                "windows_path": new_paths["win32"],
                "linux_path": new_paths["linux2"],
            },
        )

        # finally clean up the previous location
        if copy_files:
            log.info("Deleting original configuration files...")
            self._cleanup_old_location(log, local_source_path)
        log.info("")
        log.info("All done! Your configuration has been successfully moved.")
Exemplo n.º 3
0
    def _map_storages(self, params, config_uri, log, sg):
        """
        Present the user with information about the storage roots defined by
        the configuration. Allows them to map a root to an existing local
        storage in SG.

        :param params: Project setup params instance
        :param config_uri: A config uri
        :param log: python logger object
        :param sg: Shotgun API instance
        """

        # query all storages that exist in SG
        storages = sg.find(
            "LocalStorage",
            filters=[],
            fields=["code", "id", "linux_path", "mac_path", "windows_path"],
            order=[{
                "field_name": "code",
                "direction": "asc"
            }],
        )

        # build lookups by name and id
        storage_by_id = {}
        storage_by_name = {}
        for storage in storages:
            # store lower case names so we can do case insensitive comparisons
            storage_name = storage["code"].lower()
            storage_id = storage["id"]
            storage_by_id[storage_id] = storage
            storage_by_name[storage_name] = storage

        # present a summary of storages that exist in SG
        log.info("")
        log.info("The following local storages exist in ShotGrid:")
        log.info("")
        for storage in sorted(storages, key=lambda s: s["code"]):
            self._print_storage_info(storage, log)

        # get all roots required by the supplied config uri
        required_roots = params.validate_config_uri(config_uri)
        log.info("This configuration requires %s storage root(s)." %
                 len(required_roots))
        log.info("")
        log.info("For each storage root, enter the name of the local storage ")
        log.info("above you wish to use.")
        log.info("")

        # a list of tuples we'll use to map a root name to a storage name
        mapped_roots = []

        # loop over required storage roots
        for (root_name, root_info) in required_roots.items():

            log.info("%s" % (root_name, ))
            log.info("-" * len(root_name))

            # format the description so that it wraps nicely
            description = root_info.get("description")

            if description:
                wrapped_desc_lines = textwrap.wrap(description)
                for line in wrapped_desc_lines:
                    log.info("  %s" % (line, ))
            log.info("")

            # make a best guess as to which storage to use
            suggested_storage = None

            # see if a shotgun storage id is defined in the root info.
            root_sg_id = root_info.get("shotgun_id")
            if root_sg_id in storage_by_id:
                storage_name = storage_by_id[root_sg_id]["code"]
                # shotgun id defined explicitly for this root
                log.info("Press ENTER to use the explicit mapping to the '%s' "
                         "storage as defined in the configuration." %
                         (storage_name, ))
                log.info("")
                suggested_storage = storage_name

            # does name match an existing storage?
            elif root_name.lower() in storage_by_name:
                log.info(
                    "Press ENTER to use the storage wth the same name as the "
                    "root.")
                log.info("")
                # get the actual name by indexing into the storage dict
                suggested_storage = storage_by_name[root_name.lower()]["code"]

            if suggested_storage:
                suggested_storage_display = " [%s]: " % (suggested_storage, )
            else:
                suggested_storage_display = ": "

            # ask the user which storage to associate with this root
            storage_to_use = input(
                "Which local storage would you like to associate root '%s'%s" %
                (root_name, suggested_storage_display)).strip()

            if storage_to_use == "" and suggested_storage:
                storage_to_use = suggested_storage

            # match case insensitively
            if storage_to_use.lower() in storage_by_name:
                storage_to_use = storage_by_name[
                    storage_to_use.lower()]["code"]
                storage = storage_by_name[storage_to_use.lower()]
            else:
                raise TankError("Please enter a valid storage name!")

            log.info("")
            log.info("Accepted mapping: root '%s' ==> local storage '%s':" %
                     (root_name, storage_to_use))
            log.info("")

            # if the selected storage does not have a valid path for the current
            # operating system, prompt the user for a path to create/use
            current_os_key = ShotgunPath.get_shotgun_storage_key()
            current_os_path = storage.get(current_os_key)

            if not current_os_path:
                # the current os path for the selected storage is not populated.
                # prompt the user and update the path in SG.
                current_os_path = input(
                    "Please enter a path for this storage on the current OS: ")

                if not current_os_path:
                    raise TankError("A path is required for the current OS.")

                if not os.path.isabs(current_os_path):
                    raise TankError(
                        "An absolute path is required for the current OS.")

                # try to create the path on disk
                try:
                    ensure_folder_exists(current_os_path)
                except Exception as e:
                    raise TankError("Unable to create the folder on disk.\n"
                                    "Error: %s\n%s" %
                                    (e, traceback.format_exc()))

                # update the storage in SG.
                log.info("Updating the local storage in SG...")
                log.info("")
                update_data = sg.update("LocalStorage", storage["id"],
                                        {current_os_key: current_os_path})

                storage[current_os_key] = update_data[current_os_key]

            mapped_roots.append((root_name, storage_to_use))

        # ---- now we've mapped the roots, and they're all valid, we need to
        #      update the root information on the core wizard

        for (root_name, storage_name) in mapped_roots:

            root_info = required_roots[root_name]
            storage_data = storage_by_name[storage_name.lower()]

            # populate the data defined prior to mapping
            updated_storage_data = root_info

            # update the mapped shotgun data
            updated_storage_data["shotgun_storage_id"] = storage_data["id"]
            updated_storage_data["linux_path"] = str(
                storage_data["linux_path"])
            updated_storage_data["mac_path"] = str(storage_data["mac_path"])
            updated_storage_data["windows_path"] = str(
                storage_data["windows_path"])

            # now update the core wizard's root info
            params.update_storage_root(config_uri, root_name,
                                       updated_storage_data)

        log.info("")
Exemplo n.º 4
0
    def _get_project_folder_name(self, log, sg, params):
        """
        Given a project entity in Shotgun (name, id), decide where the project data
        root should be on disk. This will verify that the selected folder exists
        in each of the storages required by the configuration. It will prompt the user
        and can create these root folders if required (with open permissions).

        :param log: python logger
        :param sg: Shotgun API instance
        :param params: ProjectSetupParameters instance which holds the project setup parameters.
        :returns: The project disk name which is selected, this name may
                  include slashes if the selected location is multi-directory.
        """

        suggested_folder_name = params.get_default_project_disk_name()

        log.info("")
        log.info("")
        log.info("")
        log.info(
            "Now you need to tell Toolkit where you are storing the data for this project."
        )
        log.info(
            "The selected Toolkit config utilizes the following Local Storages, as "
        )
        log.info("defined in the SG Site Preferences:")
        log.info("")
        for storage_name in params.get_required_storages():
            current_os_path = params.get_storage_path(storage_name,
                                                      sgsix.platform)
            log.info(" - %s: '%s'" % (storage_name, current_os_path))

        # first, display a preview
        log.info("")
        log.info(
            "Each of the above locations need to have a data folder which is ")
        log.info(
            "specific to this project. These folders all need to be named the same thing."
        )
        log.info("They also need to exist on disk.")
        log.info("For example, if you named the project '%s', " %
                 suggested_folder_name)
        log.info("the following folders would need to exist on disk:")
        log.info("")
        for storage_name in params.get_required_storages():
            proj_path = params.preview_project_path(storage_name,
                                                    suggested_folder_name,
                                                    sgsix.platform)
            log.info(" - %s: %s" % (storage_name, proj_path))

        log.info("")

        # now ask for a value and validate
        while True:
            log.info("")
            proj_name = input("Please enter a folder name [%s]: " %
                              suggested_folder_name).strip()
            if proj_name == "":
                proj_name = suggested_folder_name

            # validate the project name
            try:
                params.validate_project_disk_name(proj_name)
            except TankError as e:
                # bad project name!
                log.error("Invalid project name: %s" % e)
                # back to beginning
                continue

            log.info("...that corresponds to the following data locations:")
            log.info("")
            storages_valid = True
            for storage_name in params.get_required_storages():

                proj_path = params.preview_project_path(
                    storage_name, proj_name, sgsix.platform)

                if os.path.exists(proj_path):
                    log.info(" - %s: %s [OK]" % (storage_name, proj_path))
                else:

                    # try to create the folders
                    try:

                        old_umask = os.umask(0)
                        try:
                            os.makedirs(proj_path, 0o777)
                        finally:
                            os.umask(old_umask)

                        log.info(" - %s: %s [Created]" %
                                 (storage_name, proj_path))
                        storages_valid = True
                    except Exception as e:
                        log.error(" - %s: %s [Not created]" %
                                  (storage_name, proj_path))
                        log.error("   Please create path manually.")
                        log.error("   Details: %s" % e)
                        storages_valid = False

            log.info("")

            if storages_valid:
                # looks like folders exist on disk

                val = input("Paths look valid. Continue? (Yes/No)? [Yes]: ")
                if val == "" or val.lower().startswith("y"):
                    break
            else:
                log.info(
                    "Please make sure that folders exist on disk for your project name!"
                )

        params.set_project_disk_name(proj_name)
Exemplo n.º 5
0
    def _select_project(self, log, sg, show_initialized_projects):
        """
        Returns the project id and name for a project for which setup should be done.
        Will request the user to input console input to select project.

        :param log: python logger
        :param sg: Shotgun API instance
        :param show_initialized_projects: Should alrady initialized projects be displayed in the listing?

        :returns: project_id
        """

        filters = [
            ["name", "is_not", "Template Project"],
            ["sg_status", "is_not", "Archive"],
            ["sg_status", "is_not", "Lost"],
            ["archived", "is_not", True],
        ]

        if show_initialized_projects == False:
            # not force mode. Only show non-set up projects
            filters.append(["tank_name", "is", None])

        projs = sg.find("Project", filters,
                        ["id", "name", "sg_description", "tank_name"])

        if len(projs) == 0:
            raise TankError(
                "Sorry, no projects found! All projects seem to have already been "
                "set up with the SG Pipeline Toolkit. If you are an expert "
                "user and want to run the setup on a project which already has been "
                "set up, run the setup_project command with a --force option.")

        if show_initialized_projects:
            log.info("")
            log.info("")
            log.info(
                "Below are all active projects, including ones that have been set up:"
            )
            log.info(
                "--------------------------------------------------------------------"
            )
            log.info("")

        else:
            log.info("")
            log.info("")
            log.info(
                "Below are all projects that have not yet been set up with Toolkit:"
            )
            log.info(
                "-------------------------------------------------------------------"
            )
            log.info("")

        for x in projs:
            # helper that formats a single project
            desc = x.get("sg_description")
            if desc is None:
                desc = "[No description]"

            # chop a long description
            if len(desc) > 50:
                desc = "%s..." % desc[:50]

            log.info("[%2d] %s" % (x.get("id"), x.get("name")))
            if x.get("tank_name"):
                log.info("Note: This project has already been set up.")
            log.info("     %s" % desc)
            log.info("")

        log.info("")
        answer = input(
            "Please type in the id of the project to connect to or ENTER to exit: "
        )
        if answer == "":
            raise TankError("Aborted by user.")
        try:
            project_id = int(answer)
        except:
            raise TankError("Please enter a number!")

        if project_id not in [x["id"] for x in projs]:
            raise TankError("Id %d was not found in the list of projects!" %
                            project_id)

        return project_id
Exemplo n.º 6
0
    def _select_template_configuration(self, log, sg):
        """
        Ask the user which config to use. Returns a config uri string.

        :param log: python logger
        :param sg: Shotgun API instance

        :returns: config uri string
        """
        log.info("")
        log.info("")
        log.info(
            "------------------------------------------------------------------"
        )
        log.info(
            "Which configuration would you like to associate with this project?"
        )

        primary_pcs = sg.find(
            constants.PIPELINE_CONFIGURATION_ENTITY,
            [["code", "is", constants.PRIMARY_PIPELINE_CONFIG_NAME]],
            ["code", "project", "mac_path", "windows_path", "linux_path"],
        )

        if len(primary_pcs) > 0:
            log.info("")
            log.info(
                "You can use the configuration from an existing project as a "
                "template for this new project. All settings, apps and folder "
                "configuration settings will be copied over to your new project. "
                "The following configurations were found: ")
            log.info("")
            for ppc in primary_pcs:
                # As of 6.0.2, pipeline configurations can be project less, so skip those
                if ppc.get("project") is None:
                    continue

                pc_path = ppc.get(ShotgunPath.get_shotgun_storage_key())
                if pc_path is None or pc_path == "":
                    # this Toolkit config does not exist on a disk that is reachable from this os
                    log.info("   %s: No valid config found for this OS!" %
                             ppc.get("project").get("name"))
                else:
                    config_path = os.path.join(pc_path, "config")
                    log.info("   %s: '%s'" %
                             (ppc.get("project").get("name"), config_path))

            log.info("")
            log.info(
                "If you want to use any of the configs listed about for your new project, "
                "just type in its path when prompted below.")

        log.info("")
        log.info(
            "You can use the Default Configuration for your new project.  "
            "The default configuration is a good sample config, demonstrating "
            "a typical basic setup of the SG Pipeline Toolkit using the "
            "latest apps and engines. This will be used by default if you just "
            "hit enter below.")
        log.info("")
        log.info(
            "If you have a configuration stored somewhere on disk, you can "
            "enter the path to this config and it will be used for the "
            "new project.")
        log.info("")
        log.info(
            "You can also enter an url pointing to a git repository. Toolkit will then "
            "clone this repository and base the config on its content.")
        log.info("")

        config_name = input("[%s]: " % constants.DEFAULT_CFG).strip()
        if config_name == "":
            config_name = constants.DEFAULT_CFG
        return config_name
Exemplo n.º 7
0
    def _unregister_filesystem_location_ids(self, ids, log, prompt):
        """
        Performs the unregistration of a path from the path cache database.
        Will recursively unregister any child items parented to the given
        filesystem location id.

        :param ids: List of filesystem location ids to unregister
        :param log: Logging instance
        :param prompt: Should the user be presented with confirmation prompts?
        :returns: List of dictionaries to represents the items that were unregistered.
                  Each dictionary has keys path and entity, where entity is a standard
                  Shotgun-style link dictionary containing the keys type and id.
                  Note that the shotgun ids returned will refer to retired objects in
                  Shotgun rather than live ones.
        """
        # tuple index constants for readability

        if len(ids) == 0:
            log.info("No associated folders found!")
            return []

        # first of all, make sure we are up to date.
        pc = path_cache.PathCache(self.tk)
        try:
            pc.synchronize()
        finally:
            pc.close()

        # now use the path cache to get a list of all folders (recursively) that are
        # linked up to the folders registered for this entity.
        # store this in a set so that we ensure a unique set of matches

        paths = set()
        pc = path_cache.PathCache(self.tk)
        path_ids = []
        paths = []
        try:
            for sg_fs_id in ids:
                # get path subtree for this id via the path cache
                for path_obj in pc.get_folder_tree_from_sg_id(sg_fs_id):
                    # store in the set as a tuple which is immutable
                    paths.append(path_obj["path"])
                    path_ids.append(path_obj["sg_id"])
        finally:
            pc.close()

        log.info("")
        log.info("The following folders will be unregistered:")
        for p in paths:
            log.info(" - %s" % p)

        log.info("")
        log.info(
            "Proceeding will unregister the above paths from Toolkit's path cache. "
            "This will not alter any of the content in the file system, but once you have "
            "unregistered the paths, they will not be recognized by SG until you run "
            "Toolkit folder creation again.")
        log.info("")
        log.info(
            "This is useful if you have renamed an Asset or Shot and want to move its "
            "files to a new location on disk. In this case, start by unregistering the "
            "folders for the entity, then rename the Shot or Asset in ShotGrid. "
            "Next, create new folders on disk using Toolkit's 'create folders' "
            "command. Finally, move the files to the new location on disk.")
        log.info("")
        if prompt:
            val = input(
                "Proceed with unregistering the above folders? (Yes/No) ? [Yes]: "
            )
            if val != "" and not val.lower().startswith("y"):
                log.info("Exiting! Nothing was unregistered.")
                return []

        log.info("Unregistering folders from ShotGrid...")
        log.info("")

        path_cache.PathCache.remove_filesystem_location_entries(
            self.tk, path_ids)

        # lastly, another sync
        pc = path_cache.PathCache(self.tk)
        try:
            pc.synchronize()
        finally:
            pc.close()

        log.info("")
        log.info("Unregister complete. %s paths were unregistered." %
                 len(paths))

        # now shuffle the return data into a list of dicts
        return_data = []
        for path_id, path in zip(path_ids, paths):
            return_data.append({
                "path": path,
                "entity": {
                    "type": path_cache.SHOTGUN_ENTITY,
                    "id": path_id
                },
            })

        return return_data