Skip to content
28 changes: 28 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Autodesk, Inc.

from types import ModuleType

import sgtk


Expand All @@ -21,6 +23,7 @@ def init_app(self):
"""Called as the application is being initialized."""

tk_multi_breakdown2 = self.import_module("tk_multi_breakdown2")
self._flowam = tk_multi_breakdown2.flowam

# Store a reference to manager class to expose its functionality at the application level.
self._manager_class = tk_multi_breakdown2.BreakdownManager
Expand Down Expand Up @@ -141,3 +144,28 @@ def _on_dialog_close(self, dialog):
elif dialog == self._current_panel:
self.log_debug("Current panel has been closed, clearing reference.")
self._current_panel = None

@property
def flowam_available(self) -> bool:
"""
Returns True if FlowAM integration is available in the running core.

:returns: True if FlowAM integration is available, False otherwise
:rtype: bool
"""
return (
getattr(self.context, "flow_project_id", None) is not None
and getattr(sgtk.platform.current_engine(), "flow_host", None) is not None
)

@property
def flowam(self) -> ModuleType:
"""
Access to the FlowAM integration module for this app. This module provides
drop-in replacements for the standard Shotgun-based Scene Breakdown models and actions,
backed by Flow Asset Management (FlowAM).

:returns: The FlowAM integration module for this app
:rtype: :mod:`tk_multi_breakdown2.flowam`
"""
return self._flowam
1 change: 1 addition & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ jobs:
additional_repositories:
- name: tk-framework-qtwidgets
- name: tk-framework-shotgunutils
tk_core_ref: ticket/sg-43461/migrate-host-base
3 changes: 3 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ignore:
# flowam and other files not covered by unit tests
- "**python/tk_multi_breakdown2/flowam/*"
2 changes: 0 additions & 2 deletions hooks/tk-mari_scene_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Autodesk, Inc.

import os

import sgtk
from sgtk import TankError

Expand Down
8 changes: 6 additions & 2 deletions hooks/tk-maya_scene_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class BreakdownSceneOperations(HookBaseClass):
This implementation handles detection of maya references and file texture nodes.
"""

__callback_ids = []
Comment thread
chenm1adsk marked this conversation as resolved.

def scan_scene(self):
"""
The scan scene method is executed once at startup and its purpose is
Expand Down Expand Up @@ -104,7 +106,6 @@ def update(self, item):
self.logger.debug(
"File Texture %s: Updating to version %s" % (node_name, path)
)
file_name = cmds.getAttr("%s.fileTextureName" % node_name)
cmds.setAttr("%s.fileTextureName" % node_name, path, type="string")

def register_scene_change_callback(self, scene_change_callback):
Expand Down Expand Up @@ -151,4 +152,7 @@ def unregister_scene_change_callback(self):
"""Unregister the scene change callbacks by disconnecting any signals."""

for callback_id in self.__callback_ids:
OpenMaya.MSceneMessage.removeCallback(callback_id)
try:
OpenMaya.MSceneMessage.removeCallback(callback_id)
except RuntimeError:
pass
2 changes: 1 addition & 1 deletion info.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ configuration:
create and execute actions.
default_value: {}

# The Flow Production Tracking fields that this app needs in order to operate correctly
# The Flow Production Tracking fields this app needs in order to operate correctly
requires_shotgun_fields:
# linked_projects.Asset is required for references in multiple Flow Production Tracking projects

Expand Down
1 change: 1 addition & 0 deletions python/tk_multi_breakdown2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Autodesk, Inc.

from . import flowam # noqa: F401
from .api import BreakdownManager

