# Copyright (c) 2016 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
import copy
from .data_handler import ShotgunDataHandler
from .errors import ShotgunModelDataError
import sgtk
[docs]class ShotgunNavDataHandler(ShotgunDataHandler):
"""
Shotgun Model low level data storage for use
with the Shotgun Hierarchy Model.
This implements a data storage where a series of
nav_expand queries are stringed together into a single
cache file on disk.
"""
# constant values to refer to the fields where the paths are stored in the
# returned navigation data.
_SG_PATH_FIELD = "path"
_SG_PARENT_PATH_FIELD = "parent_path"
def __init__(self, root_path, seed_entity_field, entity_fields, cache_path, include_root=None):
"""
:param str root_path: The path to the root of the hierarchy to display.
This corresponds to the ``path`` argument of the
:meth:`~shotgun-api3:shotgun_api3.Shotgun.nav_expand()`
api method. For example, ``/Project/65`` would correspond to a
project on you shotgun site with id of ``65``.
:param str seed_entity_field: This is a string that corresponds to the
field on an entity used to seed the hierarchy. For example, a value
of ``Version.entity`` would cause the model to display a hierarchy
where the leaves match the entity value of Version entities.
:param dict entity_fields: A dictionary that identifies what fields to
include on returned entities. Since the hierarchy can include any
entity structure, this argument allows for specification of
additional fields to include as these entities are returned. The
dict's keys correspond to the entity type and the value is a list
of field names to return.
:param str cache_path: Path to cache file location.
:param str include_root: Defines the name of an additional, top-level
model item that represents the root. In views, this item will appear
as a sibling to top-level children of the root. This allows for
UX whereby a user can select an item representing the root without
having a UI that shows a single, top-level item. An example would
be displaying published file entity hierarchy with top level items:
"Assets", "Shots", and "Project Publishes". In this example, the
supplied arg would look like: ``include_root="Project Publishes"``.
If ``include_root`` is ``None``, no root item will be added.
"""
super(ShotgunNavDataHandler, self).__init__(cache_path)
self.__root_path = root_path
self.__seed_entity_field = seed_entity_field
self.__entity_fields = entity_fields
self.__include_root = include_root
[docs] def generate_data_request(self, data_retriever, path):
"""
Generate a data request for a data retriever.
Once the data has arrived, the caller is expected to
call meth:`update_data` and pass in the received
data payload for processing.
:param data_retriever: :class:`~tk-framework-shotgunutils:shotgun_data.ShotgunDataRetriever` instance.
:returns: Request id or None if no work is needed
"""
self._log_debug("generate_data_request for path %s" % path)
worker_id = data_retriever.execute_nav_expand(
path,
self.__seed_entity_field,
self.__entity_fields
)
return worker_id
[docs] @sgtk.LogManager.log_timing
def update_data(self, sg_data):
"""
The counterpart to :meth:`generate_data_request`. When the data
request has been carried out, this method should be called by the calling
class and the data payload from Shotgun should be provided via the
sg_data parameter.
The shotgun nav data is compared against an existing part of the tree and
a list of differences is returned, indicating which nodes were
added, deleted and modified, on the following form::
[
{
"data": ShotgunItemData instance,
"mode": self.UPDATED|ADDED|DELETED
},
{
"data": ShotgunItemData instance,
"mode": self.UPDATED|ADDED|DELETED
},
...
]
:param sg_data: list, resulting from a Shotgun nav_expand query
:returns: list of updates. see above
:raises: :class:`ShotgunModelDataError` if no cache is loaded into memory
"""
if self._cache is None:
raise ShotgunModelDataError("No data currently loaded in memory!")
self._log_debug("Updating %s with %s shotgun records." % (self, len(sg_data)))
item_path = sg_data.get(self._SG_PATH_FIELD, None)
self._log_debug("Got hierarchy data for path: %s" % (item_path,))
if not item_path:
raise ShotgunModelDataError(
"Unexpected error occurred. Could not determine the path"
"from the queried hierarchy item."
)
if self._cache.size == 0:
self._log_debug("In-memory cache is empty.")
# ensure the data is clean
self._log_debug("sanitizing data...")
sg_data = self._sg_clean_data(sg_data)
self._log_debug("...done!")
self._log_debug("Generating new tree in memory...")
# create a brand new tree rather than trying to be clever
# about how we cull intermediate nodes for deleted items
diff_list = []
num_adds = 0
num_deletes = 0
num_modifications = 0
new_uids = set()
# a list of sg data dicts to display items for
child_data = []
if item_path == self.__root_path:
self._log_debug("This is the root of the tree.")
parent_uid = None
if self.__include_root:
# the calling code has requested that the root of the tree be
# displayed as a top-level sibling (to make it easy to discover
# hierarchy targets attached to the root entity). To do this, we
# simply make a copy of the root item dictionary (sans children)
# and add it as an additional child to display. We also set the
# label to the supplied value.
root_item = copy.deepcopy(sg_data)
root_item["label"] = self.__include_root
# get rid of child data since it'll be displayed as a sibling
root_item["has_children"] = False
del root_item["children"]
# add the root dict to the list of data to build items for
child_data.append(root_item)
else:
parent_uid = item_path
previous_uids = set(self._cache.get_child_uids(parent_uid))
# process all the children
child_data.extend(sg_data["children"])
# analyze the incoming shotgun data
for sg_item in child_data:
if self._SG_PATH_FIELD not in sg_item:
# note: leaf nodes of kind 'empty' don't have a path
unique_field_value = "%s/%s" % (parent_uid, sg_item["label"])
else:
unique_field_value = sg_item.get(self._SG_PATH_FIELD)
new_uids.add(unique_field_value)
# check if item already exists
already_exists = self._cache.item_exists(unique_field_value)
# insert the change into the data set directly.
# if the item already existed and was updated,
# this returns true
updated = self._cache.add_item(
parent_uid=parent_uid,
sg_data=sg_item,
field_name=None,
is_leaf=not sg_item["has_children"],
uid=unique_field_value
)
if not already_exists:
# item was added
diff_list.append({
"data": self._cache.get_entry_by_uid(unique_field_value),
"mode": self.ADDED
})
num_adds += 1
elif updated:
# item existed but was updated
diff_list.append({
"data": self._cache.get_entry_by_uid(unique_field_value),
"mode": self.UPDATED
})
num_modifications += 1
# now figure out if anything has been removed
for deleted_uid in previous_uids.difference(new_uids):
item = self._cache.take_item(deleted_uid)
diff_list.append({
"data": item,
"mode": self.DELETED
})
num_deletes += 1
self._log_debug("Shotgun data (%d records) received and processed. " % len(sg_data))
self._log_debug(" The new tree is %d records." % self._cache.size)
self._log_debug(" There were %d diffs from in-memory cache:" % len(diff_list))
self._log_debug(" Number of new records: %d" % num_adds)
self._log_debug(" Number of deleted records: %d" % num_deletes)
self._log_debug(" Number of modified records: %d" % num_modifications)
return diff_list