try:
Expand Down
4 changes: 3 additions & 1 deletion python/tk_multi_breakdown2/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def add_update_to_specific_version_action(file_item, model, sg_data, parent=None
:rtype: QtGui.QAction
"""

if not sg_data.get("version_number"):
if sg_data.get("version_number") is None:
return

action = UpdateToSpecificVersionAction(
Expand Down Expand Up @@ -186,6 +186,7 @@ def execute(self):
index,
[self._model.FILE_ITEM_ROLE, self._model.FILE_ITEM_SG_DATA_ROLE],
)
self._model.reload()


class UpdateToSpecificVersionAction(Action):
Expand Down Expand Up @@ -226,3 +227,4 @@ def execute(self):
index,
[self._model.FILE_ITEM_ROLE, self._model.FILE_ITEM_SG_DATA_ROLE],
)
self._model.reload()
107 changes: 96 additions & 11 deletions python/tk_multi_breakdown2/api/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Autodesk, Inc.

from typing import Any, Optional

import sgtk
from tank.errors import TankHookMethodDoesNotExistError

Expand All @@ -22,6 +24,8 @@ def __init__(self, bundle):
"""Initialize the manager."""

self._bundle = bundle
_engine = sgtk.platform.current_engine()
self._flow_host = _engine.flow_host if _engine else None

@sgtk.LogManager.log_timing
def get_scene_objects(self, execute_in_main_thread=True):
Expand Down Expand Up @@ -228,22 +232,43 @@ def get_history_published_file_filters(self):

return self._bundle.get_setting("history_published_file_filters", [])

def get_latest_published_file(self, item, data_retriever=None, extra_fields=None):
def get_latest_published_file(
self,
item: FileItem,
data_retriever: Optional[Any] = None,
extra_fields: Optional[list[str]] = None,
bg_task_manager: Optional[Any] = None,
) -> Any:
"""
Get the latest available published file according to the current item context.

:param item: :class`FileItem` object we want to get the latest published file
:type item: FileItem
:param data_retreiver: If provided, the api request will be async. The default value
:param data_retriever: If provided, the api request will be async. The default value
will execute the api request synchronously.
:type data_retriever: ShotgunDataRetriever
:param bg_task_manager: Used for async execution.
:type bg_task_manager: BackgroundTaskManager

:return: The latest published file as a Flow Production Tracking entity dictionary if the request was
synchronous, else the request background task id if the request was async.
"""

is_async = data_retriever or bg_task_manager

if not item or not item.sg_data:
return None if data_retriever else {}
return None if is_async else {}

if self._bundle.flowam_available:
result = self._bundle.flowam.get_latest_revision(
item=item,
bg_task_manager=bg_task_manager,
)
if not is_async:
if not isinstance(result, dict):
result = {}
item.latest_published_file = result
return result

fields = self.get_published_file_fields()
if extra_fields:
Expand All @@ -267,24 +292,38 @@ def get_latest_published_file(self, item, data_retriever=None, extra_fields=None
return result

def get_published_files_for_items(
self, items, data_retriever=None, extra_fields=None
):
self,
items: list[FileItem],
data_retriever: Optional[Any] = None,
extra_fields: Optional[list[str]] = None,
bg_task_manager: Optional[Any] = None,
) -> Any:
"""
Get all published files (history) for the given items.

:param items: the list of :class`FileItem` we want to get published files for.
:type items: List[FileItem]
:param data_retreiver: If provided, the api request will be async. The default value
:param data_retriever: If provided, the api request will be async. The default value
will execute the api request synchronously.
:type data_retriever: ShotgunDataRetriever
:param bg_task_manager: Used for async execution.
:type bg_task_manager: BackgroundTaskManager

:return: If the request is async, then the request task id is returned, else the
published file data result from the api request.
:rtype: str | dict
"""

is_async = data_retriever or bg_task_manager

if not items:
return None if data_retriever else {}
return None if is_async else {}

if self._bundle.flowam_available:
return self._bundle.flowam.get_assets_for_items(
items=items,
bg_task_manager=bg_task_manager,
)

fields = self.get_published_file_fields()
if extra_fields:
Expand All @@ -301,7 +340,13 @@ def get_published_files_for_items(
published_file_filters=filters,
)

def get_published_file_history(self, item, extra_fields=None, data_retriever=None):
def get_published_file_history(
self,
item: FileItem,
extra_fields: Optional[list[str]] = None,
data_retriever: Optional[Any] = None,
bg_task_manager: Optional[Any] = None,
) -> Any:
"""
Get the published history for the selected item. It will gather all the published files with the same context
than the current item (project, name, task, ...)
Expand All @@ -310,9 +355,11 @@ def get_published_file_history(self, item, extra_fields=None, data_retriever=Non
:type item: FileItem
:param extra_fields: A list of Flow Production Tracking fields to append to the Flow Production Tracking query fields.
:type extra_fields: List[str]
:param data_retreiver: If provided, the api request will be async. The default value
:param data_retriever: If provided, the api request will be async. The default value
will execute the api request synchronously.
:type data_retriever: ShotgunDataRetriever
:param bg_task_manager: Used for async execution.
:type bg_task_manager: BackgroundTaskManager

:return: If the request is async, then the request task id is returned, else the
published file history.
Expand All @@ -323,7 +370,10 @@ def get_published_file_history(self, item, extra_fields=None, data_retriever=Non
return []

result = self.get_published_files_for_items(
[item], data_retriever=data_retriever, extra_fields=extra_fields
[item],
data_retriever=data_retriever,
extra_fields=extra_fields,
bg_task_manager=bg_task_manager,
)
if result and isinstance(result, list):
item.latest_published_file = result[0]
Expand All @@ -343,6 +393,27 @@ def update_to_latest_version(self, items):
if not isinstance(items, list):
items = [items]

if self._bundle.flowam_available:
items_to_update = self._bundle.flowam.update_to_latest(items)

# The FlowAM method performs the DCC-side update but does not update the
# Python FileItem model data. We do that here so callers always get a
# consistent, up-to-date list of updated FileItem objects regardless of
# which code path ran. A non-list return (including None) means the hook
# chose not to filter, so attempt to update all items.
if not isinstance(items_to_update, list):
items_to_update = items

updated_items = []
for item in items_to_update:
data = item.latest_published_file
if not data or not data.get("path", {}).get("local_path", None):
continue
item.sg_data = data
item.path = data["path"]["local_path"]
updated_items.append(item)
return updated_items

# First try to execute the hook method to update items in batch for performance.
try:
return self.update_items_to_latest_version(items)
Expand All @@ -364,7 +435,7 @@ def update_items_to_latest_version(self, items):
:param items: The item or items to update.
:type items: FileItem | List[FileItem]

:return: The list of file item objectggs that were updated to the latest version.
:return: The list of file item objects that were updated to the latest version.
:rtype: List[FileItem]
"""

Expand Down Expand Up @@ -436,6 +507,20 @@ def update_to_specific_version(self, item, sg_data):
if not sg_data or not sg_data.get("path", {}).get("local_path", None):
return False

if self._bundle.flowam_available:
do_update = self._bundle.flowam.update_to_revision(
item=item.to_dict(),
item_data=sg_data,
)
if do_update is None:
# Default to True if the hook return value was not explictly set
do_update = True

if do_update:
item.sg_data = sg_data
item.path = sg_data["path"]["local_path"]
return do_update

item_dict = item.to_dict()
item_dict["path"] = sg_data["path"]["local_path"]
if item_dict["extra_data"] is None:
Expand Down
9 changes: 7 additions & 2 deletions python/tk_multi_breakdown2/dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -872,8 +872,7 @@ def _show_history_item_context_menu(self, view, index, pos):
# passed in references the file history item.
if isinstance(index.model(), QtGui.QSortFilterProxyModel):
index = index.model().mapToSource(index)
history_item = index.model().itemFromIndex(index)
sg_data = history_item.get_sg_data()
sg_data = index.data(FileHistoryModel.SG_DATA_ROLE)

update_action = ActionManager.add_update_to_specific_version_action(
file_item_to_update, self._file_model, sg_data, None
Expand Down Expand Up @@ -1350,6 +1349,12 @@ def _on_update_selected_to_latest(self):
self._listen_for_events(False)
try:
ActionManager.execute_update_to_latest_action(file_items, self._file_model)
except Exception as e:
QtGui.QMessageBox.critical(
None,
"Scene Breakdown",
"Error: {}".format(e),
)
finally:
self.__executing_bulk_action = False
# Turn on event handling if it was on before
Expand Down
Loading
Loading