From 559160f7c78f84c57f921ecbcf0463cab15a7931 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Mon, 15 Jun 2026 12:47:49 -0500 Subject: [PATCH 01/37] Remove framework dependency from dialogs classes --- python/tk_multi_loader/build_asset_dialog.py | 27 +++++++++---------- .../tk_multi_loader/build_template_dialog.py | 16 +++++------ python/tk_multi_loader/dialog.py | 3 --- python/tk_multi_loader/medm/__init__.py | 12 --------- python/tk_multi_loader/medm/flowam_actions.py | 2 +- python/tk_multi_loader/medm/utils.py | 27 +++++++++++++++++++ 6 files changed, 47 insertions(+), 40 deletions(-) diff --git a/python/tk_multi_loader/build_asset_dialog.py b/python/tk_multi_loader/build_asset_dialog.py index 4628d30a..ded8e0b0 100644 --- a/python/tk_multi_loader/build_asset_dialog.py +++ b/python/tk_multi_loader/build_asset_dialog.py @@ -12,8 +12,11 @@ import sgtk from sgtk.platform.qt import QtGui +from tank_vendor.flow_integration_sdk.exceptions import FlowError +from tank_vendor.flow_integration_sdk.objects import FlowProject from .medm.template_queries import get_template_pipeline_steps, get_templates +from .medm.utils import CreateMode, get_template_source_path from .ui.build_asset_dialog import Ui_BuildAssetDialog # Toolkit logger @@ -53,16 +56,10 @@ def __init__( """ super().__init__(parent) - _flow = sgtk.platform.import_framework("tk-framework-flowam", "flow") - _FlowError = _flow.FlowError - _Project = _flow.data.Project - self._CreateMode = _flow.asset_management.CreateMode - self._get_template_source_path = _flow.asset_management.get_template_source_path - # Query the project entity try: - self.project = _Project(project_id) - except _FlowError as exc: + self.project = FlowProject(project_id) + except FlowError as exc: raise ValueError(f"Project not found: {project_id}") from exc self.build = None @@ -85,9 +82,9 @@ def __init__( # Populate combo box from options list self.ui.build_mode_combo_box.addItems( [ - self._CreateMode.NEW.value, - self._CreateMode.CURRENT.value, - self._CreateMode.TEMPLATE.value, + CreateMode.NEW.value, + CreateMode.CURRENT.value, + CreateMode.TEMPLATE.value, ] ) @@ -143,7 +140,7 @@ def on_build_option_changed(self, text: str) -> None: Args: text (str): The new build option selected. """ - is_template_mode = self._CreateMode(text) == self._CreateMode.TEMPLATE + is_template_mode = CreateMode(text) == CreateMode.TEMPLATE self.setUpdatesEnabled(False) self.ui.templateWidget.setVisible(is_template_mode) @@ -191,14 +188,14 @@ def on_build_clicked(self) -> None: Returns: None. """ - self.build = self._CreateMode(self.ui.build_mode_combo_box.currentText()) + self.build = CreateMode(self.ui.build_mode_combo_box.currentText()) - if self.build == self._CreateMode.TEMPLATE: + if self.build == CreateMode.TEMPLATE: self.step = self.ui.pipeline_step_combo_box.currentText() self.template = self.ui.templates_combo_box.currentText() if self.template and self.template in self.templates: template = self.templates[self.template] - self.template_source_path = self._get_template_source_path(template) + self.template_source_path = get_template_source_path(template) else: self.step = None self.template = None diff --git a/python/tk_multi_loader/build_template_dialog.py b/python/tk_multi_loader/build_template_dialog.py index 079e8970..85eb7975 100644 --- a/python/tk_multi_loader/build_template_dialog.py +++ b/python/tk_multi_loader/build_template_dialog.py @@ -12,8 +12,11 @@ import sgtk from sgtk.platform.qt import QtGui +from tank_vendor.flow_integration_sdk.exceptions import FlowError +from tank_vendor.flow_integration_sdk.objects import FlowProject from .medm.template_queries import find_template_pipeline_step, get_templates +from .medm.utils import CreateMode from .ui.build_template_dialog import Ui_BuildTemplateDialog # Toolkit logger @@ -33,15 +36,10 @@ def __init__( ) -> None: super().__init__(parent) - _flow = sgtk.platform.import_framework("tk-framework-flowam", "flow") - _FlowError = _flow.FlowError - _Project = _flow.data.Project - self._CreateMode = _flow.asset_management.CreateMode - # Query the project entity try: - self.project = _Project(project_id) - except _FlowError as exc: + self.project = FlowProject(project_id) + except FlowError as exc: raise ValueError(f"Project not found: {project_id}") from exc if not pipeline_steps: @@ -56,7 +54,7 @@ def __init__( self.ui.setupUi(self) self.ui.build_mode_combo_box.addItems( - [self._CreateMode.NEW.value, self._CreateMode.CURRENT.value] + [CreateMode.NEW.value, CreateMode.CURRENT.value] ) self.ui.pipeline_step_combo_box.addItems(pipeline_steps) @@ -84,7 +82,7 @@ def on_build_template_clicked(self) -> None: Handler for when the build template button is clicked. Gathers input data. """ - self.mode = self._CreateMode(self.ui.build_mode_combo_box.currentText()) + self.mode = CreateMode(self.ui.build_mode_combo_box.currentText()) self.step = self.ui.pipeline_step_combo_box.currentText() self.template = self.ui.template_name_line_edit.text().strip() self.description = self.ui.description_text_edit.toPlainText() diff --git a/python/tk_multi_loader/dialog.py b/python/tk_multi_loader/dialog.py index 1a43db7d..cabb8d39 100644 --- a/python/tk_multi_loader/dialog.py +++ b/python/tk_multi_loader/dialog.py @@ -167,9 +167,6 @@ def __init__(self, action_manager, parent=None): self._publish_history_model = SgPublishHistoryModel(self, self._task_manager) # FlowAM objects are only instantiated when enable_flowam is enabled. - # tk-framework-flowam is required by these classes but is not available - # in all environments (e.g. CI). Keeping these as None when FlowAM is - # disabled prevents a hard startup failure in those environments. self._medm_cache = None self._medm_thumbnail_service = None self._medm_history_model = None diff --git a/python/tk_multi_loader/medm/__init__.py b/python/tk_multi_loader/medm/__init__.py index d5e0a887..8d83bcc9 100644 --- a/python/tk_multi_loader/medm/__init__.py +++ b/python/tk_multi_loader/medm/__init__.py @@ -28,15 +28,3 @@ get_templates, ) from .thumbnail_service import MedmThumbnailService - -__all__ = [ - "FlowAMActions", - "MedmEntityModel", - "MedmLatestPublishModel", - "MedmPublishHistoryModel", - "MedmSharedCache", - "MedmThumbnailService", - "find_template_pipeline_step", - "get_template_pipeline_steps", - "get_templates", -] diff --git a/python/tk_multi_loader/medm/flowam_actions.py b/python/tk_multi_loader/medm/flowam_actions.py index 22a871a9..aade22bf 100644 --- a/python/tk_multi_loader/medm/flowam_actions.py +++ b/python/tk_multi_loader/medm/flowam_actions.py @@ -130,7 +130,7 @@ def _build_new_scene(self, sg_publish_data: dict) -> None: :param sg_publish_data: Shotgun data dictionary with all the standard publish fields. """ parent_window = self._get_dialog_parent() - sg_flow_am_id = self._app.context.project.get("sg_flow_am_id") + sg_flow_am_id = self._get_flowam_id() # Get the pipeline step from the task task = sg_publish_data.get("task") task_id = task.get("id") if task else None diff --git a/python/tk_multi_loader/medm/utils.py b/python/tk_multi_loader/medm/utils.py index 2f1c5b02..8725554e 100644 --- a/python/tk_multi_loader/medm/utils.py +++ b/python/tk_multi_loader/medm/utils.py @@ -17,11 +17,23 @@ from __future__ import annotations import os +from enum import Enum from typing import Any, Dict, Optional, Tuple +from tank_vendor.flow_integration_sdk.objects import FlowAsset + from ..constants import DRAFT_VERSION_IDENTIFIER +class CreateMode(Enum): + """Enum of modes for creating a new asset.""" + + NEW = "new" #: Create a DCC asset from a new scene as the source. + CURRENT = "current" #: Create a DCC asset from the current scene as the source. + TEMPLATE = "template" #: Create a DCC asset from template scene as the source. + GENERIC = "generic" #: Create a generic asset from a specified source file. + + def is_structural_asset(asset: Any, flow_module: Any) -> bool: """Return ``True`` when *asset* is a structural container in the FlowAM hierarchy. @@ -258,3 +270,18 @@ def resolve_publish_type( result: Tuple[Optional[int], str] = (sg_publish_type_id, display_name) cache.publish_types[medm_type_id_str] = result return result + + +def get_template_source_path(template: FlowAsset) -> str: + """Return the published source path of the given template. + Fetch binary if necessary. + + Args: + template: Template asset. + + Returns: + Full path to template file in blob storage. + """ + revision = template.get_latest_revision() + revision.fetch() + return revision.get_storage_source_path() From 4da2df7a188056d60252aaae88643ab529356616 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Mon, 15 Jun 2026 16:12:25 -0500 Subject: [PATCH 02/37] Migrate build asset/template workflows --- python/tk_multi_loader/build_asset_dialog.py | 2 +- .../tk_multi_loader/build_template_dialog.py | 2 +- python/tk_multi_loader/medm/create.py | 736 ++++++++++++++++++ python/tk_multi_loader/medm/flowam_actions.py | 25 +- python/tk_multi_loader/medm/utils.py | 80 +- 5 files changed, 812 insertions(+), 33 deletions(-) create mode 100644 python/tk_multi_loader/medm/create.py diff --git a/python/tk_multi_loader/build_asset_dialog.py b/python/tk_multi_loader/build_asset_dialog.py index ded8e0b0..fc5d4074 100644 --- a/python/tk_multi_loader/build_asset_dialog.py +++ b/python/tk_multi_loader/build_asset_dialog.py @@ -16,7 +16,7 @@ from tank_vendor.flow_integration_sdk.objects import FlowProject from .medm.template_queries import get_template_pipeline_steps, get_templates -from .medm.utils import CreateMode, get_template_source_path +from .medm.create import CreateMode, get_template_source_path from .ui.build_asset_dialog import Ui_BuildAssetDialog # Toolkit logger diff --git a/python/tk_multi_loader/build_template_dialog.py b/python/tk_multi_loader/build_template_dialog.py index 85eb7975..34b6c127 100644 --- a/python/tk_multi_loader/build_template_dialog.py +++ b/python/tk_multi_loader/build_template_dialog.py @@ -16,7 +16,7 @@ from tank_vendor.flow_integration_sdk.objects import FlowProject from .medm.template_queries import find_template_pipeline_step, get_templates -from .medm.utils import CreateMode +from .medm.create import CreateMode from .ui.build_template_dialog import Ui_BuildTemplateDialog # Toolkit logger diff --git a/python/tk_multi_loader/medm/create.py b/python/tk_multi_loader/medm/create.py new file mode 100644 index 00000000..08bbaba4 --- /dev/null +++ b/python/tk_multi_loader/medm/create.py @@ -0,0 +1,736 @@ +# Copyright (c) 2026 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. + +""" +This module contains utilities for creating medm assets. Some of these functions +create assets only in sandbox, while others create and publish them straight to +medm. +""" + +from __future__ import annotations + +import os +import tempfile +from dataclasses import dataclass +from enum import Enum +from typing import Callable + +import sgtk +from sgtk.flowam.utils import BaseInputs, create_components_for_publish +from tank_vendor.flow_integration_sdk.exceptions import FlowError +from tank_vendor.flow_integration_sdk.globals import FOLDER_TYPE_ID +from tank_vendor.flow_integration_sdk.objects import FlowAsset, FlowProject +from tank_vendor.flow_integration_sdk.publish import publish_new_asset +from tank_vendor.flow_integration_sdk.sandbox import ( + NewDraftInfo, + create_asset_in_sandbox, + read_draft_info, +) +from tank_vendor.flow_integration_sdk.schema import get_schema_id + +from .utils import cleanpath, fileext + +# --------------------------------- +# CONSTANTS +# --------------------------------- +# Folder names +ASSET_FOLDER = "Assets" +SHOT_FOLDER = "Shots" +GENERIC_FOLDER = "Generic" +TEMPLATE_FOLDER = "Templates" + +# SG entity types +SHOT_TYPE = "Shot" +ASSET_TYPE = "Asset" + +# Schema types +PIPELINE_STEP_TYPE = "type.pipelineStep" +TEMPLATE_TYPE = "type.template" + + +# --------------------------------- +# Classes +# --------------------------------- +class CreateMode(Enum): + """Enum of modes for creating a new asset.""" + + NEW = "new" #: Create a DCC asset from a new scene as the source. + CURRENT = "current" #: Create a DCC asset from the current scene as the source. + TEMPLATE = "template" #: Create a DCC asset from template scene as the source. + GENERIC = "generic" #: Create a generic asset from a specified source file. + + +class CreateAssetError(FlowError): + def __init__(self, *args, **kwargs): + message = "Could not create asset." + super().__init__(message, *args, **kwargs) + + +@dataclass +class CreateInputs(BaseInputs): + """Convenience structure to hold create inputs and allow them to be + passed easily between helper functions. + """ + + #: Entity type of SG asset. + sg_entity_type: str + #: Name of the SG asset. + #: This will be used for the AM asset name (both the container and workfile). + sg_entity_name: str + #: The name/code of the SG pipeline step associated with the current task context. + sg_pipeline_step: str + #: The AM project under which the asset should be added. + am_project_id: str + #: The name of the current SG task. + sg_task_name: str = "" + #: Description of asset. + description: str = "" + #: Determines which initial source file to use to create the asset. + #: See `CreateMode` enum for valid values. + create_mode: CreateMode = CreateMode.CURRENT + #: Relevant only in some modes. + #: * TEMPLATE -> path to the template file to be used to build the new asset + #: * GENERIC -> path to the source file to copy directly to asset + source_path: str = "" + #: Optional callback function that will be called after scene is prepared + prep_scene_callback: Callable | None = None + + def asdict(self): + """Custom asdict to handle Enums and callables.""" + + data = {} + for key, value in self.__dict__.items(): + if isinstance(value, Enum): + data[key] = value.value + elif callable(value): + data[key] = getattr(value, "__name__", str(value)) + else: + data[key] = value + return data + + def validate(self): + """Check that input combinations are valid. + + Raises: + CreateAssetError + """ + + # If sg entity name is provided, we also expect an entity type and pipeline step + if self.sg_entity_name and not self.sg_entity_type: + msg = "Incomplete sg context provided. Must provide sg_entity_type." + raise CreateAssetError(data=self.asdict(), details=msg) + if self.sg_entity_name and not self.sg_pipeline_step: + msg = "Incomplete sg context provided. Must provide sg_pipeline_step." + raise CreateAssetError(data=self.asdict(), details=msg) + # If create mode is TEMPLATE or GENERIC, we need a source path + if self.create_mode == CreateMode.TEMPLATE and not self.source_path: + msg = "No template source path provided." + raise CreateAssetError(data=self.asdict(), details=msg) + if self.create_mode == CreateMode.GENERIC and not self.source_path: + msg = "No source path provided for generic asset." + raise CreateAssetError(data=self.asdict(), details=msg) + # If pipeline step is provided, we expect task_name to be provided as well + if self.sg_pipeline_step and not self.sg_task_name: + msg = "Incomplete sg context provided. Must provide sg_task_name." + raise CreateAssetError(data=self.asdict(), details=msg) + # prep_scene_callback is only applicable when create_mode is NEW or TEMPLATE + if ( + self.create_mode == CreateMode.GENERIC + and self.prep_scene_callback is not None + ): + msg = "prep_scene_callback is not applicable when create_mode is GENERIC." + raise CreateAssetError(data=self.asdict(), details=msg) + + # There should always be a project id provided + if not self.am_project_id: + raise CreateAssetError( + data=self.asdict(), details="No project id provided." + ) + + +@dataclass +class CreateTemplateInputs(BaseInputs): + """Convenience structure to hold create inputs and allow them to be + passed easily between helper functions. + """ + + #: The name/code of the SG pipeline step that new template is for. + sg_pipeline_step: str + #: The AM project under which the template should be added. + am_project_id: str + #: The name new template asset. + template_name: str = "" + #: Description of template. + description: str = "" + #: Determines which initial source file to use to create the asset. + #: See `CreateMode` enum for valid values. + #: In the case of template creation, only NEW and CURRENT are applicable. + create_mode: CreateMode = CreateMode.CURRENT + + def validate(self): + """Check that input combinations are valid. + + Raises: + CreateAssetError + """ + + # Pipeline step value must be provided + if not self.sg_pipeline_step: + msg = "No pipeline step provided." + raise CreateAssetError(data=self.asdict(), details=msg) + # There should always be a project id provided + if not self.am_project_id: + raise CreateAssetError( + data=self.asdict(), details="No project id provided." + ) + # Template must have a name + if not self.template_name: + raise CreateAssetError( + data=self.asdict(), details="No template name provided." + ) + # Create mode TEMPLATE and GENERIC are not applicable for templates. + if ( + self.create_mode == CreateMode.TEMPLATE + or self.create_mode == CreateMode.GENERIC + ): + msg = f"Invalid CreateMode provided for template creation: {self.create_mode}." + raise CreateAssetError(data=self.asdict(), details=msg) + + +def get_template_source_path(template: FlowAsset) -> str: + """Return the published source path of the given template. + Fetch binary if necessary. + + Args: + template: Template asset. + + Returns: + Full path to template file in blob storage. + """ + revision = template.get_latest_revision() + revision.fetch() + return revision.get_storage_source_path() + + +# --------------------------------- +# Workflows +# --------------------------------- +def create_dcc_workfile(inputs: CreateInputs) -> NewDraftInfo: + """Create a DCC workfile asset in sandbox based on criteria provided in inputs. + See documentation for CreateInputs for expected inputs. + + .. note:: Inputs can be passed in as a CreateInputs object assigned to the keyword + argument _inputs_ or as a set of individual parameters. (e.g. sg_entity_name="my_name") + + Returns: + A NewDraftInfo object containing all pertinent information about + draft created for new asset, including draft id. + + Raises: + CreateAssetError + """ + app = sgtk.platform.current_bundle() + inputs.log_intro("Creating new DCC workfile asset") + inputs.validate() + + if not in_dcc_context(): + msg = "Cannot create DCC workfile without being in DCC FlowContext." + raise CreateAssetError(data=inputs.asdict(), details=msg) + + # Create any necessary hierarchy above current asset + parent = _create_asset_hierarchy(inputs) + + # Create the workfile asset in sandbox + draft_id = _create_dcc_workfile_asset(parent, inputs) + + app.log_info("Creating DCC asset complete!") + + # Open the draft file + draft_info = read_draft_info(draft_id) + draft_path = draft_info.source_path + app.log_info(f"Opening draft path: {draft_path}") + flow_host().open_file(draft_path) + + # Set asset context (before it was only FlowContext.draft_id) + app.context.set_flow_context(flow_host().current_file) + + return draft_info + + +def create_template_workfile(inputs: CreateTemplateInputs) -> NewDraftInfo: + """Create a DCC workfile asset in sandbox based on criteria provided in inputs. + See documentation for CreateTemplateInputs for expected inputs. + + .. note:: Inputs can be passed in as a CreateTemplateInputs object assigned to the keyword + argument _inputs_ or as a set of individual parameters. (e.g. sg_entity_name="my_name") + + Returns: + A NewDraftInfo object containing all pertinent information about + draft created for new asset, including draft id. + + Raises: + CreateAssetError + """ + app = sgtk.platform.current_bundle() + + inputs.log_intro("Creating new template asset") + inputs.validate() + + if not in_dcc_context(): + msg = "Cannot create template workfile without being in DCC FlowContext." + raise CreateAssetError(data=inputs.asdict(), details=msg) + + # Create any necessary hierarchy above current asset + parent = _create_template_hierarchy(inputs) + + # Create the workfile asset in sandbox + draft_id = _create_template_workfile_asset(parent, inputs) + + app.log_info("Creating template asset complete!") + + # Open the draft file + draft_info = read_draft_info(draft_id) + draft_path = draft_info.source_path + app.log_info(f"Opening draft path: {draft_path}") + flow_host().open_file(draft_path) + + # Set asset context (before it was only FlowContext.draft_id) + app.context.set_flow_context(flow_host().current_file) + + return draft_info + + +# --------------------------------- +# Auxiliary functions for workflows +# --------------------------------- +def flow_host() -> sgtk.flowam.host.FlowHost: + """Convenience function to return the current host.""" + return sgtk.platform.current_engine().flow_host + + +def in_dcc_context() -> bool: + """Return True if currently in a DCC context (i.e. engine is not tk-desktop).""" + engine = sgtk.platform.current_engine() + return engine.name != "tk-desktop" + + +def _has_workfile_type(parent: FlowAsset, type_id: str) -> bool: + """Return True if parent asset contains a child of given type.""" + + if parent.find_children(type_id=type_id): + return True + return False + + +def _create_asset_hierarchy(inputs: CreateInputs) -> FlowAsset: + """Called when creating an asset for an sg entity for the first time. + This function will ensure that any hierarchical structuring above the workfile asset + is created if necessary. (These will be committed directly to remote immediately.) + + High-level structure of SG-mirrored project in AM + ------------------------------------------------- + + - PROJECT + - SHOTS FOLDER + - sg shot 1 + - pipeline step 1 + - task 1 folder + - sg shot 1 (workfile) + - task 2 folder + - sg shot 1 (workfile) + - pipeline step 2 + - task 1 folder + - sg shot 1 (workfile) + - task 2 folder + - sg shot 1 (workfile) + ... + - sg shot 2 + ... + ... + - ASSETS FOLDER + - sg asset 1 + - pipeline step 1 + - task 1 folder + - sg asset 1 (workfile) + - task 2 folder + - sg asset 1 (workfile) + - pipeline step 2 + - task 1 folder + - sg asset 1 (workfile) + - task 2 folder + - sg asset 1 (workfile) + ... + - sg asset 2 + ... + ... + - GENERIC FOLDER + - generic asset 1 (workfile) + - generic asset 2 (workfile) + ... + + Args: + See CreateInputs documentation. + + Returns: + The parent asset of the workfile asset to be created. + """ + + root_folder = _get_or_create_root_folder(inputs) + + # Create or retrieve parent asset + if inputs.sg_entity_name: + parent = _get_or_create_workfile_parent(root_folder, inputs) + else: + # If no sg entity context was provided, folder will be parent + # (applicable to generic assets) + parent = root_folder + + return parent + + +def _get_or_create_root_folder(inputs: CreateInputs) -> FlowAsset: + """Retrieve top-level folder pertinent to new asset. If it doesn't exist, create it. + + Returns: + Folder asset object. + + Raises: + CreateAssetError + """ + app = sgtk.platform.current_bundle() + + am_project_id = inputs.am_project_id + sg_entity_type = inputs.sg_entity_type + + try: + project = FlowProject(am_project_id) + except FlowError as exc: + msg = f"Invalid Flow project id provided: {am_project_id}" + raise CreateAssetError(data=inputs.asdict(), details=msg) from exc + + # Create top-level folders if they don't already exist in project + + if sg_entity_type == SHOT_TYPE: + # Create shot folder or grab existing one if sg entity is a shot type + folder = project.find_child(SHOT_FOLDER) + if not folder: + app.log_info(f'Creating "{SHOT_FOLDER}" folder...') + desc = "Folder for Shot assets." + folder = publish_new_asset( + name=SHOT_FOLDER, + parent=project, + components=create_components_for_publish( + type_ids=[FOLDER_TYPE_ID], + ), + description=desc, + ) + elif sg_entity_type == ASSET_TYPE: + # Create asset folder or grab existing one if sg entity is an asset type + folder = project.find_child(ASSET_FOLDER) + if not folder: + app.log_info(f'Creating "{ASSET_FOLDER}" folder...') + desc = "Folder for Asset Build assets." + folder = publish_new_asset( + name=ASSET_FOLDER, + parent=project, + components=create_components_for_publish( + type_ids=[FOLDER_TYPE_ID], + ), + description=desc, + ) + elif inputs.create_mode == CreateMode.GENERIC: + # Create generic folder or grab existing one + folder = project.find_child(GENERIC_FOLDER) + if not folder: + app.log_info(f'Creating "{GENERIC_FOLDER}" folder...') + desc = "Folder for Generic assets." + folder = publish_new_asset( + name=GENERIC_FOLDER, + parent=project, + components=create_components_for_publish( + type_ids=[FOLDER_TYPE_ID], + ), + description=desc, + ) + else: + msg = f"Invalid entity type provided: {sg_entity_type}." + raise CreateAssetError(data=inputs.asdict(), details=msg) + + return folder + + +def _get_or_create_workfile_parent( + root_folder: FlowAsset, inputs: CreateInputs +) -> FlowAsset: + """Determine the appropriate parent asset of the workfile to be created. + This should be a pipeline step asset since we will only call this function + if a sg context was provided. + + Any necessary hierarchical assets that don't exist will be created + (i.e. containers and pipeline steps). + + Returns: + The parent asset object. + """ + app = sgtk.platform.current_bundle() + + sg_entity_type = inputs.sg_entity_type + sg_entity_name = inputs.sg_entity_name + sg_pipeline_step = inputs.sg_pipeline_step + sg_task_name = inputs.sg_task_name + + # If a container asset associated with sg entity doesn't exist, create it + container = root_folder.find_child(sg_entity_name) + if not container: + app.log_info( + f'Creating container asset for "{sg_entity_name}" under folder "{root_folder.name}"...' + ) + container_type = ( + "type.container.asset" + if sg_entity_type == ASSET_TYPE + else "type.container.shot" + ) + container_type_id = get_schema_id(container_type) + container = publish_new_asset( + name=sg_entity_name, + parent=root_folder, + components=create_components_for_publish( + type_ids=[container_type_id], + ), + ) + + # If a pipeline step asset associated with sg pipeline step doesn't exist, create it + pipeline_step = container.find_child(sg_pipeline_step) + if not pipeline_step: + app.log_info(f'Creating pipeline step asset for "{sg_pipeline_step}"...') + pipeline_step_type_id = get_schema_id(PIPELINE_STEP_TYPE) + pipeline_step = publish_new_asset( + name=sg_pipeline_step, + parent=container, + components=create_components_for_publish( + type_ids=[pipeline_step_type_id], + ), + ) + + # If a task folder associated with sg task doesn't exist, create it + task_folder = pipeline_step.find_child(sg_task_name) + if not task_folder: + app.log_info(f'Creating task folder asset for "{sg_task_name}"...') + desc = f'Folder for task "{sg_task_name}".' + task_folder = publish_new_asset( + name=sg_task_name, + parent=pipeline_step, + components=create_components_for_publish( + type_ids=[FOLDER_TYPE_ID], + ), + description=desc, + ) + + return task_folder + + +def _create_dcc_workfile_asset(parent: FlowAsset, inputs: CreateInputs) -> str: + """Called when creating a new dcc workfile asset. + This function will create the workfile asset in sandbox under the given parent. + + Args: + parent: Asset to create workfile asset under. + See CreateInputs documentation. + + Returns: + The draft id of the workfile asset created. + + Raises: + CreateAssetError + """ + app = sgtk.platform.current_bundle() + + # Determine workfile type to be created + workfile_type = flow_host().WORKFILE_TYPE + type_id = get_schema_id(workfile_type) + + # Only allow one workfile of DCC type under parent + if _has_workfile_type(parent, type_id): + msg = f'A workfile of type "{workfile_type}" has already been created ' + msg += f'under pipeline step "{parent.name}". Please open the asset from ' + msg += "the Loader app to publish another revision of this asset." + raise CreateAssetError(data=inputs.asdict(), details=msg) + + # By convention asset name will be the sg entity name + name = inputs.sg_entity_name + + # Prepare the source file and save to temporary location + # By convention the source file will be named after the asset + with tempfile.TemporaryDirectory() as temp_dir: + ext = fileext(inputs.source_path) or flow_host().FILE_TYPES[0] + temp_file = cleanpath(temp_dir, f"{name}.{ext}") + if inputs.create_mode == CreateMode.NEW: + # Clear scene + flow_host().new_scene() + elif inputs.create_mode == CreateMode.TEMPLATE: + # Open the template file + if not os.path.exists(inputs.source_path): + msg = f"Template path does not exist: {inputs.source_path}" + raise CreateAssetError(data=inputs.asdict(), details=msg) + try: + flow_host().open_file(inputs.source_path) + except Exception as exc: # pylint: disable=broad-except + msg = f"Could not open template path: {inputs.source_path}" + raise CreateAssetError(data=inputs.asdict(), details=msg) from exc + + # Call prep scene callback if provided + if inputs.prep_scene_callback: + try: + inputs.prep_scene_callback() + except Exception as exc: # pylint: disable=broad-except + msg = f"Error running prep_scene_callback during scene prep: {exc}" + raise CreateAssetError(data=inputs.asdict(), details=msg) + + app.log_info(f"Saving temp file to: {temp_file}") + flow_host().save_file(temp_file) + + # Create a new asset in sandbox with a unique draft id + app.log_info( + f'Creating a workfile asset of type "{workfile_type}" for sg entity "{inputs.sg_entity_name}" in sandbox...' + ) + desc = inputs.description + draft_id = publish_new_asset( + name=name, + description=desc, + parent_id=parent.id, + components=create_components_for_publish( + type_ids=[type_id], + ), + source_path=temp_file, + ) + + return draft_id + + +def _create_template_hierarchy(inputs: CreateTemplateInputs) -> FlowAsset: + """Called when creating a template asset. + This function will ensure that any hierarchical structuring above the workfile asset + is created if necessary. (These will be committed directly to remote immediately.) + + High-level structure of template organization in AM project + ----------------------------------------------------------- + + - PROJECT + - TEMPLATES FOLDER + - pipeline step 1 + - template 1 (workfile) + - template 2 (workfile) + - pipeline step 2 + - template 1 (workfile) + - template 2 (workfile) + ... + - pipeline step 3 + ... + + Args: + See CreateTemplateInputs documentation. + + Returns: + The parent asset of the workfile asset to be created. + + Raises: + CreateAssetError + """ + app = sgtk.platform.current_bundle() + + am_project_id = inputs.am_project_id + sg_pipeline_step = inputs.sg_pipeline_step + + try: + project = FlowProject(am_project_id) + except FlowError as exc: + msg = f"Invalid Flow project id provided: {am_project_id}" + raise CreateAssetError(data=inputs.asdict(), details=msg) from exc + + # Create top-level folder if it doesn't already exist in project + folder = project.find_child(TEMPLATE_FOLDER) + if not folder: + app.log_info(f'Creating "{TEMPLATE_FOLDER}" folder...') + desc = "Folder for template assets." + folder = publish_new_asset( + name=TEMPLATE_FOLDER, + parent=project, + components=create_components_for_publish( + type_ids=[FOLDER_TYPE_ID], + ), + description=desc, + ) + + # Create pipeline step if necessary + # If a pipeline step asset associated with sg pipeline step doesn't exist, create it + pipeline_step = folder.find_child(sg_pipeline_step) + if not pipeline_step: + app.log_info(f'Creating pipeline step asset for "{sg_pipeline_step}"...') + pipeline_step_type_id = get_schema_id(PIPELINE_STEP_TYPE) + pipeline_step = publish_new_asset( + name=sg_pipeline_step, + parent=folder, + components=create_components_for_publish( + type_ids=[pipeline_step_type_id], + ), + ) + + return pipeline_step + + +def _create_template_workfile_asset( + parent: FlowAsset, inputs: CreateTemplateInputs +) -> str: + """Called when creating a new template workfile asset. + This function will create the workfile asset in sandbox under the given parent. + + Args: + parent: Asset to create workfile asset under. + See CreateTemplateInputs documentation. + + Returns: + The draft id of the workfile asset created. + """ + app = sgtk.platform.current_bundle() + + # Determine workfile type to be created + # NOTE: templates will have dual types, both a dcc type + # and template designation + workfile_type = flow_host().WORKFILE_TYPE + workfile_type_id = get_schema_id(workfile_type) + template_type_id = get_schema_id(TEMPLATE_TYPE) + + name = inputs.template_name + + # Prepare the source file and save to temporary location + # By convention the source file will be named after the asset + with tempfile.TemporaryDirectory() as temp_dir: + ext = flow_host().FILE_TYPES[0] + temp_file = cleanpath(temp_dir, f"{name}.{ext}") + if inputs.create_mode == CreateMode.NEW: + # Clear scene + flow_host().new_scene() + app.log_info(f"Saving temp file to: {temp_file}") + flow_host().save_file(temp_file) + + # Create a new asset in sandbox with a unique draft id + app.log_info( + f'Creating a template asset of type "{workfile_type}" for pipeline step "{inputs.sg_pipeline_step}" in sandbox...' + ) + desc = inputs.description + draft_id = create_asset_in_sandbox( + name=name, + description=desc, + parent_id=parent.id, + type_ids=[workfile_type_id, template_type_id], # flag as template type + source_path=temp_file, + ) + + return draft_id diff --git a/python/tk_multi_loader/medm/flowam_actions.py b/python/tk_multi_loader/medm/flowam_actions.py index aade22bf..4a3282d2 100644 --- a/python/tk_multi_loader/medm/flowam_actions.py +++ b/python/tk_multi_loader/medm/flowam_actions.py @@ -20,6 +20,14 @@ from ..build_asset_dialog import BuildAssetDialog from ..build_template_dialog import BuildTemplateDialog from ..constants import DRAFT_VERSION_IDENTIFIER +from .create import ( + CreateAssetError, + CreateInputs, + CreateMode, + CreateTemplateInputs, + create_dcc_workfile, + create_template_workfile, +) class FlowAMActions: @@ -156,12 +164,11 @@ def _on_build_scene_dialog_accepted( self._app.log_warning(message) return - flow_module = self.load_framework("tk-framework-flowam", "flow") parent_window = self._get_dialog_parent() sg_flow_am_id = self._get_flowam_id() - if dialog.build == flow_module.asset_management.CreateMode.TEMPLATE: + if dialog.build == CreateMode.TEMPLATE: template_path = dialog.template_source_path else: template_path = "" @@ -175,7 +182,7 @@ def _on_build_scene_dialog_accepted( or {} ) - create_inputs = flow_module.asset_management.CreateInputs( + create_inputs = CreateInputs( sg_entity_type=sg_publish_data["entity"]["type"], # Asset, Shot, etc. sg_entity_name=sg_publish_data["entity"]["name"], sg_pipeline_step=(task.get("step") or {}).get( @@ -189,11 +196,11 @@ def _on_build_scene_dialog_accepted( ) try: - draft_info = flow_module.asset_management.create_dcc_workfile(create_inputs) + draft_info = create_dcc_workfile(create_inputs) self._app.log_debug( f"Created a DCC workfile on Flow AM framework with the draft_id: {draft_info.draft_id}" ) - except flow_module.CreateAssetError as exc: + except CreateAssetError as exc: self._app.log_error(f"Create asset failed: {exc}\nInput data: {exc.data}") QtGui.QMessageBox.critical( @@ -366,19 +373,15 @@ def _on_build_template_dialog_accepted( QtGui.QMessageBox.critical(parent_window, "Error", message) return - flow_module = self.load_framework("tk-framework-flowam", "flow") - sg_flow_am_id = self._get_flowam_id() - create_inputs = flow_module.asset_management.CreateTemplateInputs( + create_inputs = CreateTemplateInputs( sg_pipeline_step=dialog.step, am_project_id=sg_flow_am_id, template_name=dialog.template, create_mode=dialog.mode, ) - draft_info = flow_module.asset_management.create_template_workfile( - create_inputs - ) + draft_info = create_template_workfile(create_inputs) self._app.log_debug( f"Created a Template workfile on Flow AM framework with the draft_id: {draft_info.draft_id}" ) diff --git a/python/tk_multi_loader/medm/utils.py b/python/tk_multi_loader/medm/utils.py index 8725554e..ab1292f8 100644 --- a/python/tk_multi_loader/medm/utils.py +++ b/python/tk_multi_loader/medm/utils.py @@ -17,23 +17,11 @@ from __future__ import annotations import os -from enum import Enum from typing import Any, Dict, Optional, Tuple -from tank_vendor.flow_integration_sdk.objects import FlowAsset - from ..constants import DRAFT_VERSION_IDENTIFIER -class CreateMode(Enum): - """Enum of modes for creating a new asset.""" - - NEW = "new" #: Create a DCC asset from a new scene as the source. - CURRENT = "current" #: Create a DCC asset from the current scene as the source. - TEMPLATE = "template" #: Create a DCC asset from template scene as the source. - GENERIC = "generic" #: Create a generic asset from a specified source file. - - def is_structural_asset(asset: Any, flow_module: Any) -> bool: """Return ``True`` when *asset* is a structural container in the FlowAM hierarchy. @@ -272,16 +260,68 @@ def resolve_publish_type( return result -def get_template_source_path(template: FlowAsset) -> str: - """Return the published source path of the given template. - Fetch binary if necessary. +def cleanpath(path: str, *extra: str) -> str: + """Return the same path, normalized and using only front slashes. Args: - template: Template asset. + path: String absolute or relative path. + *extra: Zero or more string arguments representing extra bits to + add to input path in given order. Returns: - Full path to template file in blob storage. + str: Path that is the product of all input parameters joined. + + Examples: + >>> cleanpath('c:\\dev\\my_root', 'my_dir', 'my_file.ma') + 'c:/dev/my_root/my_dir/my_file.ma' + >>> cleanpath('/Users//smith/folder1/file1.txt') + '/Users/smith/folder1/file1.txt' + >>> cleanpath('C:/temp/some_dir/', '/some_folder/') + 'C:/temp/some_dir/some_folder' + >>> cleanpath('/Applications', '/\\some_app') + '/Applications/some_app' + >>> cleanpath('D:', 'MIM_Files') + 'D:/MIM_Files' + >>> cleanpath('D:\\\\', 'MIM_Files') + 'D:/MIM_Files' + >>> cleanpath('') + '' + >>> cleanpath('', 'blah', 'blah') + 'blah/blah' + >>> cleanpath('/path/to/dir/') + '/path/to/dir' + >>> cleanpath('/path/./to/../file.txt') + '/path/file.txt' + """ + # Add slash if first argument is a drive + # (os.path.join will not add one in this case) + if path.endswith(":"): + path += "/" + # Must strip any leading slashes from extra bits + extras = [] + for ext in extra: + extras.append(ext.lstrip("/\\")) + result = os.path.join(path, *extras) + if not result: + return "" + return os.path.normpath(result).replace("\\", "/") + + +def fileext(filepath: str): + """Return extension of given file path without the dot and in lower case. + Examples: + >>> fileext('c:/temp/file.txt') + 'txt' + >>> fileext('dir/another_dir/file.PNG') + 'png' + >>> fileext('dir/another_dir') + '' + >>> fileext('dir/another.dir/folder') + '' + >>> fileext('file.backup.tar.gz') + 'gz' """ - revision = template.get_latest_revision() - revision.fetch() - return revision.get_storage_source_path() + filename = os.path.basename(filepath) + if "." not in filename: + return "" + return os.path.splitext(filename)[-1].strip(".").lower() From eb20c5a44f15c5a3b2beb3968eba8166c1f39318 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Tue, 16 Jun 2026 08:52:23 -0500 Subject: [PATCH 03/37] Remove more references --- python/tk_multi_loader/medm/create.py | 1 + python/tk_multi_loader/medm/entity_model.py | 6 ++-- .../medm/latestpublish_model.py | 10 ++---- .../medm/publishhistory_model.py | 4 +-- .../tk_multi_loader/medm/template_queries.py | 35 ++++++++----------- python/tk_multi_loader/medm/utils.py | 24 ++++++------- 6 files changed, 34 insertions(+), 46 deletions(-) diff --git a/python/tk_multi_loader/medm/create.py b/python/tk_multi_loader/medm/create.py index 08bbaba4..d687fdcb 100644 --- a/python/tk_multi_loader/medm/create.py +++ b/python/tk_multi_loader/medm/create.py @@ -51,6 +51,7 @@ ASSET_TYPE = "Asset" # Schema types +CONTAINER_TYPE = "type.container" PIPELINE_STEP_TYPE = "type.pipelineStep" TEMPLATE_TYPE = "type.template" diff --git a/python/tk_multi_loader/medm/entity_model.py b/python/tk_multi_loader/medm/entity_model.py index ebc4ecd9..6d42a76a 100644 --- a/python/tk_multi_loader/medm/entity_model.py +++ b/python/tk_multi_loader/medm/entity_model.py @@ -304,7 +304,7 @@ def _is_tree_node(self, asset: Asset) -> bool: :param asset: FlowAM ``Asset`` to test. :returns: ``True`` if the asset should appear in the tree. """ - if _is_structural_asset_util(asset, self._flow_module): + if _is_structural_asset_util(asset): return True # Non-structural: show in the tree only when the asset has direct @@ -326,9 +326,7 @@ def _icon_for_asset(self, asset: Asset) -> QtGui.QIcon: :returns: A :class:`QtGui.QIcon` instance. """ return ( - self._folder_icon - if _is_structural_asset_util(asset, self._flow_module) - else self._binary_icon + self._folder_icon if _is_structural_asset_util(asset) else self._binary_icon ) def _load_medm_assets(self) -> None: diff --git a/python/tk_multi_loader/medm/latestpublish_model.py b/python/tk_multi_loader/medm/latestpublish_model.py index 09caedc0..c6dda102 100644 --- a/python/tk_multi_loader/medm/latestpublish_model.py +++ b/python/tk_multi_loader/medm/latestpublish_model.py @@ -211,9 +211,7 @@ def _populate_model_from_selected_item( # be the publishable leaf. Only fall back to showing it directly when # the asset is NOT a structural container. leaf_asset_fallback = None - if not children_asset_sg_dicts and not _is_structural_asset_util( - asset, self._flow_module - ): + if not children_asset_sg_dicts and not _is_structural_asset_util(asset): try: children_asset_sg_dicts = [self._asset_to_sg_dict(asset)] except Exception as e: @@ -351,7 +349,7 @@ def _fetch_asset_children(self, asset: Asset) -> List[Dict[str, Any]]: self._cache.children[asset.id] = child_assets for child_asset in child_assets: - if _is_structural_asset_util(child_asset, self._flow_module): + if _is_structural_asset_util(child_asset): self._app.log_debug( f"FlowAM: Skipping structural asset '{child_asset.name}' from center panel" ) @@ -549,9 +547,7 @@ def _fetch_new_draft_items_for_parent( return result def _resolve_publish_type(self, medm_type_id_str: str) -> tuple: - return resolve_publish_type( - medm_type_id_str, self._cache, self._flow_module, self._app - ) + return resolve_publish_type(medm_type_id_str, self._cache, self._app) def _add_sg_dict_as_qt_item(self, sg_item: Dict[str, Any]) -> None: """ diff --git a/python/tk_multi_loader/medm/publishhistory_model.py b/python/tk_multi_loader/medm/publishhistory_model.py index f2d7c880..8ca56d83 100644 --- a/python/tk_multi_loader/medm/publishhistory_model.py +++ b/python/tk_multi_loader/medm/publishhistory_model.py @@ -419,9 +419,7 @@ def get_sg_data(): ) def _resolve_publish_type(self, medm_type_id_str: str) -> tuple: - return resolve_publish_type( - medm_type_id_str, self._cache, self._flow_module, self._app - ) + return resolve_publish_type(medm_type_id_str, self._cache, self._app) def _resolve_and_download_thumbnail( self, qt_item: QtGui.QStandardItem, revision_id: str diff --git a/python/tk_multi_loader/medm/template_queries.py b/python/tk_multi_loader/medm/template_queries.py index fa99e1fa..bb90a1c8 100644 --- a/python/tk_multi_loader/medm/template_queries.py +++ b/python/tk_multi_loader/medm/template_queries.py @@ -8,60 +8,55 @@ # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Shotgun Software Inc. -"""Template query helpers for Flow Asset Management. - -These functions mirror the template-browsing API exposed by the -``tk-framework-flowam`` ``flow.asset_management`` module, but load the -framework lazily so callers do not need a direct framework reference. -""" +"""Template query helpers for Flow Asset Management.""" from __future__ import annotations from typing import Any, Optional import sgtk +from tank_vendor.flow_integration_sdk.objects import FlowAsset, FlowProject +from tank_vendor.flow_integration_sdk.schema import get_schema_id + +from .create import PIPELINE_STEP_TYPE, TEMPLATE_FOLDER, TEMPLATE_TYPE logger = sgtk.platform.get_logger(__name__) -def get_template_pipeline_steps(project: Any) -> list[Any]: +def get_template_pipeline_steps(project: FlowProject) -> list[FlowAsset]: """Return pipeline steps available under the Templates folder. - :param project: Flow AM ``Project`` instance to query. + :param project: Flow AM ``FlowProject`` instance to query. :returns: List of pipeline step ``Asset`` objects found under the Templates folder, or an empty list when the folder is absent. """ - _flow = sgtk.platform.import_framework("tk-framework-flowam", "flow") - _am = _flow.asset_management - template_folder = project.find_child(_am.TEMPLATE_FOLDER) + template_folder = project.find_child(TEMPLATE_FOLDER) if not template_folder: return [] - pipeline_step_type_id = _flow.schema.get_schema_id(_am.PIPELINE_STEP_TYPE) + pipeline_step_type_id = get_schema_id(PIPELINE_STEP_TYPE) return template_folder.find_children(type_id=pipeline_step_type_id) -def get_templates(pipeline_step: Any) -> list[Any]: +def get_templates(pipeline_step: FlowAsset) -> list[FlowAsset]: """Return template assets available under a given pipeline step. :param pipeline_step: Flow AM ``Asset`` representing a pipeline step. :returns: List of template ``Asset`` objects under the pipeline step. """ - _flow = sgtk.platform.import_framework("tk-framework-flowam", "flow") - _am = _flow.asset_management - template_type_id = _flow.schema.get_schema_id(_am.TEMPLATE_TYPE) + template_type_id = get_schema_id(TEMPLATE_TYPE) return pipeline_step.find_children(type_id=template_type_id) -def find_template_pipeline_step(project: Any, pipeline_step_name: str) -> Optional[Any]: +def find_template_pipeline_step( + project: FlowProject, pipeline_step_name: str +) -> Optional[FlowAsset]: """Find a pipeline step by name under the Templates folder. :param project: Flow AM ``Project`` instance to query. :param pipeline_step_name: Name of the pipeline step to look for. :returns: The matching pipeline step ``Asset``, or ``None`` if not found. """ - _flow = sgtk.platform.import_framework("tk-framework-flowam", "flow") - _am = _flow.asset_management - template_folder = project.find_child(_am.TEMPLATE_FOLDER) + template_folder = project.find_child(TEMPLATE_FOLDER) if not template_folder: return None return template_folder.find_child(pipeline_step_name) diff --git a/python/tk_multi_loader/medm/utils.py b/python/tk_multi_loader/medm/utils.py index ab1292f8..bb723149 100644 --- a/python/tk_multi_loader/medm/utils.py +++ b/python/tk_multi_loader/medm/utils.py @@ -19,10 +19,17 @@ import os from typing import Any, Dict, Optional, Tuple +from tank_vendor.flow_integration_sdk.globals import FOLDER_TYPE_ID +from tank_vendor.flow_integration_sdk.schema import ( + get_schema_display_name, + get_schema_id, +) + from ..constants import DRAFT_VERSION_IDENTIFIER +from .create import CONTAINER_TYPE, PIPELINE_STEP_TYPE -def is_structural_asset(asset: Any, flow_module: Any) -> bool: +def is_structural_asset(asset: Any) -> bool: """Return ``True`` when *asset* is a structural container in the FlowAM hierarchy. An asset is considered structural — and therefore belongs only in the @@ -38,20 +45,16 @@ def is_structural_asset(asset: Any, flow_module: Any) -> bool: ``False`` (treat the asset as publishable). :param asset: FlowAM ``Asset`` object to test. - :param flow_module: The ``flow`` framework module imported via - ``sgtk.platform.import_framework("tk-framework-flowam", "flow")``. :returns: ``True`` if the asset is a structural container. """ try: type_ids = set(getattr(asset, "type_ids", None) or []) - if flow_module.data.FOLDER_TYPE_ID in type_ids: + if FOLDER_TYPE_ID in type_ids: return True - _am = flow_module.asset_management - structural_types = (_am.CONTAINER_TYPE, _am.PIPELINE_STEP_TYPE) + structural_types = (CONTAINER_TYPE, PIPELINE_STEP_TYPE) return any( - asset.find_component(type_id=flow_module.schema.get_schema_id(ct)) - for ct in structural_types + asset.find_component(type_id=get_schema_id(ct)) for ct in structural_types ) except Exception: return False @@ -200,7 +203,6 @@ def build_draft_sg_dict( def resolve_publish_type( medm_type_id_str: str, cache: Any, - flow_module: Any, app: Any, ) -> Tuple[Optional[int], str]: """Resolve a FlowAM schema type ID to a ``(sg_publish_type_id, display_name)`` pair. @@ -214,8 +216,6 @@ def resolve_publish_type( :param medm_type_id_str: FlowAM schema type ID string. :param cache: :class:`MedmSharedCache` instance whose ``publish_types`` dict is used for caching. - :param flow_module: The ``flow`` framework module imported via - ``sgtk.platform.import_framework("tk-framework-flowam", "flow")``. :param app: The current Toolkit bundle (provides ``shotgun`` and ``log_debug``). :returns: Tuple of ``(integer_publish_type_id_or_none, human_readable_display_name)``. """ @@ -224,7 +224,7 @@ def resolve_publish_type( display_name = medm_type_id_str try: - schema_name = flow_module.schema.get_schema_display_name(medm_type_id_str) + schema_name = get_schema_display_name(medm_type_id_str) if schema_name: display_name = schema_name except Exception as e: From 9c0d72d4e3e5c39a7569dff23d7f234d3dae1269 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Tue, 16 Jun 2026 10:54:29 -0500 Subject: [PATCH 04/37] Use sandbox functions --- python/tk_multi_loader/medm/flowam_actions.py | 44 +++++-------------- 1 file changed, 11 insertions(+), 33 deletions(-) diff --git a/python/tk_multi_loader/medm/flowam_actions.py b/python/tk_multi_loader/medm/flowam_actions.py index 4a3282d2..b9239a5c 100644 --- a/python/tk_multi_loader/medm/flowam_actions.py +++ b/python/tk_multi_loader/medm/flowam_actions.py @@ -16,6 +16,12 @@ import sgtk from sgtk import TankError from sgtk.platform.qt import QtGui +from tank_vendor.flow_integration_sdk.sandbox import ( + get_draft_folder, + is_local_draft, + is_new_asset, + read_draft_info, +) from ..build_asset_dialog import BuildAssetDialog from ..build_template_dialog import BuildTemplateDialog @@ -81,8 +87,8 @@ def _do_open(self, sg_publish_data: dict) -> None: flow_module = self.load_framework("tk-framework-flowam", "flow") - if version_number == DRAFT_VERSION_IDENTIFIER and self._is_local_draft( - sg_publish_data + if version_number == DRAFT_VERSION_IDENTIFIER and is_local_draft( + sg_publish_data.get("sg_flow_revision_id") ): flow_module.asset_management.open_draft(flow_revision_id) elif version_number > DRAFT_VERSION_IDENTIFIER: @@ -223,30 +229,6 @@ def _prep_scene(self, sg_publish_data: dict) -> None: # TDs can override this method to add custom scene prep logic pass - def _is_local_draft(self, sg_publish_data: dict) -> bool: - """ - Check if the given PublishedFile is a local AM draft. - - :param sg_publish_data: FPTR data dictionary with all the standard entity fields. - :returns: True if it's a local draft, False otherwise. - """ - flow_module = self.load_framework("tk-framework-flowam", "flow") - - return flow_module.sandbox.is_local_draft( - sg_publish_data.get("sg_flow_revision_id") - ) - - def _is_new_asset(self, draft_id: str | None) -> bool: - """ - Check if the given draft ID corresponds to a new asset draft. - - :param draft_id: The draft ID to check. - :returns: True if it's a new asset draft, False otherwise. - """ - flow_module = self.load_framework("tk-framework-flowam", "flow") - - return flow_module.sandbox.is_new_asset(draft_id) - def _discard_draft(self, sg_publish_data: dict) -> None: """ Discard the local draft for the given PublishedFile. @@ -256,11 +238,9 @@ def _discard_draft(self, sg_publish_data: dict) -> None: flow_module = self.load_framework("tk-framework-flowam", "flow") parent_window = self._get_dialog_parent() - draft_folder = flow_module.sandbox.get_draft_folder( - sg_publish_data.get("sg_flow_revision_id") - ) + draft_folder = get_draft_folder(sg_publish_data.get("sg_flow_revision_id")) - if flow_module.sandbox.is_new_asset(sg_publish_data.get("sg_flow_revision_id")): + if is_new_asset(sg_publish_data.get("sg_flow_revision_id")): # Case 1: new asset message = ( f"Discard the new unpublished asset {sg_publish_data.get('name')}?" @@ -269,9 +249,7 @@ def _discard_draft(self, sg_publish_data: dict) -> None: ) else: # Case 2: draft of existing asset - draft_info = flow_module.sandbox.read_draft_info( - sg_publish_data.get("sg_flow_revision_id") - ) + draft_info = read_draft_info(sg_publish_data.get("sg_flow_revision_id")) version = draft_info.version message = ( f"Discard the draft of asset {sg_publish_data.get('name')} checked out from version {version}?" From 5f2375400a9619653ad7aa0dc7de59b83063e44e Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Tue, 16 Jun 2026 10:54:48 -0500 Subject: [PATCH 05/37] Update references on models --- python/tk_multi_loader/medm/entity_model.py | 41 ++++++++---------- .../medm/latestpublish_model.py | 43 ++++++++----------- .../medm/publishhistory_model.py | 35 ++++----------- 3 files changed, 44 insertions(+), 75 deletions(-) diff --git a/python/tk_multi_loader/medm/entity_model.py b/python/tk_multi_loader/medm/entity_model.py index 6d42a76a..51f2d1a5 100644 --- a/python/tk_multi_loader/medm/entity_model.py +++ b/python/tk_multi_loader/medm/entity_model.py @@ -27,17 +27,15 @@ import sgtk from sgtk.platform.qt import QtCore, QtGui +from tank_vendor.flow_integration_sdk.exceptions import FlowError +from tank_vendor.flow_integration_sdk.globals import FOLDER_TYPE_ID +from tank_vendor.flow_integration_sdk.objects import FlowAsset, FlowProject +from tank_vendor.flow_integration_sdk.schema import get_schema_id +from .create import PIPELINE_STEP_TYPE from .shared_cache import MedmSharedCache from .utils import is_structural_asset as _is_structural_asset_util -# Import types for type hints only - actual objects come from framework at runtime -# Framework is loaded dynamically via sgtk.platform.import_framework() -if TYPE_CHECKING: - from adsk.flow.data import Asset -else: - Asset = Any - class MedmEntityModel(QtGui.QStandardItemModel): """ @@ -90,9 +88,6 @@ def __init__( super().__init__(parent) self._app = sgtk.platform.current_bundle() - self._flow_module = sgtk.platform.import_framework( - "tk-framework-flowam", "flow" - ) self._cache = cache if cache is not None else MedmSharedCache() self._folder_icon = QtGui.QIcon(QtGui.QPixmap(":/res/icon_Folder.png")) @@ -209,9 +204,9 @@ def search_item(parent): return search_item(None) - def get_cached_children(self, asset: Asset) -> List[Asset]: + def get_cached_children(self, asset: FlowAsset) -> List[FlowAsset]: """ - Return child :class:`Asset` objects for *asset*. + Return child :class:`FlowAsset` objects for *asset*. Uses the internal cache when available; otherwise fetches from the FlowAM API and stores the result. This is the single entry-point that both @@ -219,7 +214,7 @@ def get_cached_children(self, asset: Asset) -> List[Asset]: that a drill-down never fetches the same level twice. :param asset: Parent FlowAM Asset whose children are needed. - :returns: List of child Asset objects (may be empty). + :returns: List of child FlowAsset objects (may be empty). """ return self._fetch_and_cache_children(asset) @@ -233,8 +228,8 @@ def _initialize_project(self) -> None: Called during __init__ to fail fast if project is unavailable. """ try: - session_project = self._flow_module.data.get_session_project() - self._project = self._flow_module.data.Project(session_project.id) + current_engine = sgtk.platform.current_engine() + self._project = FlowProject(current_engine.context.flow_project_id) self._app.log_debug( f"FlowAM Entity: Initialized project '{self._project.name}'" ) @@ -266,16 +261,14 @@ def _get_structural_type_ids(self) -> set: return self._structural_type_ids try: - folder_id = self._flow_module.data.FOLDER_TYPE_ID - pipeline_step_id = self._flow_module.schema.get_schema_id( - self._flow_module.asset_management.PIPELINE_STEP_TYPE - ) + folder_id = FOLDER_TYPE_ID + pipeline_step_id = get_schema_id(PIPELINE_STEP_TYPE) self._structural_type_ids = {folder_id, pipeline_step_id} self._app.log_debug( f"FlowAM Entity: structural type IDs = {self._structural_type_ids}" ) - except self._flow_module.FlowError as e: + except FlowError as e: self._app.log_warning( f"FlowAM Entity: could not resolve structural type IDs ({e}); " "non-structural assets without structural descendants will be hidden." @@ -284,7 +277,7 @@ def _get_structural_type_ids(self) -> set: return self._structural_type_ids - def _is_tree_node(self, asset: Asset) -> bool: + def _is_tree_node(self, asset: FlowAsset) -> bool: """ Return ``True`` when *asset* should appear as a node in the left-hand tree view. @@ -313,7 +306,7 @@ def _is_tree_node(self, asset: Asset) -> bool: children = self._fetch_and_cache_children(asset) return len(children) > 0 - def _icon_for_asset(self, asset: Asset) -> QtGui.QIcon: + def _icon_for_asset(self, asset: FlowAsset) -> QtGui.QIcon: """ Return the appropriate tree icon for *asset* based on its type. @@ -366,7 +359,7 @@ def _load_medm_assets(self) -> None: self.data_refresh_fail.emit(str(e)) def _add_asset_item( - self, asset: Asset, parent_item: Optional[QtGui.QStandardItem] + self, asset: FlowAsset, parent_item: Optional[QtGui.QStandardItem] ) -> QtGui.QStandardItem: """ Create a single ``QStandardItem`` for *asset* and append it to the tree. @@ -436,7 +429,7 @@ def _load_children_for_item(self, item: QtGui.QStandardItem) -> None: f"FlowAM: Could not get children for '{asset.name}': {e}" ) - def _fetch_and_cache_children(self, asset: Asset) -> List[Asset]: + def _fetch_and_cache_children(self, asset: FlowAsset) -> List[FlowAsset]: """ Return child assets for *asset*, fetching from the API only on the first call and caching the result in the shared cache for subsequent diff --git a/python/tk_multi_loader/medm/latestpublish_model.py b/python/tk_multi_loader/medm/latestpublish_model.py index c6dda102..d0a8e0d8 100644 --- a/python/tk_multi_loader/medm/latestpublish_model.py +++ b/python/tk_multi_loader/medm/latestpublish_model.py @@ -17,23 +17,23 @@ from __future__ import annotations from collections import defaultdict -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import Any, Dict, List, Optional import sgtk from sgtk.platform.qt import QtCore, QtGui +from tank_vendor.flow_integration_sdk.objects import FlowAsset +from tank_vendor.flow_integration_sdk.sandbox import ( + DraftInfo, + get_asset_drafts, + get_drafts, +) +from ..constants import DRAFT_VERSION_IDENTIFIER from .shared_cache import MedmSharedCache from .thumbnail_service import MedmThumbnailService -from ..constants import DRAFT_VERSION_IDENTIFIER -from .utils import build_draft_sg_dict, resolve_publish_type +from .utils import build_draft_sg_dict from .utils import is_structural_asset as _is_structural_asset_util - -if TYPE_CHECKING: - from flow.data import Asset - from flow.sandbox import DraftInfo -else: - Asset = Any - DraftInfo = Any +from .utils import resolve_publish_type class MedmLatestPublishModel(QtGui.QStandardItemModel): @@ -101,9 +101,6 @@ def __init__( super().__init__(parent) self._app = sgtk.platform.current_bundle() - self._flow_module = sgtk.platform.import_framework( - "tk-framework-flowam", "flow" - ) self._publish_type_model = publish_type_model self._bg_task_manager = bg_task_manager @@ -240,9 +237,7 @@ def _populate_model_from_selected_item( if child_asset.id in self._cache.drafts: raw_drafts = self._cache.drafts[child_asset.id] else: - raw_drafts = self._flow_module.asset_management.get_asset_drafts( - child_asset.id - ) + raw_drafts = get_asset_drafts(child_asset.id) self._cache.drafts[child_asset.id] = raw_drafts except Exception as e: self._app.log_debug( @@ -309,7 +304,7 @@ def _populate_model_from_selected_item( def _extract_asset_from_tree_item( self, item: QtGui.QStandardItem - ) -> Optional[Asset]: + ) -> Optional[FlowAsset]: """ Extract the FlowAM Asset object from a tree view QStandardItem. @@ -324,7 +319,7 @@ def _extract_asset_from_tree_item( asset_data = item.data(QtCore.Qt.UserRole + 1) return asset_data - def _fetch_asset_children(self, asset: Asset) -> List[Dict[str, Any]]: + def _fetch_asset_children(self, asset: FlowAsset) -> List[Dict[str, Any]]: """ Fetch all non-structural child assets and convert to sg_data dicts. @@ -375,7 +370,7 @@ def _fetch_asset_children(self, asset: Asset) -> List[Dict[str, Any]]: ) return children_asset_sg_dicts - def _asset_to_sg_dict(self, asset: Asset) -> Dict[str, Any]: + def _asset_to_sg_dict(self, asset: FlowAsset) -> Dict[str, Any]: """ Convert an FlowAM Asset to a Shotgun-compatible dictionary. @@ -433,7 +428,7 @@ def _asset_to_sg_dict(self, asset: Asset) -> Dict[str, Any]: return sg_dict def _draft_to_sg_dict( - self, draft_info: DraftInfo, asset: Optional[Asset] = None + self, draft_info: DraftInfo, asset: Optional[FlowAsset] = None ) -> Dict[str, Any]: """ Convert a local DraftInfo into a Shotgun-compatible dictionary suitable @@ -442,9 +437,9 @@ def _draft_to_sg_dict( Key conventions that V1 action hooks rely on: - ``version_number == DRAFT_VERSION_IDENTIFIER (-1)`` - identifies a local draft - ``sg_flow_revision_id`` - the draft's sandbox ID (draft_info.draft_id), used - by asset_management.open_draft() and sandbox.is_local_draft() + by open_draft() and is_local_draft() - :param draft_info: DraftInfo returned by asset_management.get_asset_drafts() + :param draft_info: DraftInfo returned by get_asset_drafts() (CheckoutDraftInfo) or get_drafts() (NewDraftInfo). :param asset: The FlowAM Asset the draft belongs to. May be ``None`` for ``NewDraftInfo`` entries whose parent asset has not been published yet. @@ -518,9 +513,7 @@ def _fetch_new_draft_items_for_parent( """ if self._NEW_DRAFTS_CACHE_KEY not in self._cache.drafts: try: - all_new_drafts = self._flow_module.asset_management.get_drafts( - draft_type="new" - ) + all_new_drafts = get_drafts(draft_type="new") except Exception as e: self._app.log_warning(f"FlowAM: Could not fetch new-asset drafts: {e}") all_new_drafts = [] diff --git a/python/tk_multi_loader/medm/publishhistory_model.py b/python/tk_multi_loader/medm/publishhistory_model.py index 8ca56d83..3f79217f 100644 --- a/python/tk_multi_loader/medm/publishhistory_model.py +++ b/python/tk_multi_loader/medm/publishhistory_model.py @@ -21,27 +21,15 @@ import sgtk from sgtk.platform.qt import QtCore, QtGui +from tank_vendor.flow_data_sdk.base.model import AssetVersion +from tank_vendor.flow_integration_sdk.objects import FlowAsset +from tank_vendor.flow_integration_sdk.sandbox import DraftInfo, get_asset_drafts from .. import utils from .shared_cache import MedmSharedCache from .thumbnail_service import MedmThumbnailService from .utils import build_draft_sg_dict, resolve_publish_type -if TYPE_CHECKING: - from adsk.flow.am import ( - Asset, - AssetRevision, - AssetVersion, - ) - from flow.sandbox import CheckoutDraftInfo, DraftInfo, NewDraftInfo -else: - Asset = Any - AssetRevision = Any - AssetVersion = Any - CheckoutDraftInfo = Any - DraftInfo = Any - NewDraftInfo = Any - class MedmPublishHistoryModel(QtGui.QStandardItemModel): """ @@ -92,9 +80,6 @@ def __init__( super().__init__(parent) self._app = sgtk.platform.current_bundle() - self._flow_module = sgtk.platform.import_framework( - "tk-framework-flowam", "flow" - ) self._bg_task_manager = bg_task_manager self._cache = cache if cache is not None else MedmSharedCache() @@ -171,9 +156,7 @@ def load_data(self, sg_data: Dict[str, Any]) -> None: if asset_id in self._cache.drafts: drafts = self._cache.drafts[asset_id] else: - drafts = self._flow_module.asset_management.get_asset_drafts( - asset_id - ) + drafts = get_asset_drafts(asset_id) self._cache.drafts[asset_id] = drafts for draft_info in drafts: self._add_draft_as_qt_item(draft_info, medm_asset) @@ -227,7 +210,7 @@ def _initialize_project_info(self) -> None: ) def _add_version_as_qt_item( - self, asset_version: AssetVersion, asset: Asset + self, asset_version: AssetVersion, asset: FlowAsset ) -> None: """ Convert an AssetVersion to a QStandardItem and add it to the history model. @@ -265,7 +248,7 @@ def get_sg_data(): self._app.log_debug(f"FlowAM History: Added version v{version_number}") def _version_to_sg_dict( - self, version: AssetVersion, asset: Asset + self, version: AssetVersion, asset: FlowAsset ) -> Dict[str, Any]: """ Convert a FlowAM AssetVersion to Shotgun-compatible dictionary. @@ -326,7 +309,7 @@ def _version_to_sg_dict( return sg_dict def _draft_to_sg_dict( - self, draft_info: DraftInfo, asset: Optional[Asset] + self, draft_info: DraftInfo, asset: Optional[FlowAsset] ) -> Dict[str, Any]: """ Convert a DraftInfo (CheckoutDraftInfo or NewDraftInfo) to a Shotgun-compatible @@ -337,7 +320,7 @@ def _draft_to_sg_dict( - ``sg_flow_revision_id`` -> the draft's unique sandbox ID (draft_info.draft_id), used by asset_management.open_draft() and sandbox.is_local_draft() - :param draft_info: DraftInfo object returned by asset_management.get_asset_drafts() + :param draft_info: DraftInfo object returned by get_asset_drafts() or get_drafts(). May be CheckoutDraftInfo or NewDraftInfo. :param asset: The parent FlowAM Asset (may be None for NewDraftInfo) :returns: sg_data dictionary compatible with action hooks and Shotgun UI @@ -370,7 +353,7 @@ def _draft_to_sg_dict( return sg_dict def _add_draft_as_qt_item( - self, draft_info: DraftInfo, asset: Optional[Asset] + self, draft_info: DraftInfo, asset: Optional[FlowAsset] ) -> None: """ Convert a DraftInfo to a QStandardItem and insert it at the top of From c1df4e36875b0273e16db519486296ef909f61fa Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Tue, 16 Jun 2026 11:08:40 -0500 Subject: [PATCH 06/37] Migrate thumbnail utilities --- python/tk_multi_loader/medm/thumbnail_service.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/python/tk_multi_loader/medm/thumbnail_service.py b/python/tk_multi_loader/medm/thumbnail_service.py index 3c69f441..f6abb3a5 100644 --- a/python/tk_multi_loader/medm/thumbnail_service.py +++ b/python/tk_multi_loader/medm/thumbnail_service.py @@ -29,6 +29,7 @@ import sgtk from sgtk.platform.qt import QtCore, QtGui +from tank_vendor.flow_integration_sdk.objects import FlowRevision if TYPE_CHECKING: from .shared_cache import MedmSharedCache @@ -71,9 +72,6 @@ def __init__( super().__init__(parent) self._app = sgtk.platform.current_bundle() - self._flow_module = sgtk.platform.import_framework( - "tk-framework-flowam", "flow" - ) # Both dicts are references into the shared cache - not owned here. self._url_cache: Dict[str, Optional[str]] = cache.thumbnail_urls @@ -151,7 +149,8 @@ def _resolve_and_fetch( url = self._url_cache.get(revision_id) if url is None and revision_id not in self._url_cache: try: - url = self._flow_module.asset_management.get_thumbnail_url(revision_id) + rev = FlowRevision.get_revision(revision_id) + url = rev.get_thumbnail_url() except Exception as exc: self._app.log_debug( f"FlowAM ThumbnailService: URL resolve failed for {revision_id}: {exc}" From 1657118b5b5fc9d368822cd9642b5ae53b649967 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Tue, 16 Jun 2026 11:55:16 -0500 Subject: [PATCH 07/37] Use more sandbox functions --- python/tk_multi_loader/medm/flowam_actions.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/python/tk_multi_loader/medm/flowam_actions.py b/python/tk_multi_loader/medm/flowam_actions.py index b9239a5c..86d4be3c 100644 --- a/python/tk_multi_loader/medm/flowam_actions.py +++ b/python/tk_multi_loader/medm/flowam_actions.py @@ -21,6 +21,8 @@ is_local_draft, is_new_asset, read_draft_info, + checkout_revision, + discard_draft, ) from ..build_asset_dialog import BuildAssetDialog @@ -93,7 +95,7 @@ def _do_open(self, sg_publish_data: dict) -> None: flow_module.asset_management.open_draft(flow_revision_id) elif version_number > DRAFT_VERSION_IDENTIFIER: # Checkout the revision to the local sandbox - flow_module.asset_management.checkout_revision(flow_revision_id) + checkout_revision(flow_revision_id) else: raise TankError( f"Cannot open item {sg_publish_data['name']} with version number {version_number}. " @@ -235,7 +237,6 @@ def _discard_draft(self, sg_publish_data: dict) -> None: :param sg_publish_data: FPTR data dictionary with all the standard entity fields. """ - flow_module = self.load_framework("tk-framework-flowam", "flow") parent_window = self._get_dialog_parent() draft_folder = get_draft_folder(sg_publish_data.get("sg_flow_revision_id")) @@ -268,7 +269,7 @@ def _discard_draft(self, sg_publish_data: dict) -> None: ) if message_response == QtGui.QMessageBox.StandardButton.Yes: - flow_module.asset_management.discard_draft( + discard_draft( sg_publish_data.get("sg_flow_revision_id") ) From 5952719dd9a823053ad79a3dd3f417586df86a98 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Tue, 16 Jun 2026 12:20:17 -0500 Subject: [PATCH 08/37] Finish migrating framework-floam references --- info.yml | 5 +- python/tk_multi_loader/medm/file.py | 180 ++++++++++++++++++ python/tk_multi_loader/medm/flowam_actions.py | 15 +- python/tk_multi_loader/medm/reference.py | 136 +++++++++++++ 4 files changed, 323 insertions(+), 13 deletions(-) create mode 100644 python/tk_multi_loader/medm/file.py create mode 100644 python/tk_multi_loader/medm/reference.py diff --git a/info.yml b/info.yml index 6e530f5d..4634cb89 100644 --- a/info.yml +++ b/info.yml @@ -38,8 +38,7 @@ configuration: default_value: false description: Set to True to use Flow Asset Management data instead of Shotgun data. When enabled, the loader fetches publish data from the Flow Asset - Management system. Requires tk-framework-flowam to be configured in the - environment. + Management system. # hooks actions_hook: @@ -207,5 +206,3 @@ documentation_url: "https://help.autodesk.com/view/SGDEV/ENU/?guid=SG_Supervisor frameworks: - {"name": "tk-framework-shotgunutils", "version": "v5.x.x", "minimum_version": "v5.8.6"} - {"name": "tk-framework-qtwidgets", "version": "v2.x.x", "minimum_version": "v2.10.6"} - # TODO: Remove the following line after SG-43459. - - {"name": "tk-framework-flowam", "version": "v1.x.x"} diff --git a/python/tk_multi_loader/medm/file.py b/python/tk_multi_loader/medm/file.py new file mode 100644 index 00000000..3c0a637e --- /dev/null +++ b/python/tk_multi_loader/medm/file.py @@ -0,0 +1,180 @@ +# Copyright (c) 2026 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. + +from __future__ import annotations # needed for Houdini support + +import os + +import sgtk +from tank_vendor.flow_integration_sdk.exceptions import FlowError +from tank_vendor.flow_integration_sdk.globals import FILE_SEQ_TYPE, SOURCE_PURPOSE +from tank_vendor.flow_integration_sdk.objects import FlowAssetRevision +from tank_vendor.flow_integration_sdk.sandbox import is_local_draft, read_draft_info +from tank_vendor.flow_integration_sdk.schema import get_schema_id + +from .utils import cleanpath + + +class DownloadRevisionError(FlowError): + def __init__( + self, + *args, + revision_id: str, + directory: str = "", + **kwargs, + ): + message = "Download revision failed." + super().__init__(message, *args, **kwargs) + self.revision_id = revision_id + self.directory = directory + + +class InvalidDraftError(FlowError): + def __init__(self, *args, draft_id: str, **kwargs): + """ + Args: + draft_id: Id that uniquely identifies a draft in local sandbox. + """ + message = f'Draft id "{draft_id}" is invalid.' + super().__init__(message, *args, **kwargs) + self.draft_id = draft_id + + +def download_revision( + revision_id: str, + component_purpose: str = SOURCE_PURPOSE, + directory: str = "", +) -> dict[int, str]: + """Download the requested component of the given revision + to the location specified. If no directory is provided, + a file dialog will be launched to allow the user to choose a location. + + Args: + revision_id: Id of AssetRevision to be downloaded. + component_purpose: Purpose of binary component on revision to be + downloaded. By default, the source component + will be used. + directory: An explicitly folder location to download to. + It will be created if it doesn't exist. + If not provided, the user can choose via a file dialog. + + Returns: + Dictionary of blob index to full path of downloaded file. + + Raises: + DownloadRevisionError + """ + + engine = sgtk.platform.current_engine() + + # Ensure revision id is valid + try: + revision = FlowAssetRevision.get_revision(revision_id) + except FlowError as exc: + msg = f"Invalid revision id provided: {revision_id}" + raise DownloadRevisionError( + revision_id=revision_id, + directory=directory, + details=msg, + ) from exc + + # Ensure component exists + component = revision.find_component(purpose=component_purpose) + if not component: + msg = f'Component of purpose "{component_purpose}" does not exist on revision.' + raise DownloadRevisionError( + revision_id=revision_id, + directory=directory, + details=msg, + ) + + # Ensure directory exists + if directory: + directory = cleanpath(directory) + if os.path.isfile(directory): + # Name collision between directory and existing file + # NOTE: OS will not allow a directory to be created with same name + # in this case. + msg = "A file already exists with same name of download directory." + msg += " Please choose a new input directory." + raise DownloadRevisionError( + revision_id=revision_id, + directory=directory, + details=msg, + ) + if not os.path.exists(directory): + try: + os.makedirs(directory, exist_ok=True) + except Exception as exc: # pylint: disable=broad-except + msg = f"Could not create download directory: {directory}" + raise DownloadRevisionError( + revision_id=revision_id, + directory=directory, + details=msg, + ) from exc + + elif engine.flow_host: + result = engine.flow_host.file_dialog( + title="Choose Download Location", + folder_mode=True, # select directory + ) + if not result: + engine.log_warning("Download operation cancelled.") + return {} + directory = cleanpath(result[0]) + else: + msg = "No download location provided." + raise DownloadRevisionError( + revision_id=revision_id, + directory=directory, + details=msg, + ) + + # Determine if revision contains a file sequence + file_seq_comp = revision.find_component(type_id=get_schema_id(FILE_SEQ_TYPE)) + result = component.download(directory, file_sequence=file_seq_comp is not None) + + msg = f'Download complete for "{revision.name}" - "{component.name}"!\n' + msg += "The following files were downloaded:\n" + for i, file_path in result.items(): + msg += f"\tBlob {i} -> {file_path}\n" + engine.log_info(msg) + + return result + + +def open_draft(draft_id: str): + """Open draft source file for editing if draft is local. + + Args: + draft_id: Unique id that identifies a draft location in local sandbox. + + Raises: + FlowError + InvalidDraftError + """ + engine = sgtk.platform.current_engine() + + if not engine.flow_host: + raise FlowError("Opening a draft must be done in a host FlowContext.") + + if not is_local_draft(draft_id): + msg = f'The draft "{draft_id}" is not in local sandbox.' + raise InvalidDraftError(draft_id=draft_id, details=msg) + + draft_info = read_draft_info(draft_id) + draft_path = draft_info.source_path + if not os.path.exists(draft_path): + msg = f'Corrupted draft folder. The file "{draft_path}" does not exist.' + raise InvalidDraftError(draft_id=draft_id, details=msg) + + # Open file + engine.log_info(f"Opening file: {draft_path}") + engine.flow_host.open_file(draft_path) diff --git a/python/tk_multi_loader/medm/flowam_actions.py b/python/tk_multi_loader/medm/flowam_actions.py index 86d4be3c..a5af4a16 100644 --- a/python/tk_multi_loader/medm/flowam_actions.py +++ b/python/tk_multi_loader/medm/flowam_actions.py @@ -36,6 +36,8 @@ create_dcc_workfile, create_template_workfile, ) +from .file import open_draft, download_revision +from .reference import reference_revision, copy_reference_link class FlowAMActions: @@ -87,12 +89,10 @@ def _do_open(self, sg_publish_data: dict) -> None: ) raise TankError("No Revision ID found for this item {}.".format(item_id)) - flow_module = self.load_framework("tk-framework-flowam", "flow") - if version_number == DRAFT_VERSION_IDENTIFIER and is_local_draft( sg_publish_data.get("sg_flow_revision_id") ): - flow_module.asset_management.open_draft(flow_revision_id) + open_draft(flow_revision_id) elif version_number > DRAFT_VERSION_IDENTIFIER: # Checkout the revision to the local sandbox checkout_revision(flow_revision_id) @@ -116,8 +116,7 @@ def _create_reference_am(self, sg_publish_data: dict) -> None: ) raise TankError("No Revision ID found for this item {}.".format(item_id)) - flow_module = self.load_framework("tk-framework-flowam", "flow") - flow_module.asset_management.reference_revision(flow_revision_id) + reference_revision(flow_revision_id) def _create_reference_copy_link(self, sg_publish_data: dict) -> None: """ @@ -133,8 +132,7 @@ def _create_reference_copy_link(self, sg_publish_data: dict) -> None: ) raise TankError("No Revision ID found for this item {}.".format(item_id)) - flow_module = self.load_framework("tk-framework-flowam", "flow") - path = flow_module.asset_management.copy_reference_link(flow_revision_id) + path = copy_reference_link(flow_revision_id) self._app.log_info(f"Reference path copied: {path}") @@ -430,8 +428,7 @@ def _download_asset_revision(self, sg_publish_data: dict) -> None: ) raise TankError("No Revision ID found for this item {}.".format(item_id)) - flow_module = self.load_framework("tk-framework-flowam", "flow") - result = flow_module.asset_management.download_revision(flow_revision_id) + result = download_revision(flow_revision_id) # Notify the user about the download result if result: diff --git a/python/tk_multi_loader/medm/reference.py b/python/tk_multi_loader/medm/reference.py new file mode 100644 index 00000000..397d2f70 --- /dev/null +++ b/python/tk_multi_loader/medm/reference.py @@ -0,0 +1,136 @@ +# Copyright (c) 2026 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. + +from __future__ import annotations # needed for Houdini support + +import os + +import sgtk +from tank_vendor.flow_integration_sdk.exceptions import FlowError +from tank_vendor.flow_integration_sdk.objects import FlowAssetRevision, FlowAssetVersion + + +class CreateReferenceError(FlowError): + def __init__(self, *args, input_id: str = "", file_path: str = "", **kwargs): + """ + Args: + input_id: Id of revision or version being referenced. + file_path: File being referenced. + """ + if input_id: + message = f"Could not create reference to {input_id}." + elif file_path: + message = f"Could not create reference of file: {file_path}." + else: + message = "Could not create reference." + super().__init__(message, *args, **kwargs) + self.input_id = input_id + self.file_path = file_path + + +def reference_revision(revision_id: str) -> str: + """Reference the source component of the given revision into the current scene. + + Args: + revision_id: The id of the asset revision to be referenced. + This can also be a version id. + + Returns: + File path of referenced file. + + Raises: + CreateReferenceError + """ + engine = sgtk.platform.current_engine() + + if not hasattr(engine.flow_host, "create_reference"): + msg = "Referencing is not supported in current execution FlowContext." + raise CreateReferenceError(input_id=revision_id, details=msg) + + # We will disallow referencing into a non-asset scene + if engine.context.flow_draft_id is None: + msg = "Please open an asset from the loader before doing a reference operation." + raise CreateReferenceError(input_id=revision_id, details=msg) + + try: + if FlowAssetVersion.is_version_id(revision_id): + input_type = "version" + revision = FlowAssetVersion(revision_id).revision + else: + input_type = "revision" + revision = FlowAssetRevision.get_revision(revision_id) + except FlowError as exc: + msg = f"Could not retrieve {input_type} object." + raise CreateReferenceError(input_id=revision_id, details=msg) from exc + + # Fetch source component of revision + revision.fetch() + + # Get path to source path of revision in local storage + file_path = revision.get_storage_source_path() + if file_path is None: + msg = "Revision does not have a source component to be referenced." + raise CreateReferenceError(input_id=revision_id, details=msg) + if not os.path.exists(file_path): + msg = f"Source file does not exist in storage: {file_path}. " + msg += "Fetching the revision was not successful!" + raise CreateReferenceError(input_id=revision_id, details=msg) + + # Create reference + depdata = engine.flow_host.create_reference(file_path, namespace=revision.name) + return depdata.file_path + + +def copy_reference_link(revision_id: str) -> str: + """Copy the reference link (file path) to the source component + the of given revision to application clipboard. + + Args: + revision_id: The id of the AssetRevision to be referenced. + This can also be a version id. + + Returns: + File path copied to clipboard. + + Raises: + FlowError + CreateReferenceError + """ + engine = sgtk.platform.current_engine() + + if engine.flow_host is None: + raise FlowError("Not running in a supported host FlowContext.") + + try: + if FlowAssetVersion.is_version_id(revision_id): + input_type = "version" + revision = FlowAssetVersion(revision_id).revision + else: + input_type = "revision" + revision = FlowAssetRevision.get_revision(revision_id) + except FlowError as exc: + msg = f"Could not retrieve {input_type} object." + raise CreateReferenceError(input_id=revision_id, details=msg) from exc + + # Fetch source component of revision + revision.fetch() + + # Get path to source path of revision in local storage + file_path = revision.get_storage_source_path() + if file_path is None: + msg = "Revision does not have a source component to be referenced." + raise CreateReferenceError(input_id=revision_id, details=msg) + if not os.path.exists(file_path): + msg = f"Source file does not exist in storage: {file_path}" + raise CreateReferenceError(input_id=revision_id, details=msg) + + # Copy to clipboard + engine.flow_host.copy_to_clipboard(file_path) + return file_path From 77efc5e32ce6112b6c9325d3c9bffe550678f53e Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Tue, 16 Jun 2026 12:37:17 -0500 Subject: [PATCH 09/37] Update references from sgtk.flowam.create --- python/tk_multi_loader/medm/create.py | 169 +++----------------------- 1 file changed, 14 insertions(+), 155 deletions(-) diff --git a/python/tk_multi_loader/medm/create.py b/python/tk_multi_loader/medm/create.py index d687fdcb..35723375 100644 --- a/python/tk_multi_loader/medm/create.py +++ b/python/tk_multi_loader/medm/create.py @@ -23,8 +23,17 @@ from typing import Callable import sgtk +from sgtk.flowam.create import ( + ASSET_FOLDER, + ASSET_TYPE, + GENERIC_FOLDER, + PIPELINE_STEP_TYPE, + SHOT_FOLDER, + SHOT_TYPE, + create_asset_hierarchy, +) from sgtk.flowam.utils import BaseInputs, create_components_for_publish -from tank_vendor.flow_integration_sdk.exceptions import FlowError +from tank_vendor.flow_integration_sdk.exceptions import CreateAssetError, FlowError from tank_vendor.flow_integration_sdk.globals import FOLDER_TYPE_ID from tank_vendor.flow_integration_sdk.objects import FlowAsset, FlowProject from tank_vendor.flow_integration_sdk.publish import publish_new_asset @@ -37,22 +46,14 @@ from .utils import cleanpath, fileext -# --------------------------------- -# CONSTANTS -# --------------------------------- +# ------------------------------------------- +# CONSTANTS not defined in sgtk.flowam.create +# ------------------------------------------- # Folder names -ASSET_FOLDER = "Assets" -SHOT_FOLDER = "Shots" -GENERIC_FOLDER = "Generic" TEMPLATE_FOLDER = "Templates" -# SG entity types -SHOT_TYPE = "Shot" -ASSET_TYPE = "Asset" - # Schema types CONTAINER_TYPE = "type.container" -PIPELINE_STEP_TYPE = "type.pipelineStep" TEMPLATE_TYPE = "type.template" @@ -68,12 +69,6 @@ class CreateMode(Enum): GENERIC = "generic" #: Create a generic asset from a specified source file. -class CreateAssetError(FlowError): - def __init__(self, *args, **kwargs): - message = "Could not create asset." - super().__init__(message, *args, **kwargs) - - @dataclass class CreateInputs(BaseInputs): """Convenience structure to hold create inputs and allow them to be @@ -246,7 +241,7 @@ def create_dcc_workfile(inputs: CreateInputs) -> NewDraftInfo: raise CreateAssetError(data=inputs.asdict(), details=msg) # Create any necessary hierarchy above current asset - parent = _create_asset_hierarchy(inputs) + parent = create_asset_hierarchy(inputs) # Create the workfile asset in sandbox draft_id = _create_dcc_workfile_asset(parent, inputs) @@ -330,72 +325,6 @@ def _has_workfile_type(parent: FlowAsset, type_id: str) -> bool: return False -def _create_asset_hierarchy(inputs: CreateInputs) -> FlowAsset: - """Called when creating an asset for an sg entity for the first time. - This function will ensure that any hierarchical structuring above the workfile asset - is created if necessary. (These will be committed directly to remote immediately.) - - High-level structure of SG-mirrored project in AM - ------------------------------------------------- - - - PROJECT - - SHOTS FOLDER - - sg shot 1 - - pipeline step 1 - - task 1 folder - - sg shot 1 (workfile) - - task 2 folder - - sg shot 1 (workfile) - - pipeline step 2 - - task 1 folder - - sg shot 1 (workfile) - - task 2 folder - - sg shot 1 (workfile) - ... - - sg shot 2 - ... - ... - - ASSETS FOLDER - - sg asset 1 - - pipeline step 1 - - task 1 folder - - sg asset 1 (workfile) - - task 2 folder - - sg asset 1 (workfile) - - pipeline step 2 - - task 1 folder - - sg asset 1 (workfile) - - task 2 folder - - sg asset 1 (workfile) - ... - - sg asset 2 - ... - ... - - GENERIC FOLDER - - generic asset 1 (workfile) - - generic asset 2 (workfile) - ... - - Args: - See CreateInputs documentation. - - Returns: - The parent asset of the workfile asset to be created. - """ - - root_folder = _get_or_create_root_folder(inputs) - - # Create or retrieve parent asset - if inputs.sg_entity_name: - parent = _get_or_create_workfile_parent(root_folder, inputs) - else: - # If no sg entity context was provided, folder will be parent - # (applicable to generic assets) - parent = root_folder - - return parent - - def _get_or_create_root_folder(inputs: CreateInputs) -> FlowAsset: """Retrieve top-level folder pertinent to new asset. If it doesn't exist, create it. @@ -467,76 +396,6 @@ def _get_or_create_root_folder(inputs: CreateInputs) -> FlowAsset: return folder -def _get_or_create_workfile_parent( - root_folder: FlowAsset, inputs: CreateInputs -) -> FlowAsset: - """Determine the appropriate parent asset of the workfile to be created. - This should be a pipeline step asset since we will only call this function - if a sg context was provided. - - Any necessary hierarchical assets that don't exist will be created - (i.e. containers and pipeline steps). - - Returns: - The parent asset object. - """ - app = sgtk.platform.current_bundle() - - sg_entity_type = inputs.sg_entity_type - sg_entity_name = inputs.sg_entity_name - sg_pipeline_step = inputs.sg_pipeline_step - sg_task_name = inputs.sg_task_name - - # If a container asset associated with sg entity doesn't exist, create it - container = root_folder.find_child(sg_entity_name) - if not container: - app.log_info( - f'Creating container asset for "{sg_entity_name}" under folder "{root_folder.name}"...' - ) - container_type = ( - "type.container.asset" - if sg_entity_type == ASSET_TYPE - else "type.container.shot" - ) - container_type_id = get_schema_id(container_type) - container = publish_new_asset( - name=sg_entity_name, - parent=root_folder, - components=create_components_for_publish( - type_ids=[container_type_id], - ), - ) - - # If a pipeline step asset associated with sg pipeline step doesn't exist, create it - pipeline_step = container.find_child(sg_pipeline_step) - if not pipeline_step: - app.log_info(f'Creating pipeline step asset for "{sg_pipeline_step}"...') - pipeline_step_type_id = get_schema_id(PIPELINE_STEP_TYPE) - pipeline_step = publish_new_asset( - name=sg_pipeline_step, - parent=container, - components=create_components_for_publish( - type_ids=[pipeline_step_type_id], - ), - ) - - # If a task folder associated with sg task doesn't exist, create it - task_folder = pipeline_step.find_child(sg_task_name) - if not task_folder: - app.log_info(f'Creating task folder asset for "{sg_task_name}"...') - desc = f'Folder for task "{sg_task_name}".' - task_folder = publish_new_asset( - name=sg_task_name, - parent=pipeline_step, - components=create_components_for_publish( - type_ids=[FOLDER_TYPE_ID], - ), - description=desc, - ) - - return task_folder - - def _create_dcc_workfile_asset(parent: FlowAsset, inputs: CreateInputs) -> str: """Called when creating a new dcc workfile asset. This function will create the workfile asset in sandbox under the given parent. From cb88ca187e9a1c67f92bee00b76878349a420724 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Tue, 16 Jun 2026 12:39:10 -0500 Subject: [PATCH 10/37] Remove framework loading --- python/tk_multi_loader/medm/flowam_actions.py | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/python/tk_multi_loader/medm/flowam_actions.py b/python/tk_multi_loader/medm/flowam_actions.py index a5af4a16..a40e6ca3 100644 --- a/python/tk_multi_loader/medm/flowam_actions.py +++ b/python/tk_multi_loader/medm/flowam_actions.py @@ -57,23 +57,6 @@ def DRAFT_VERSION_IDENTIFIER(self): """ return DRAFT_VERSION_IDENTIFIER - def load_framework( - self, framework_instance_name: str, module_name: str - ) -> ModuleType: - """ - Simple wrapper around the base class implementation to - provide user feedback if the framework cannot be loaded. - - :param framework_instance_name: Name of the framework instance to load - :returns: sgtk.platform.Framework instance - """ - try: - return sgtk.platform.import_framework(framework_instance_name, module_name) - except Exception as e: - message = f"Could not load the required framework '{framework_instance_name}'.\n\nError details: {e}" - self._app.log_error(message) - QtGui.QMessageBox.critical(None, "Error", message) - def _do_open(self, sg_publish_data: dict) -> None: """ Open the given PublishedFile. @@ -204,7 +187,7 @@ def _on_build_scene_dialog_accepted( try: draft_info = create_dcc_workfile(create_inputs) self._app.log_debug( - f"Created a DCC workfile on Flow AM framework with the draft_id: {draft_info.draft_id}" + f"Created a DCC workfile with the draft_id: {draft_info.draft_id}" ) except CreateAssetError as exc: self._app.log_error(f"Create asset failed: {exc}\nInput data: {exc.data}") @@ -360,7 +343,7 @@ def _on_build_template_dialog_accepted( ) draft_info = create_template_workfile(create_inputs) self._app.log_debug( - f"Created a Template workfile on Flow AM framework with the draft_id: {draft_info.draft_id}" + f"Created a Template workfile with the draft_id: {draft_info.draft_id}" ) def _get_flowam_id(self) -> str: From 74da4d5e3636bbfcc331dc3e0d7a2578d8a9feb7 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Tue, 16 Jun 2026 12:43:56 -0500 Subject: [PATCH 11/37] Rename fucntion to get FlowAMActions instance --- python/tk_multi_loader/api/manager.py | 10 +++++----- python/tk_multi_loader/dialog.py | 2 +- python/tk_multi_loader/loader_action_manager.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/python/tk_multi_loader/api/manager.py b/python/tk_multi_loader/api/manager.py index 32bf1237..86cc1c36 100644 --- a/python/tk_multi_loader/api/manager.py +++ b/python/tk_multi_loader/api/manager.py @@ -130,7 +130,7 @@ def get_actions_for_publish(self, sg_data, ui_area): sg_publish_data=sg_data, actions=actions, ui_area=ui_area_str, - am_base_obj=self.get_am_base_obj(), + am_base_obj=self.get_flowam_actions_instance(), ) except Exception: self._logger.exception("Could not execute generate_actions hook.") @@ -244,7 +244,7 @@ def execute_action(self, sg_data, action): name=action["name"], params=action["params"], sg_publish_data=sg_data, - am_base_obj=self.get_am_base_obj(), + am_base_obj=self.get_flowam_actions_instance(), ) except Exception as e: self._logger.exception( @@ -268,7 +268,7 @@ def execute_multiple_actions(self, actions): "actions_hook", "execute_multiple_actions", actions=actions, - am_base_obj=self.get_am_base_obj(), + am_base_obj=self.get_flowam_actions_instance(), ) except Exception as e: self._logger.exception( @@ -310,7 +310,7 @@ def get_actions_for_entity(self, sg_data): sg_publish_data=sg_data, actions=actions, ui_area="main", - am_base_obj=self.get_am_base_obj(), + am_base_obj=self.get_flowam_actions_instance(), ) # folder options only found in main ui area except Exception: self._logger.exception("Could not execute generate_actions hook.") @@ -351,7 +351,7 @@ def _fix_timestamp(sg_data): ) sg_data["created_at"] = sg_timestamp - def get_am_base_obj(self) -> "FlowAMActions | None": + def get_flowam_actions_instance(self) -> "FlowAMActions | None": """ """ if sgtk.platform.current_bundle().get_setting("enable_flowam", False): from ..medm import FlowAMActions diff --git a/python/tk_multi_loader/dialog.py b/python/tk_multi_loader/dialog.py index cabb8d39..ed98eb43 100644 --- a/python/tk_multi_loader/dialog.py +++ b/python/tk_multi_loader/dialog.py @@ -1994,7 +1994,7 @@ def on_action_click(act): name=act["name"], params=act["params"], sg_publish_data=sg_data, - am_base_obj=self._action_manager.get_am_base_obj(), + am_base_obj=self._action_manager.get_flowam_actions_instance(), ) action = QtGui.QAction(entity_action["caption"], view) diff --git a/python/tk_multi_loader/loader_action_manager.py b/python/tk_multi_loader/loader_action_manager.py index 03b2d5e4..8dcf8422 100644 --- a/python/tk_multi_loader/loader_action_manager.py +++ b/python/tk_multi_loader/loader_action_manager.py @@ -213,7 +213,7 @@ def get_actions_for_folder(self, sg_data): def get_actions_for_entity(self, sg_data): return self._loader_manager.get_actions_for_entity(sg_data) - def get_am_base_obj(self) -> "FlowAMActions | None": + def get_flowam_actions_instance(self) -> "FlowAMActions | None": """ Returns the base object for asset management actions, if available. @@ -222,7 +222,7 @@ def get_am_base_obj(self) -> "FlowAMActions | None": :returns: The base object for asset management actions, or None if not available. """ - return self._loader_manager.get_am_base_obj() + return self._loader_manager.get_flowam_actions_instance() ######################################################################################## # callbacks From e3c5ba5aa98fcda4e0c9611bf964be43b3d893fd Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Tue, 16 Jun 2026 12:48:15 -0500 Subject: [PATCH 12/37] Rename `am_base_obj` to `flowam_actions` --- hooks/tk-desktop_actions.py | 16 +++++------ hooks/tk-houdini_actions.py | 34 +++++++++++------------ hooks/tk-maya_actions.py | 40 +++++++++++++-------------- hooks/tk-nuke_actions.py | 34 +++++++++++------------ python/tk_multi_loader/api/manager.py | 8 +++--- python/tk_multi_loader/dialog.py | 2 +- 6 files changed, 67 insertions(+), 67 deletions(-) diff --git a/hooks/tk-desktop_actions.py b/hooks/tk-desktop_actions.py index 12cdabec..dcda64c3 100644 --- a/hooks/tk-desktop_actions.py +++ b/hooks/tk-desktop_actions.py @@ -79,8 +79,8 @@ def generate_actions( enable_flowam = app.get_setting("enable_flowam", False) if enable_flowam: - am_base_obj = kwargs.get("am_base_obj") - if not am_base_obj: + flowam_actions = kwargs.get("flowam_actions") + if not flowam_actions: raise Exception( "FlowAM is enabled but no Asset Management base object was passed to the action hook. " "FlowAM specific actions will not be generated." @@ -91,7 +91,7 @@ def generate_actions( if ( version_number is not None - and version_number != am_base_obj.DRAFT_VERSION_IDENTIFIER + and version_number != flowam_actions.DRAFT_VERSION_IDENTIFIER ): action_instances.append( { @@ -130,9 +130,9 @@ def generate_actions( if ( "reference_copy_link" in actions and sg_publish_data.get( - "version_number", am_base_obj.DRAFT_VERSION_IDENTIFIER + "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) - > am_base_obj.DRAFT_VERSION_IDENTIFIER + > flowam_actions.DRAFT_VERSION_IDENTIFIER ): action_instances.append( { @@ -202,7 +202,7 @@ def execute_action( "Execute action called for action %s. " "Parameters: %s. Publish Data: %s" % (name, params, sg_publish_data) ) - am_base_obj = kwargs.get("am_base_obj") + flowam_actions = kwargs.get("flowam_actions") if name == "create_generic_asset": # Right click a task the left panel @@ -213,10 +213,10 @@ def execute_action( self._launch_publisher(name, sg_publish_data) elif name == "reference_copy_link": - am_base_obj._create_reference_copy_link(sg_publish_data) + flowam_actions._create_reference_copy_link(sg_publish_data) elif name == "download": - am_base_obj._download_asset_revision(sg_publish_data) + flowam_actions._download_asset_revision(sg_publish_data) def _launch_publisher(self, action_name: str, sg_publish_data: dict) -> None: """ diff --git a/hooks/tk-houdini_actions.py b/hooks/tk-houdini_actions.py index 0d9ec7ac..a299f09a 100644 --- a/hooks/tk-houdini_actions.py +++ b/hooks/tk-houdini_actions.py @@ -104,8 +104,8 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): # ----------------------- enable_flowam = app.get_setting("enable_flowam", False) if enable_flowam: - am_base_obj = kwargs.get("am_base_obj") - if not am_base_obj: + flowam_actions = kwargs.get("flowam_actions") + if not flowam_actions: raise Exception( "FlowAM is enabled but no Asset Management base object was passed to the action hook. " "FlowAM specific actions will not be generated." @@ -113,11 +113,11 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): if "open" in actions and sg_publish_data.get("type") == "PublishedFile": if ( - am_base_obj._is_local_draft(sg_publish_data) + flowam_actions._is_local_draft(sg_publish_data) or sg_publish_data.get( - "version_number", am_base_obj.DRAFT_VERSION_IDENTIFIER + "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) - > am_base_obj.DRAFT_VERSION_IDENTIFIER + > flowam_actions.DRAFT_VERSION_IDENTIFIER ): action_instances.append( { @@ -134,7 +134,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): if ( version_number is not None - and version_number != am_base_obj.DRAFT_VERSION_IDENTIFIER + and version_number != flowam_actions.DRAFT_VERSION_IDENTIFIER ): action_instances.append( { @@ -148,9 +148,9 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): if "discard_draft" in actions: draft_id = sg_publish_data.get("sg_flow_revision_id") - if am_base_obj._is_local_draft( + if flowam_actions._is_local_draft( sg_publish_data - ) and am_base_obj._is_new_asset(draft_id): + ) and flowam_actions._is_new_asset(draft_id): action_instances.append( { "name": "discard_draft", @@ -163,9 +163,9 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): if ( "reference_copy_link" in actions and sg_publish_data.get( - "version_number", am_base_obj.DRAFT_VERSION_IDENTIFIER + "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) - != am_base_obj.DRAFT_VERSION_IDENTIFIER + != flowam_actions.DRAFT_VERSION_IDENTIFIER ): action_instances.append( { @@ -251,25 +251,25 @@ def execute_action(self, name, params, sg_publish_data, **kwargs): # ----------------------- enable_flowam = app.get_setting("enable_flowam", False) if enable_flowam: - am_base_obj = kwargs.get("am_base_obj") + flowam_actions = kwargs.get("flowam_actions") if name == "open": - am_base_obj._do_open(sg_publish_data) + flowam_actions._do_open(sg_publish_data) if name == "reference_copy_link": - am_base_obj._create_reference_copy_link(sg_publish_data) + flowam_actions._create_reference_copy_link(sg_publish_data) if name == "discard_draft": - am_base_obj._discard_draft(sg_publish_data) + flowam_actions._discard_draft(sg_publish_data) if name == "build_new_scene": - am_base_obj._build_new_scene(sg_publish_data) + flowam_actions._build_new_scene(sg_publish_data) if name == "build_new_template": - am_base_obj._build_new_template(sg_publish_data) + flowam_actions._build_new_template(sg_publish_data) if name == "download": - am_base_obj._download_asset_revision(sg_publish_data) + flowam_actions._download_asset_revision(sg_publish_data) return diff --git a/hooks/tk-maya_actions.py b/hooks/tk-maya_actions.py index c7d9446b..ab4b6c45 100644 --- a/hooks/tk-maya_actions.py +++ b/hooks/tk-maya_actions.py @@ -131,8 +131,8 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): # ----------------------- enable_flowam = app.get_setting("enable_flowam", False) if enable_flowam: - am_base_obj = kwargs.get("am_base_obj") - if not am_base_obj: + flowam_actions = kwargs.get("flowam_actions") + if not flowam_actions: raise Exception( "FlowAM is enabled but no Asset Management base object was passed to the action hook. " "FlowAM specific actions will not be generated." @@ -142,11 +142,11 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): "open" in actions and sg_publish_data.get("type") == "PublishedFile" and ( - am_base_obj._is_local_draft(sg_publish_data) + flowam_actions._is_local_draft(sg_publish_data) or sg_publish_data.get( - "version_number", am_base_obj.DRAFT_VERSION_IDENTIFIER + "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) - > am_base_obj.DRAFT_VERSION_IDENTIFIER + > flowam_actions.DRAFT_VERSION_IDENTIFIER ) ): action_instances.append( @@ -165,7 +165,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): and ( sg_publish_data.get("version_number") is not None and sg_publish_data.get("version_number") - != am_base_obj.DRAFT_VERSION_IDENTIFIER + != flowam_actions.DRAFT_VERSION_IDENTIFIER ) ): action_instances.append( @@ -180,9 +180,9 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): if "discard_draft" in actions: draft_id = sg_publish_data.get("sg_flow_revision_id") - if am_base_obj._is_local_draft( + if flowam_actions._is_local_draft( sg_publish_data - ) and am_base_obj._is_new_asset(draft_id): + ) and flowam_actions._is_new_asset(draft_id): action_instances.append( { "name": "discard_draft", @@ -195,9 +195,9 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): if ( "reference_am" in actions and sg_publish_data.get( - "version_number", am_base_obj.DRAFT_VERSION_IDENTIFIER + "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) - != am_base_obj.DRAFT_VERSION_IDENTIFIER + != flowam_actions.DRAFT_VERSION_IDENTIFIER ): action_instances.append( { @@ -211,9 +211,9 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): if ( "reference_copy_link" in actions and sg_publish_data.get( - "version_number", am_base_obj.DRAFT_VERSION_IDENTIFIER + "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) - != am_base_obj.DRAFT_VERSION_IDENTIFIER + != flowam_actions.DRAFT_VERSION_IDENTIFIER ): action_instances.append( { @@ -299,28 +299,28 @@ def execute_action(self, name, params, sg_publish_data, **kwargs): # ----------------------- enable_flowam = app.get_setting("enable_flowam", False) if enable_flowam: - am_base_obj = kwargs.get("am_base_obj") + flowam_actions = kwargs.get("flowam_actions") if name == "reference_am": - am_base_obj._create_reference_am(sg_publish_data) + flowam_actions._create_reference_am(sg_publish_data) if name == "reference_copy_link": - am_base_obj._create_reference_copy_link(sg_publish_data) + flowam_actions._create_reference_copy_link(sg_publish_data) if name == "open": - am_base_obj._do_open(sg_publish_data) + flowam_actions._do_open(sg_publish_data) if name == "discard_draft": - am_base_obj._discard_draft(sg_publish_data) + flowam_actions._discard_draft(sg_publish_data) if name == "build_new_scene": - am_base_obj._build_new_scene(sg_publish_data) + flowam_actions._build_new_scene(sg_publish_data) if name == "build_new_template": - am_base_obj._build_new_template(sg_publish_data) + flowam_actions._build_new_template(sg_publish_data) if name == "download": - am_base_obj._download_asset_revision(sg_publish_data) + flowam_actions._download_asset_revision(sg_publish_data) return diff --git a/hooks/tk-nuke_actions.py b/hooks/tk-nuke_actions.py index b3d078b9..566d06c9 100644 --- a/hooks/tk-nuke_actions.py +++ b/hooks/tk-nuke_actions.py @@ -117,8 +117,8 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): # ----------------------- enable_flowam = app.get_setting("enable_flowam", False) if enable_flowam: - am_base_obj = kwargs.get("am_base_obj") - if not am_base_obj: + flowam_actions = kwargs.get("flowam_actions") + if not flowam_actions: raise Exception( "FlowAM is enabled but no Asset Management base object was passed to the action hook. " "FlowAM specific actions will not be generated." @@ -147,11 +147,11 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): # 1. Local drafts (version_number == -1 and is_local_draft) # 2. Published revisions (version_number > -1) if ( - am_base_obj._is_local_draft(sg_publish_data) + flowam_actions._is_local_draft(sg_publish_data) or sg_publish_data.get( - "version_number", am_base_obj.DRAFT_VERSION_IDENTIFIER + "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) - > am_base_obj.DRAFT_VERSION_IDENTIFIER + > flowam_actions.DRAFT_VERSION_IDENTIFIER ): action_instances.append( { @@ -162,7 +162,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): } ) - if "discard_draft" in actions and am_base_obj._is_local_draft( + if "discard_draft" in actions and flowam_actions._is_local_draft( sg_publish_data ): action_instances.append( @@ -176,9 +176,9 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): if ( "reference_copy_link" in actions and sg_publish_data.get( - "version_number", am_base_obj.DRAFT_VERSION_IDENTIFIER + "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) - != am_base_obj.DRAFT_VERSION_IDENTIFIER + != flowam_actions.DRAFT_VERSION_IDENTIFIER ): action_instances.append( { @@ -192,9 +192,9 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): if ( "create_read_node" in actions and sg_publish_data.get( - "version_number", am_base_obj.DRAFT_VERSION_IDENTIFIER + "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) - != am_base_obj.DRAFT_VERSION_IDENTIFIER + != flowam_actions.DRAFT_VERSION_IDENTIFIER ): action_instances.append( { @@ -260,25 +260,25 @@ def execute_action(self, name, params, sg_publish_data, **kwargs): # ----------------------- use_medm_data = app.get_setting("use_medm_data", False) if use_medm_data: - am_base_obj = kwargs.get("am_base_obj") + flowam_actions = kwargs.get("flowam_actions") if name == "build_new_script": - am_base_obj._build_new_scene(sg_publish_data) + flowam_actions._build_new_scene(sg_publish_data) if name == "build_new_template": - am_base_obj._build_new_template(sg_publish_data) + flowam_actions._build_new_template(sg_publish_data) if name == "open": - am_base_obj._do_open(sg_publish_data) + flowam_actions._do_open(sg_publish_data) if name == "discard_draft": - am_base_obj._discard_draft(sg_publish_data) + flowam_actions._discard_draft(sg_publish_data) if name == "reference_copy_link": - am_base_obj._create_reference_copy_link(sg_publish_data) + flowam_actions._create_reference_copy_link(sg_publish_data) if name == "create_read_node": - am_base_obj._create_reference(sg_publish_data) + flowam_actions._create_reference(sg_publish_data) return diff --git a/python/tk_multi_loader/api/manager.py b/python/tk_multi_loader/api/manager.py index 86cc1c36..59f2d1df 100644 --- a/python/tk_multi_loader/api/manager.py +++ b/python/tk_multi_loader/api/manager.py @@ -130,7 +130,7 @@ def get_actions_for_publish(self, sg_data, ui_area): sg_publish_data=sg_data, actions=actions, ui_area=ui_area_str, - am_base_obj=self.get_flowam_actions_instance(), + flowam_actions=self.get_flowam_actions_instance(), ) except Exception: self._logger.exception("Could not execute generate_actions hook.") @@ -244,7 +244,7 @@ def execute_action(self, sg_data, action): name=action["name"], params=action["params"], sg_publish_data=sg_data, - am_base_obj=self.get_flowam_actions_instance(), + flowam_actions=self.get_flowam_actions_instance(), ) except Exception as e: self._logger.exception( @@ -268,7 +268,7 @@ def execute_multiple_actions(self, actions): "actions_hook", "execute_multiple_actions", actions=actions, - am_base_obj=self.get_flowam_actions_instance(), + flowam_actions=self.get_flowam_actions_instance(), ) except Exception as e: self._logger.exception( @@ -310,7 +310,7 @@ def get_actions_for_entity(self, sg_data): sg_publish_data=sg_data, actions=actions, ui_area="main", - am_base_obj=self.get_flowam_actions_instance(), + flowam_actions=self.get_flowam_actions_instance(), ) # folder options only found in main ui area except Exception: self._logger.exception("Could not execute generate_actions hook.") diff --git a/python/tk_multi_loader/dialog.py b/python/tk_multi_loader/dialog.py index ed98eb43..069850cc 100644 --- a/python/tk_multi_loader/dialog.py +++ b/python/tk_multi_loader/dialog.py @@ -1994,7 +1994,7 @@ def on_action_click(act): name=act["name"], params=act["params"], sg_publish_data=sg_data, - am_base_obj=self._action_manager.get_flowam_actions_instance(), + flowam_actions=self._action_manager.get_flowam_actions_instance(), ) action = QtGui.QAction(entity_action["caption"], view) From fc27088e3ac50af8846bfc3ed28406d4121db185 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Tue, 16 Jun 2026 16:10:00 -0500 Subject: [PATCH 13/37] Latest Updates --- python/tk_multi_loader/constants.py | 7 +++++ python/tk_multi_loader/medm/create.py | 11 +------- python/tk_multi_loader/medm/entity_model.py | 8 +++--- python/tk_multi_loader/medm/file.py | 6 ++--- python/tk_multi_loader/medm/flowam_actions.py | 10 +++---- .../medm/latestpublish_model.py | 20 +++++++------- .../medm/publishhistory_model.py | 27 +++++++++---------- python/tk_multi_loader/medm/reference.py | 16 +++++------ python/tk_multi_loader/medm/shared_cache.py | 4 +-- .../tk_multi_loader/medm/template_queries.py | 3 ++- .../tk_multi_loader/medm/thumbnail_service.py | 2 +- python/tk_multi_loader/medm/utils.py | 4 +-- 12 files changed, 56 insertions(+), 62 deletions(-) diff --git a/python/tk_multi_loader/constants.py b/python/tk_multi_loader/constants.py index ca94e8ee..917b6c76 100644 --- a/python/tk_multi_loader/constants.py +++ b/python/tk_multi_loader/constants.py @@ -103,3 +103,10 @@ # FlowAM versions starts with 0 and FlowPT versions starts with 1. # This is used to identify a FlowAM draft version in the UI. DRAFT_VERSION_IDENTIFIER = -1 + +# Folder names +TEMPLATE_FOLDER = "Templates" + +# Schema types +CONTAINER_TYPE = "type.container" +TEMPLATE_TYPE = "type.template" diff --git a/python/tk_multi_loader/medm/create.py b/python/tk_multi_loader/medm/create.py index 35723375..b203e033 100644 --- a/python/tk_multi_loader/medm/create.py +++ b/python/tk_multi_loader/medm/create.py @@ -45,16 +45,7 @@ from tank_vendor.flow_integration_sdk.schema import get_schema_id from .utils import cleanpath, fileext - -# ------------------------------------------- -# CONSTANTS not defined in sgtk.flowam.create -# ------------------------------------------- -# Folder names -TEMPLATE_FOLDER = "Templates" - -# Schema types -CONTAINER_TYPE = "type.container" -TEMPLATE_TYPE = "type.template" +from ..constants import TEMPLATE_TYPE, TEMPLATE_FOLDER # --------------------------------- diff --git a/python/tk_multi_loader/medm/entity_model.py b/python/tk_multi_loader/medm/entity_model.py index 51f2d1a5..ff1b1736 100644 --- a/python/tk_multi_loader/medm/entity_model.py +++ b/python/tk_multi_loader/medm/entity_model.py @@ -23,7 +23,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, List, Optional +from typing import Optional import sgtk from sgtk.platform.qt import QtCore, QtGui @@ -31,8 +31,8 @@ from tank_vendor.flow_integration_sdk.globals import FOLDER_TYPE_ID from tank_vendor.flow_integration_sdk.objects import FlowAsset, FlowProject from tank_vendor.flow_integration_sdk.schema import get_schema_id +from sgtk.flowam.create import PIPELINE_STEP_TYPE -from .create import PIPELINE_STEP_TYPE from .shared_cache import MedmSharedCache from .utils import is_structural_asset as _is_structural_asset_util @@ -204,7 +204,7 @@ def search_item(parent): return search_item(None) - def get_cached_children(self, asset: FlowAsset) -> List[FlowAsset]: + def get_cached_children(self, asset: FlowAsset) -> list[FlowAsset]: """ Return child :class:`FlowAsset` objects for *asset*. @@ -429,7 +429,7 @@ def _load_children_for_item(self, item: QtGui.QStandardItem) -> None: f"FlowAM: Could not get children for '{asset.name}': {e}" ) - def _fetch_and_cache_children(self, asset: FlowAsset) -> List[FlowAsset]: + def _fetch_and_cache_children(self, asset: FlowAsset) -> list[FlowAsset]: """ Return child assets for *asset*, fetching from the API only on the first call and caching the result in the shared cache for subsequent diff --git a/python/tk_multi_loader/medm/file.py b/python/tk_multi_loader/medm/file.py index 3c0a637e..292cea0a 100644 --- a/python/tk_multi_loader/medm/file.py +++ b/python/tk_multi_loader/medm/file.py @@ -15,7 +15,7 @@ import sgtk from tank_vendor.flow_integration_sdk.exceptions import FlowError from tank_vendor.flow_integration_sdk.globals import FILE_SEQ_TYPE, SOURCE_PURPOSE -from tank_vendor.flow_integration_sdk.objects import FlowAssetRevision +from tank_vendor.flow_integration_sdk.objects import FlowRevision from tank_vendor.flow_integration_sdk.sandbox import is_local_draft, read_draft_info from tank_vendor.flow_integration_sdk.schema import get_schema_id @@ -57,7 +57,7 @@ def download_revision( a file dialog will be launched to allow the user to choose a location. Args: - revision_id: Id of AssetRevision to be downloaded. + revision_id: Id of FlowRevision to be downloaded. component_purpose: Purpose of binary component on revision to be downloaded. By default, the source component will be used. @@ -76,7 +76,7 @@ def download_revision( # Ensure revision id is valid try: - revision = FlowAssetRevision.get_revision(revision_id) + revision = FlowRevision.get_revision(revision_id) except FlowError as exc: msg = f"Invalid revision id provided: {revision_id}" raise DownloadRevisionError( diff --git a/python/tk_multi_loader/medm/flowam_actions.py b/python/tk_multi_loader/medm/flowam_actions.py index a40e6ca3..66d67408 100644 --- a/python/tk_multi_loader/medm/flowam_actions.py +++ b/python/tk_multi_loader/medm/flowam_actions.py @@ -10,8 +10,6 @@ from __future__ import annotations import functools -from types import ModuleType -from typing import Any import sgtk from sgtk import TankError @@ -146,7 +144,7 @@ def _build_new_scene(self, sg_publish_data: dict) -> None: build_scene_dialog.exec_() def _on_build_scene_dialog_accepted( - self, dialog: Any, sg_publish_data: dict + self, dialog: BuildAssetDialog, sg_publish_data: dict ) -> None: if not dialog.build: message = "Not enough data from the build dialog." @@ -250,9 +248,7 @@ def _discard_draft(self, sg_publish_data: dict) -> None: ) if message_response == QtGui.QMessageBox.StandardButton.Yes: - discard_draft( - sg_publish_data.get("sg_flow_revision_id") - ) + discard_draft(sg_publish_data.get("sg_flow_revision_id")) QtGui.QMessageBox.information( parent_window, @@ -318,7 +314,7 @@ def _build_new_template(self, sg_publish_data: dict) -> None: build_template_dialog.exec_() def _on_build_template_dialog_accepted( - self, dialog: Any, sg_publish_data: dict + self, dialog: BuildTemplateDialog, sg_publish_data: dict ) -> None: if not dialog.mode: message = "Not enough data from the build dialog." diff --git a/python/tk_multi_loader/medm/latestpublish_model.py b/python/tk_multi_loader/medm/latestpublish_model.py index d0a8e0d8..dbca907b 100644 --- a/python/tk_multi_loader/medm/latestpublish_model.py +++ b/python/tk_multi_loader/medm/latestpublish_model.py @@ -17,7 +17,7 @@ from __future__ import annotations from collections import defaultdict -from typing import Any, Dict, List, Optional +from typing import Any, Optional import sgtk from sgtk.platform.qt import QtCore, QtGui @@ -231,7 +231,7 @@ def _populate_model_from_selected_item( assets_for_draft_lookup = [leaf_asset_fallback] # --- Pass 1: collect draft cards per asset ---------------------------- - drafts_by_asset_id: Dict[str, list] = {} + drafts_by_asset_id: dict[str, list] = {} for child_asset in assets_for_draft_lookup: try: if child_asset.id in self._cache.drafts: @@ -319,7 +319,7 @@ def _extract_asset_from_tree_item( asset_data = item.data(QtCore.Qt.UserRole + 1) return asset_data - def _fetch_asset_children(self, asset: FlowAsset) -> List[Dict[str, Any]]: + def _fetch_asset_children(self, asset: FlowAsset) -> list[dict[str, Any]]: """ Fetch all non-structural child assets and convert to sg_data dicts. @@ -370,7 +370,7 @@ def _fetch_asset_children(self, asset: FlowAsset) -> List[Dict[str, Any]]: ) return children_asset_sg_dicts - def _asset_to_sg_dict(self, asset: FlowAsset) -> Dict[str, Any]: + def _asset_to_sg_dict(self, asset: FlowAsset) -> dict[str, Any]: """ Convert an FlowAM Asset to a Shotgun-compatible dictionary. @@ -429,7 +429,7 @@ def _asset_to_sg_dict(self, asset: FlowAsset) -> Dict[str, Any]: def _draft_to_sg_dict( self, draft_info: DraftInfo, asset: Optional[FlowAsset] = None - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """ Convert a local DraftInfo into a Shotgun-compatible dictionary suitable for display as a center-panel card. @@ -493,7 +493,7 @@ def _draft_to_sg_dict( def _fetch_new_draft_items_for_parent( self, parent_asset_id: str - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: """ Return sg_dict items for ``NewDraftInfo`` entries whose ``parent_id`` matches *parent_asset_id*. @@ -542,7 +542,7 @@ def _fetch_new_draft_items_for_parent( def _resolve_publish_type(self, medm_type_id_str: str) -> tuple: return resolve_publish_type(medm_type_id_str, self._cache, self._app) - def _add_sg_dict_as_qt_item(self, sg_item: Dict[str, Any]) -> None: + def _add_sg_dict_as_qt_item(self, sg_item: dict[str, Any]) -> None: """ Create a QStandardItem from a Shotgun-compatible dict and add it to the model. @@ -593,7 +593,7 @@ def get_sg_data(): f"FlowAM: Added item '{qt_item.text()}' to model (row count: {self.rowCount()})" ) - def _set_tooltip(self, item: QtGui.QStandardItem, sg_item: Dict[str, Any]) -> None: + def _set_tooltip(self, item: QtGui.QStandardItem, sg_item: dict[str, Any]) -> None: """ Sets a tooltip for a publish item. @@ -630,7 +630,7 @@ def _resolve_and_download_thumbnail( on the main thread once the image bytes are available. :param qt_item: The QStandardItem to set the thumbnail on. - :param revision_id: FlowAM AssetRevision ID whose thumbnail is needed. + :param revision_id: FlowAM FlowRevision ID whose thumbnail is needed. """ self._thumbnail_service.request(qt_item, revision_id, self._apply_thumbnail) @@ -649,7 +649,7 @@ def _apply_thumbnail(self, qt_item: QtGui.QStandardItem, image_data: bytes) -> N ) qt_item.setIcon(QtGui.QIcon(scaled)) - def _calculate_sg_publish_type_counts(self) -> Dict[int, int]: + def _calculate_sg_publish_type_counts(self) -> dict[int, int]: """ Count how many items in the model have each Shotgun PublishedFileType. diff --git a/python/tk_multi_loader/medm/publishhistory_model.py b/python/tk_multi_loader/medm/publishhistory_model.py index 3f79217f..16095511 100644 --- a/python/tk_multi_loader/medm/publishhistory_model.py +++ b/python/tk_multi_loader/medm/publishhistory_model.py @@ -17,12 +17,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import Any, Optional import sgtk from sgtk.platform.qt import QtCore, QtGui -from tank_vendor.flow_data_sdk.base.model import AssetVersion -from tank_vendor.flow_integration_sdk.objects import FlowAsset +from tank_vendor.flow_integration_sdk.objects import FlowAsset, FlowVersion from tank_vendor.flow_integration_sdk.sandbox import DraftInfo, get_asset_drafts from .. import utils @@ -48,7 +47,7 @@ class MedmPublishHistoryModel(QtGui.QStandardItemModel): ASSET_ROLE = ( QtCore.Qt.UserRole + 200 ) # Stores FlowAM Asset object (shared with all FlowAM models) - VERSION_ROLE = QtCore.Qt.UserRole + 201 # Stores FlowAM AssetVersion object + VERSION_ROLE = QtCore.Qt.UserRole + 201 # Stores FlowAM FlowVersion object DRAFT_ROLE = QtCore.Qt.UserRole + 202 # Stores DraftInfo for draft rows # Signals for compatibility with ShotgunModelOverlayWidget @@ -105,7 +104,7 @@ def destroy(self) -> None: if self._owns_thumbnail_service: self._thumbnail_service.destroy() - def load_data(self, sg_data: Dict[str, Any]) -> None: + def load_data(self, sg_data: dict[str, Any]) -> None: """ Load and display all versions (version history) for the selected asset. @@ -210,12 +209,12 @@ def _initialize_project_info(self) -> None: ) def _add_version_as_qt_item( - self, asset_version: AssetVersion, asset: FlowAsset + self, asset_version: FlowVersion, asset: FlowAsset ) -> None: """ - Convert an AssetVersion to a QStandardItem and add it to the history model. + Convert an FlowVersion to a QStandardItem and add it to the history model. - :param asset_version: The FlowAM AssetVersion to add + :param asset_version: The FlowAM FlowVersion to add :param asset: The parent FlowAM Asset """ version_number = asset_version.version_number @@ -248,12 +247,12 @@ def get_sg_data(): self._app.log_debug(f"FlowAM History: Added version v{version_number}") def _version_to_sg_dict( - self, version: AssetVersion, asset: FlowAsset - ) -> Dict[str, Any]: + self, version: FlowVersion, asset: FlowAsset + ) -> dict[str, Any]: """ - Convert a FlowAM AssetVersion to Shotgun-compatible dictionary. + Convert a FlowAM FlowVersion to Shotgun-compatible dictionary. - :param version: The FlowAM AssetVersion + :param version: The FlowAM FlowVersion :param asset: The parent FlowAM Asset :returns: sg_data dictionary compatible with Shotgun UI """ @@ -310,7 +309,7 @@ def _version_to_sg_dict( def _draft_to_sg_dict( self, draft_info: DraftInfo, asset: Optional[FlowAsset] - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """ Convert a DraftInfo (CheckoutDraftInfo or NewDraftInfo) to a Shotgun-compatible dictionary for display in the version history list as a local draft entry. @@ -413,7 +412,7 @@ def _resolve_and_download_thumbnail( on the main thread once the image bytes are available. :param qt_item: The QStandardItem to set the thumbnail on. - :param revision_id: FlowAM AssetRevision ID whose thumbnail is needed. + :param revision_id: FlowAM FlowRevision ID whose thumbnail is needed. """ self._thumbnail_service.request(qt_item, revision_id, self._apply_thumbnail) diff --git a/python/tk_multi_loader/medm/reference.py b/python/tk_multi_loader/medm/reference.py index 397d2f70..f4aa6573 100644 --- a/python/tk_multi_loader/medm/reference.py +++ b/python/tk_multi_loader/medm/reference.py @@ -14,7 +14,7 @@ import sgtk from tank_vendor.flow_integration_sdk.exceptions import FlowError -from tank_vendor.flow_integration_sdk.objects import FlowAssetRevision, FlowAssetVersion +from tank_vendor.flow_integration_sdk.objects import FlowVersion, FlowRevision class CreateReferenceError(FlowError): @@ -60,12 +60,12 @@ def reference_revision(revision_id: str) -> str: raise CreateReferenceError(input_id=revision_id, details=msg) try: - if FlowAssetVersion.is_version_id(revision_id): + if FlowVersion.is_version_id(revision_id): input_type = "version" - revision = FlowAssetVersion(revision_id).revision + revision = FlowVersion(revision_id).revision else: input_type = "revision" - revision = FlowAssetRevision.get_revision(revision_id) + revision = FlowRevision.get_revision(revision_id) except FlowError as exc: msg = f"Could not retrieve {input_type} object." raise CreateReferenceError(input_id=revision_id, details=msg) from exc @@ -93,7 +93,7 @@ def copy_reference_link(revision_id: str) -> str: the of given revision to application clipboard. Args: - revision_id: The id of the AssetRevision to be referenced. + revision_id: The id of the FlowRevision to be referenced. This can also be a version id. Returns: @@ -109,12 +109,12 @@ def copy_reference_link(revision_id: str) -> str: raise FlowError("Not running in a supported host FlowContext.") try: - if FlowAssetVersion.is_version_id(revision_id): + if FlowVersion.is_version_id(revision_id): input_type = "version" - revision = FlowAssetVersion(revision_id).revision + revision = FlowVersion(revision_id).revision else: input_type = "revision" - revision = FlowAssetRevision.get_revision(revision_id) + revision = FlowRevision.get_revision(revision_id) except FlowError as exc: msg = f"Could not retrieve {input_type} object." raise CreateReferenceError(input_id=revision_id, details=msg) from exc diff --git a/python/tk_multi_loader/medm/shared_cache.py b/python/tk_multi_loader/medm/shared_cache.py index 08c22a0c..9a0d4b97 100644 --- a/python/tk_multi_loader/medm/shared_cache.py +++ b/python/tk_multi_loader/medm/shared_cache.py @@ -49,7 +49,7 @@ class MedmSharedCache: so local-draft state is always up-to-date. ``versions`` - ``asset.id → list[AssetVersion]``. Cleared on refresh; a publish + ``asset.id → list[FlowVersion]``. Cleared on refresh; a publish action could add a new version. ``publish_types`` @@ -68,7 +68,7 @@ class MedmSharedCache: children: Dict[str, List[Any]] = dataclasses.field(default_factory=dict) # asset.id → list[DraftInfo] drafts: Dict[str, List[Any]] = dataclasses.field(default_factory=dict) - # asset.id → list[AssetVersion] + # asset.id → list[FlowVersion] versions: Dict[str, List[Any]] = dataclasses.field(default_factory=dict) # medm_type_id_str → (sg_publish_type_id, display_name) publish_types: Dict[str, tuple] = dataclasses.field(default_factory=dict) diff --git a/python/tk_multi_loader/medm/template_queries.py b/python/tk_multi_loader/medm/template_queries.py index bb90a1c8..9ccf7273 100644 --- a/python/tk_multi_loader/medm/template_queries.py +++ b/python/tk_multi_loader/medm/template_queries.py @@ -17,8 +17,9 @@ import sgtk from tank_vendor.flow_integration_sdk.objects import FlowAsset, FlowProject from tank_vendor.flow_integration_sdk.schema import get_schema_id +from sgtk.flowam.create import PIPELINE_STEP_TYPE -from .create import PIPELINE_STEP_TYPE, TEMPLATE_FOLDER, TEMPLATE_TYPE +from ..constants import TEMPLATE_TYPE, TEMPLATE_FOLDER logger = sgtk.platform.get_logger(__name__) diff --git a/python/tk_multi_loader/medm/thumbnail_service.py b/python/tk_multi_loader/medm/thumbnail_service.py index f6abb3a5..3ebc8dd6 100644 --- a/python/tk_multi_loader/medm/thumbnail_service.py +++ b/python/tk_multi_loader/medm/thumbnail_service.py @@ -99,7 +99,7 @@ def request( Otherwise a daemon thread resolves the URL and downloads the image. :param qt_item: ``QStandardItem`` whose icon should be updated. - :param revision_id: FlowAM ``AssetRevision`` ID to look up. + :param revision_id: FlowAM ``FlowRevision`` ID to look up. :param callback: ``callable(qt_item, image_data: bytes)`` that will be invoked on the **main thread** once the image bytes are available. The callback is responsible for converting bytes to a ``QPixmap`` diff --git a/python/tk_multi_loader/medm/utils.py b/python/tk_multi_loader/medm/utils.py index bb723149..e4f069a6 100644 --- a/python/tk_multi_loader/medm/utils.py +++ b/python/tk_multi_loader/medm/utils.py @@ -24,9 +24,9 @@ get_schema_display_name, get_schema_id, ) +from sgtk.flowam.create import PIPELINE_STEP_TYPE -from ..constants import DRAFT_VERSION_IDENTIFIER -from .create import CONTAINER_TYPE, PIPELINE_STEP_TYPE +from ..constants import DRAFT_VERSION_IDENTIFIER, CONTAINER_TYPE def is_structural_asset(asset: Any) -> bool: From 4a3f93681dd3aacd6fc1c95c1c85622e9c07d200 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Tue, 16 Jun 2026 17:38:24 -0500 Subject: [PATCH 14/37] Finish clean-up --- python/tk_multi_loader/medm/create.py | 4 ++-- python/tk_multi_loader/medm/file.py | 2 +- python/tk_multi_loader/medm/reference.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/python/tk_multi_loader/medm/create.py b/python/tk_multi_loader/medm/create.py index b203e033..62f0b604 100644 --- a/python/tk_multi_loader/medm/create.py +++ b/python/tk_multi_loader/medm/create.py @@ -228,7 +228,7 @@ def create_dcc_workfile(inputs: CreateInputs) -> NewDraftInfo: inputs.validate() if not in_dcc_context(): - msg = "Cannot create DCC workfile without being in DCC FlowContext." + msg = "Cannot create DCC workfile without being in DCC." raise CreateAssetError(data=inputs.asdict(), details=msg) # Create any necessary hierarchy above current asset @@ -271,7 +271,7 @@ def create_template_workfile(inputs: CreateTemplateInputs) -> NewDraftInfo: inputs.validate() if not in_dcc_context(): - msg = "Cannot create template workfile without being in DCC FlowContext." + msg = "Cannot create template workfile without being in DCC." raise CreateAssetError(data=inputs.asdict(), details=msg) # Create any necessary hierarchy above current asset diff --git a/python/tk_multi_loader/medm/file.py b/python/tk_multi_loader/medm/file.py index 292cea0a..4278d731 100644 --- a/python/tk_multi_loader/medm/file.py +++ b/python/tk_multi_loader/medm/file.py @@ -163,7 +163,7 @@ def open_draft(draft_id: str): engine = sgtk.platform.current_engine() if not engine.flow_host: - raise FlowError("Opening a draft must be done in a host FlowContext.") + raise FlowError("Opening a draft must be done in a host.") if not is_local_draft(draft_id): msg = f'The draft "{draft_id}" is not in local sandbox.' diff --git a/python/tk_multi_loader/medm/reference.py b/python/tk_multi_loader/medm/reference.py index f4aa6573..3b59b3dc 100644 --- a/python/tk_multi_loader/medm/reference.py +++ b/python/tk_multi_loader/medm/reference.py @@ -51,7 +51,7 @@ def reference_revision(revision_id: str) -> str: engine = sgtk.platform.current_engine() if not hasattr(engine.flow_host, "create_reference"): - msg = "Referencing is not supported in current execution FlowContext." + msg = "Referencing is not supported in current execution." raise CreateReferenceError(input_id=revision_id, details=msg) # We will disallow referencing into a non-asset scene @@ -106,7 +106,7 @@ def copy_reference_link(revision_id: str) -> str: engine = sgtk.platform.current_engine() if engine.flow_host is None: - raise FlowError("Not running in a supported host FlowContext.") + raise FlowError("Not running in a supported host.") try: if FlowVersion.is_version_id(revision_id): @@ -120,7 +120,7 @@ def copy_reference_link(revision_id: str) -> str: raise CreateReferenceError(input_id=revision_id, details=msg) from exc # Fetch source component of revision - revision.fetch() + revision.fetch(component_purpose="") # TODO: component_purpose required # Get path to source path of revision in local storage file_path = revision.get_storage_source_path() From 9fad77e9d66c59f2fa39e78007170681f40a7dd5 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Wed, 17 Jun 2026 09:22:48 -0500 Subject: [PATCH 15/37] Update imports --- hooks/tk-houdini_actions.py | 9 +- hooks/tk-maya_actions.py | 9 +- hooks/tk-nuke_actions.py | 7 +- python/tk_multi_loader/medm/create.py | 118 +++++++++--------- python/tk_multi_loader/medm/entity_model.py | 25 ++-- python/tk_multi_loader/medm/file.py | 32 ++--- python/tk_multi_loader/medm/flowam_actions.py | 25 ++-- .../medm/latestpublish_model.py | 19 ++- .../medm/publishhistory_model.py | 13 +- python/tk_multi_loader/medm/reference.py | 33 ++--- python/tk_multi_loader/medm/shared_cache.py | 14 +-- .../tk_multi_loader/medm/template_queries.py | 19 +-- .../tk_multi_loader/medm/thumbnail_service.py | 4 +- python/tk_multi_loader/medm/utils.py | 13 +- 14 files changed, 170 insertions(+), 170 deletions(-) diff --git a/hooks/tk-houdini_actions.py b/hooks/tk-houdini_actions.py index a299f09a..102e6a4c 100644 --- a/hooks/tk-houdini_actions.py +++ b/hooks/tk-houdini_actions.py @@ -16,6 +16,7 @@ import re import sgtk +from tank_vendor.flow_integration_sdk.sandbox import is_local_draft, is_new_asset HookBaseClass = sgtk.get_hook_baseclass() @@ -113,7 +114,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): if "open" in actions and sg_publish_data.get("type") == "PublishedFile": if ( - flowam_actions._is_local_draft(sg_publish_data) + is_local_draft(sg_publish_data.get("sg_flow_revision_id")) or sg_publish_data.get( "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) @@ -148,9 +149,9 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): if "discard_draft" in actions: draft_id = sg_publish_data.get("sg_flow_revision_id") - if flowam_actions._is_local_draft( - sg_publish_data - ) and flowam_actions._is_new_asset(draft_id): + if is_local_draft( + sg_publish_data.get("sg_flow_revision_id") + ) and is_new_asset(draft_id): action_instances.append( { "name": "discard_draft", diff --git a/hooks/tk-maya_actions.py b/hooks/tk-maya_actions.py index ab4b6c45..fe3fb597 100644 --- a/hooks/tk-maya_actions.py +++ b/hooks/tk-maya_actions.py @@ -19,6 +19,7 @@ import maya.cmds as cmds import maya.mel as mel import sgtk +from tank_vendor.flow_integration_sdk.sandbox import is_local_draft, is_new_asset HookBaseClass = sgtk.get_hook_baseclass() @@ -142,7 +143,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): "open" in actions and sg_publish_data.get("type") == "PublishedFile" and ( - flowam_actions._is_local_draft(sg_publish_data) + is_local_draft(sg_publish_data.get("sg_flow_revision_id")) or sg_publish_data.get( "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) @@ -180,9 +181,9 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): if "discard_draft" in actions: draft_id = sg_publish_data.get("sg_flow_revision_id") - if flowam_actions._is_local_draft( - sg_publish_data - ) and flowam_actions._is_new_asset(draft_id): + if is_local_draft( + sg_publish_data.get("sg_flow_revision_id") + ) and is_new_asset(draft_id): action_instances.append( { "name": "discard_draft", diff --git a/hooks/tk-nuke_actions.py b/hooks/tk-nuke_actions.py index 566d06c9..125ba5b0 100644 --- a/hooks/tk-nuke_actions.py +++ b/hooks/tk-nuke_actions.py @@ -18,6 +18,7 @@ import sys import sgtk +from tank_vendor.flow_integration_sdk.sandbox import is_local_draft HookBaseClass = sgtk.get_hook_baseclass() @@ -147,7 +148,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): # 1. Local drafts (version_number == -1 and is_local_draft) # 2. Published revisions (version_number > -1) if ( - flowam_actions._is_local_draft(sg_publish_data) + is_local_draft(sg_publish_data.get("sg_flow_revision_id")) or sg_publish_data.get( "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) @@ -162,8 +163,8 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): } ) - if "discard_draft" in actions and flowam_actions._is_local_draft( - sg_publish_data + if "discard_draft" in actions and is_local_draft( + sg_publish_data.get("sg_flow_revision_id") ): action_instances.append( { diff --git a/python/tk_multi_loader/medm/create.py b/python/tk_multi_loader/medm/create.py index 62f0b604..d412c95f 100644 --- a/python/tk_multi_loader/medm/create.py +++ b/python/tk_multi_loader/medm/create.py @@ -33,19 +33,17 @@ create_asset_hierarchy, ) from sgtk.flowam.utils import BaseInputs, create_components_for_publish -from tank_vendor.flow_integration_sdk.exceptions import CreateAssetError, FlowError -from tank_vendor.flow_integration_sdk.globals import FOLDER_TYPE_ID -from tank_vendor.flow_integration_sdk.objects import FlowAsset, FlowProject -from tank_vendor.flow_integration_sdk.publish import publish_new_asset -from tank_vendor.flow_integration_sdk.sandbox import ( - NewDraftInfo, - create_asset_in_sandbox, - read_draft_info, +from tank_vendor.flow_integration_sdk import ( + exceptions, + globals, + objects, + publish, + sandbox, + schema, ) -from tank_vendor.flow_integration_sdk.schema import get_schema_id +from ..constants import TEMPLATE_FOLDER, TEMPLATE_TYPE from .utils import cleanpath, fileext -from ..constants import TEMPLATE_TYPE, TEMPLATE_FOLDER # --------------------------------- @@ -112,32 +110,32 @@ def validate(self): # If sg entity name is provided, we also expect an entity type and pipeline step if self.sg_entity_name and not self.sg_entity_type: msg = "Incomplete sg context provided. Must provide sg_entity_type." - raise CreateAssetError(data=self.asdict(), details=msg) + raise exceptions.CreateAssetError(data=self.asdict(), details=msg) if self.sg_entity_name and not self.sg_pipeline_step: msg = "Incomplete sg context provided. Must provide sg_pipeline_step." - raise CreateAssetError(data=self.asdict(), details=msg) + raise exceptions.CreateAssetError(data=self.asdict(), details=msg) # If create mode is TEMPLATE or GENERIC, we need a source path if self.create_mode == CreateMode.TEMPLATE and not self.source_path: msg = "No template source path provided." - raise CreateAssetError(data=self.asdict(), details=msg) + raise exceptions.CreateAssetError(data=self.asdict(), details=msg) if self.create_mode == CreateMode.GENERIC and not self.source_path: msg = "No source path provided for generic asset." - raise CreateAssetError(data=self.asdict(), details=msg) + raise exceptions.CreateAssetError(data=self.asdict(), details=msg) # If pipeline step is provided, we expect task_name to be provided as well if self.sg_pipeline_step and not self.sg_task_name: msg = "Incomplete sg context provided. Must provide sg_task_name." - raise CreateAssetError(data=self.asdict(), details=msg) + raise exceptions.CreateAssetError(data=self.asdict(), details=msg) # prep_scene_callback is only applicable when create_mode is NEW or TEMPLATE if ( self.create_mode == CreateMode.GENERIC and self.prep_scene_callback is not None ): msg = "prep_scene_callback is not applicable when create_mode is GENERIC." - raise CreateAssetError(data=self.asdict(), details=msg) + raise exceptions.CreateAssetError(data=self.asdict(), details=msg) # There should always be a project id provided if not self.am_project_id: - raise CreateAssetError( + raise exceptions.CreateAssetError( data=self.asdict(), details="No project id provided." ) @@ -171,15 +169,15 @@ def validate(self): # Pipeline step value must be provided if not self.sg_pipeline_step: msg = "No pipeline step provided." - raise CreateAssetError(data=self.asdict(), details=msg) + raise exceptions.CreateAssetError(data=self.asdict(), details=msg) # There should always be a project id provided if not self.am_project_id: - raise CreateAssetError( + raise exceptions.CreateAssetError( data=self.asdict(), details="No project id provided." ) # Template must have a name if not self.template_name: - raise CreateAssetError( + raise exceptions.CreateAssetError( data=self.asdict(), details="No template name provided." ) # Create mode TEMPLATE and GENERIC are not applicable for templates. @@ -188,10 +186,10 @@ def validate(self): or self.create_mode == CreateMode.GENERIC ): msg = f"Invalid CreateMode provided for template creation: {self.create_mode}." - raise CreateAssetError(data=self.asdict(), details=msg) + raise exceptions.CreateAssetError(data=self.asdict(), details=msg) -def get_template_source_path(template: FlowAsset) -> str: +def get_template_source_path(template: objects.FlowAsset) -> str: """Return the published source path of the given template. Fetch binary if necessary. @@ -209,7 +207,7 @@ def get_template_source_path(template: FlowAsset) -> str: # --------------------------------- # Workflows # --------------------------------- -def create_dcc_workfile(inputs: CreateInputs) -> NewDraftInfo: +def create_dcc_workfile(inputs: CreateInputs) -> sandbox.NewDraftInfo: """Create a DCC workfile asset in sandbox based on criteria provided in inputs. See documentation for CreateInputs for expected inputs. @@ -229,7 +227,7 @@ def create_dcc_workfile(inputs: CreateInputs) -> NewDraftInfo: if not in_dcc_context(): msg = "Cannot create DCC workfile without being in DCC." - raise CreateAssetError(data=inputs.asdict(), details=msg) + raise exceptions.CreateAssetError(data=inputs.asdict(), details=msg) # Create any necessary hierarchy above current asset parent = create_asset_hierarchy(inputs) @@ -240,7 +238,7 @@ def create_dcc_workfile(inputs: CreateInputs) -> NewDraftInfo: app.log_info("Creating DCC asset complete!") # Open the draft file - draft_info = read_draft_info(draft_id) + draft_info = sandbox.read_draft_info(draft_id) draft_path = draft_info.source_path app.log_info(f"Opening draft path: {draft_path}") flow_host().open_file(draft_path) @@ -251,7 +249,7 @@ def create_dcc_workfile(inputs: CreateInputs) -> NewDraftInfo: return draft_info -def create_template_workfile(inputs: CreateTemplateInputs) -> NewDraftInfo: +def create_template_workfile(inputs: CreateTemplateInputs) -> sandbox.NewDraftInfo: """Create a DCC workfile asset in sandbox based on criteria provided in inputs. See documentation for CreateTemplateInputs for expected inputs. @@ -272,7 +270,7 @@ def create_template_workfile(inputs: CreateTemplateInputs) -> NewDraftInfo: if not in_dcc_context(): msg = "Cannot create template workfile without being in DCC." - raise CreateAssetError(data=inputs.asdict(), details=msg) + raise exceptions.CreateAssetError(data=inputs.asdict(), details=msg) # Create any necessary hierarchy above current asset parent = _create_template_hierarchy(inputs) @@ -283,7 +281,7 @@ def create_template_workfile(inputs: CreateTemplateInputs) -> NewDraftInfo: app.log_info("Creating template asset complete!") # Open the draft file - draft_info = read_draft_info(draft_id) + draft_info = sandbox.read_draft_info(draft_id) draft_path = draft_info.source_path app.log_info(f"Opening draft path: {draft_path}") flow_host().open_file(draft_path) @@ -308,7 +306,7 @@ def in_dcc_context() -> bool: return engine.name != "tk-desktop" -def _has_workfile_type(parent: FlowAsset, type_id: str) -> bool: +def _has_workfile_type(parent: objects.FlowAsset, type_id: str) -> bool: """Return True if parent asset contains a child of given type.""" if parent.find_children(type_id=type_id): @@ -316,7 +314,7 @@ def _has_workfile_type(parent: FlowAsset, type_id: str) -> bool: return False -def _get_or_create_root_folder(inputs: CreateInputs) -> FlowAsset: +def _get_or_create_root_folder(inputs: CreateInputs) -> objects.FlowAsset: """Retrieve top-level folder pertinent to new asset. If it doesn't exist, create it. Returns: @@ -331,10 +329,10 @@ def _get_or_create_root_folder(inputs: CreateInputs) -> FlowAsset: sg_entity_type = inputs.sg_entity_type try: - project = FlowProject(am_project_id) - except FlowError as exc: + project = objects.FlowProject(am_project_id) + except exceptions.FlowError as exc: msg = f"Invalid Flow project id provided: {am_project_id}" - raise CreateAssetError(data=inputs.asdict(), details=msg) from exc + raise exceptions.CreateAssetError(data=inputs.asdict(), details=msg) from exc # Create top-level folders if they don't already exist in project @@ -344,11 +342,11 @@ def _get_or_create_root_folder(inputs: CreateInputs) -> FlowAsset: if not folder: app.log_info(f'Creating "{SHOT_FOLDER}" folder...') desc = "Folder for Shot assets." - folder = publish_new_asset( + folder = publish.publish_new_asset( name=SHOT_FOLDER, parent=project, components=create_components_for_publish( - type_ids=[FOLDER_TYPE_ID], + type_ids=[globals.FOLDER_TYPE_ID], ), description=desc, ) @@ -358,11 +356,11 @@ def _get_or_create_root_folder(inputs: CreateInputs) -> FlowAsset: if not folder: app.log_info(f'Creating "{ASSET_FOLDER}" folder...') desc = "Folder for Asset Build assets." - folder = publish_new_asset( + folder = publish.publish_new_asset( name=ASSET_FOLDER, parent=project, components=create_components_for_publish( - type_ids=[FOLDER_TYPE_ID], + type_ids=[globals.FOLDER_TYPE_ID], ), description=desc, ) @@ -372,22 +370,22 @@ def _get_or_create_root_folder(inputs: CreateInputs) -> FlowAsset: if not folder: app.log_info(f'Creating "{GENERIC_FOLDER}" folder...') desc = "Folder for Generic assets." - folder = publish_new_asset( + folder = publish.publish_new_asset( name=GENERIC_FOLDER, parent=project, components=create_components_for_publish( - type_ids=[FOLDER_TYPE_ID], + type_ids=[globals.FOLDER_TYPE_ID], ), description=desc, ) else: msg = f"Invalid entity type provided: {sg_entity_type}." - raise CreateAssetError(data=inputs.asdict(), details=msg) + raise exceptions.CreateAssetError(data=inputs.asdict(), details=msg) return folder -def _create_dcc_workfile_asset(parent: FlowAsset, inputs: CreateInputs) -> str: +def _create_dcc_workfile_asset(parent: objects.FlowAsset, inputs: CreateInputs) -> str: """Called when creating a new dcc workfile asset. This function will create the workfile asset in sandbox under the given parent. @@ -405,14 +403,14 @@ def _create_dcc_workfile_asset(parent: FlowAsset, inputs: CreateInputs) -> str: # Determine workfile type to be created workfile_type = flow_host().WORKFILE_TYPE - type_id = get_schema_id(workfile_type) + type_id = schema.get_schema_id(workfile_type) # Only allow one workfile of DCC type under parent if _has_workfile_type(parent, type_id): msg = f'A workfile of type "{workfile_type}" has already been created ' msg += f'under pipeline step "{parent.name}". Please open the asset from ' msg += "the Loader app to publish another revision of this asset." - raise CreateAssetError(data=inputs.asdict(), details=msg) + raise exceptions.CreateAssetError(data=inputs.asdict(), details=msg) # By convention asset name will be the sg entity name name = inputs.sg_entity_name @@ -429,12 +427,14 @@ def _create_dcc_workfile_asset(parent: FlowAsset, inputs: CreateInputs) -> str: # Open the template file if not os.path.exists(inputs.source_path): msg = f"Template path does not exist: {inputs.source_path}" - raise CreateAssetError(data=inputs.asdict(), details=msg) + raise exceptions.CreateAssetError(data=inputs.asdict(), details=msg) try: flow_host().open_file(inputs.source_path) except Exception as exc: # pylint: disable=broad-except msg = f"Could not open template path: {inputs.source_path}" - raise CreateAssetError(data=inputs.asdict(), details=msg) from exc + raise exceptions.CreateAssetError( + data=inputs.asdict(), details=msg + ) from exc # Call prep scene callback if provided if inputs.prep_scene_callback: @@ -442,7 +442,7 @@ def _create_dcc_workfile_asset(parent: FlowAsset, inputs: CreateInputs) -> str: inputs.prep_scene_callback() except Exception as exc: # pylint: disable=broad-except msg = f"Error running prep_scene_callback during scene prep: {exc}" - raise CreateAssetError(data=inputs.asdict(), details=msg) + raise exceptions.CreateAssetError(data=inputs.asdict(), details=msg) app.log_info(f"Saving temp file to: {temp_file}") flow_host().save_file(temp_file) @@ -452,7 +452,7 @@ def _create_dcc_workfile_asset(parent: FlowAsset, inputs: CreateInputs) -> str: f'Creating a workfile asset of type "{workfile_type}" for sg entity "{inputs.sg_entity_name}" in sandbox...' ) desc = inputs.description - draft_id = publish_new_asset( + draft_id = publish.publish_new_asset( name=name, description=desc, parent_id=parent.id, @@ -465,7 +465,7 @@ def _create_dcc_workfile_asset(parent: FlowAsset, inputs: CreateInputs) -> str: return draft_id -def _create_template_hierarchy(inputs: CreateTemplateInputs) -> FlowAsset: +def _create_template_hierarchy(inputs: CreateTemplateInputs) -> objects.FlowAsset: """Called when creating a template asset. This function will ensure that any hierarchical structuring above the workfile asset is created if necessary. (These will be committed directly to remote immediately.) @@ -500,21 +500,21 @@ def _create_template_hierarchy(inputs: CreateTemplateInputs) -> FlowAsset: sg_pipeline_step = inputs.sg_pipeline_step try: - project = FlowProject(am_project_id) - except FlowError as exc: + project = objects.FlowProject(am_project_id) + except exceptions.FlowError as exc: msg = f"Invalid Flow project id provided: {am_project_id}" - raise CreateAssetError(data=inputs.asdict(), details=msg) from exc + raise exceptions.CreateAssetError(data=inputs.asdict(), details=msg) from exc # Create top-level folder if it doesn't already exist in project folder = project.find_child(TEMPLATE_FOLDER) if not folder: app.log_info(f'Creating "{TEMPLATE_FOLDER}" folder...') desc = "Folder for template assets." - folder = publish_new_asset( + folder = publish.publish_new_asset( name=TEMPLATE_FOLDER, parent=project, components=create_components_for_publish( - type_ids=[FOLDER_TYPE_ID], + type_ids=[globals.FOLDER_TYPE_ID], ), description=desc, ) @@ -524,8 +524,8 @@ def _create_template_hierarchy(inputs: CreateTemplateInputs) -> FlowAsset: pipeline_step = folder.find_child(sg_pipeline_step) if not pipeline_step: app.log_info(f'Creating pipeline step asset for "{sg_pipeline_step}"...') - pipeline_step_type_id = get_schema_id(PIPELINE_STEP_TYPE) - pipeline_step = publish_new_asset( + pipeline_step_type_id = schema.get_schema_id(PIPELINE_STEP_TYPE) + pipeline_step = publish.publish_new_asset( name=sg_pipeline_step, parent=folder, components=create_components_for_publish( @@ -537,7 +537,7 @@ def _create_template_hierarchy(inputs: CreateTemplateInputs) -> FlowAsset: def _create_template_workfile_asset( - parent: FlowAsset, inputs: CreateTemplateInputs + parent: objects.FlowAsset, inputs: CreateTemplateInputs ) -> str: """Called when creating a new template workfile asset. This function will create the workfile asset in sandbox under the given parent. @@ -555,8 +555,8 @@ def _create_template_workfile_asset( # NOTE: templates will have dual types, both a dcc type # and template designation workfile_type = flow_host().WORKFILE_TYPE - workfile_type_id = get_schema_id(workfile_type) - template_type_id = get_schema_id(TEMPLATE_TYPE) + workfile_type_id = schema.get_schema_id(workfile_type) + template_type_id = schema.get_schema_id(TEMPLATE_TYPE) name = inputs.template_name @@ -576,7 +576,7 @@ def _create_template_workfile_asset( f'Creating a template asset of type "{workfile_type}" for pipeline step "{inputs.sg_pipeline_step}" in sandbox...' ) desc = inputs.description - draft_id = create_asset_in_sandbox( + draft_id = sandbox.create_asset_in_sandbox( name=name, description=desc, parent_id=parent.id, diff --git a/python/tk_multi_loader/medm/entity_model.py b/python/tk_multi_loader/medm/entity_model.py index ff1b1736..973da7c0 100644 --- a/python/tk_multi_loader/medm/entity_model.py +++ b/python/tk_multi_loader/medm/entity_model.py @@ -27,10 +27,7 @@ import sgtk from sgtk.platform.qt import QtCore, QtGui -from tank_vendor.flow_integration_sdk.exceptions import FlowError -from tank_vendor.flow_integration_sdk.globals import FOLDER_TYPE_ID -from tank_vendor.flow_integration_sdk.objects import FlowAsset, FlowProject -from tank_vendor.flow_integration_sdk.schema import get_schema_id +from tank_vendor.flow_integration_sdk import globals, objects, exceptions, schema from sgtk.flowam.create import PIPELINE_STEP_TYPE from .shared_cache import MedmSharedCache @@ -204,7 +201,7 @@ def search_item(parent): return search_item(None) - def get_cached_children(self, asset: FlowAsset) -> list[FlowAsset]: + def get_cached_children(self, asset: objects.FlowAsset) -> list[objects.FlowAsset]: """ Return child :class:`FlowAsset` objects for *asset*. @@ -229,7 +226,7 @@ def _initialize_project(self) -> None: """ try: current_engine = sgtk.platform.current_engine() - self._project = FlowProject(current_engine.context.flow_project_id) + self._project = objects.FlowProject(current_engine.context.flow_project_id) self._app.log_debug( f"FlowAM Entity: Initialized project '{self._project.name}'" ) @@ -261,14 +258,14 @@ def _get_structural_type_ids(self) -> set: return self._structural_type_ids try: - folder_id = FOLDER_TYPE_ID - pipeline_step_id = get_schema_id(PIPELINE_STEP_TYPE) + folder_id = globals.FOLDER_TYPE_ID + pipeline_step_id = schema.get_schema_id(PIPELINE_STEP_TYPE) self._structural_type_ids = {folder_id, pipeline_step_id} self._app.log_debug( f"FlowAM Entity: structural type IDs = {self._structural_type_ids}" ) - except FlowError as e: + except exceptions.FlowError as e: self._app.log_warning( f"FlowAM Entity: could not resolve structural type IDs ({e}); " "non-structural assets without structural descendants will be hidden." @@ -277,7 +274,7 @@ def _get_structural_type_ids(self) -> set: return self._structural_type_ids - def _is_tree_node(self, asset: FlowAsset) -> bool: + def _is_tree_node(self, asset: objects.FlowAsset) -> bool: """ Return ``True`` when *asset* should appear as a node in the left-hand tree view. @@ -306,7 +303,7 @@ def _is_tree_node(self, asset: FlowAsset) -> bool: children = self._fetch_and_cache_children(asset) return len(children) > 0 - def _icon_for_asset(self, asset: FlowAsset) -> QtGui.QIcon: + def _icon_for_asset(self, asset: objects.FlowAsset) -> QtGui.QIcon: """ Return the appropriate tree icon for *asset* based on its type. @@ -359,7 +356,7 @@ def _load_medm_assets(self) -> None: self.data_refresh_fail.emit(str(e)) def _add_asset_item( - self, asset: FlowAsset, parent_item: Optional[QtGui.QStandardItem] + self, asset: objects.FlowAsset, parent_item: Optional[QtGui.QStandardItem] ) -> QtGui.QStandardItem: """ Create a single ``QStandardItem`` for *asset* and append it to the tree. @@ -429,7 +426,9 @@ def _load_children_for_item(self, item: QtGui.QStandardItem) -> None: f"FlowAM: Could not get children for '{asset.name}': {e}" ) - def _fetch_and_cache_children(self, asset: FlowAsset) -> list[FlowAsset]: + def _fetch_and_cache_children( + self, asset: objects.FlowAsset + ) -> list[objects.FlowAsset]: """ Return child assets for *asset*, fetching from the API only on the first call and caching the result in the shared cache for subsequent diff --git a/python/tk_multi_loader/medm/file.py b/python/tk_multi_loader/medm/file.py index 4278d731..3d8c8462 100644 --- a/python/tk_multi_loader/medm/file.py +++ b/python/tk_multi_loader/medm/file.py @@ -13,16 +13,18 @@ import os import sgtk -from tank_vendor.flow_integration_sdk.exceptions import FlowError -from tank_vendor.flow_integration_sdk.globals import FILE_SEQ_TYPE, SOURCE_PURPOSE -from tank_vendor.flow_integration_sdk.objects import FlowRevision -from tank_vendor.flow_integration_sdk.sandbox import is_local_draft, read_draft_info -from tank_vendor.flow_integration_sdk.schema import get_schema_id +from tank_vendor.flow_integration_sdk import ( + globals, + objects, + sandbox, + exceptions, + schema, +) from .utils import cleanpath -class DownloadRevisionError(FlowError): +class DownloadRevisionError(exceptions.FlowError): def __init__( self, *args, @@ -36,7 +38,7 @@ def __init__( self.directory = directory -class InvalidDraftError(FlowError): +class InvalidDraftError(exceptions.FlowError): def __init__(self, *args, draft_id: str, **kwargs): """ Args: @@ -49,7 +51,7 @@ def __init__(self, *args, draft_id: str, **kwargs): def download_revision( revision_id: str, - component_purpose: str = SOURCE_PURPOSE, + component_purpose: str = globals.SOURCE_PURPOSE, directory: str = "", ) -> dict[int, str]: """Download the requested component of the given revision @@ -76,8 +78,8 @@ def download_revision( # Ensure revision id is valid try: - revision = FlowRevision.get_revision(revision_id) - except FlowError as exc: + revision = objects.FlowRevision.get_revision(revision_id) + except exceptions.FlowError as exc: msg = f"Invalid revision id provided: {revision_id}" raise DownloadRevisionError( revision_id=revision_id, @@ -138,7 +140,9 @@ def download_revision( ) # Determine if revision contains a file sequence - file_seq_comp = revision.find_component(type_id=get_schema_id(FILE_SEQ_TYPE)) + file_seq_comp = revision.find_component( + type_id=schema.get_schema_id(globals.FILE_SEQ_TYPE) + ) result = component.download(directory, file_sequence=file_seq_comp is not None) msg = f'Download complete for "{revision.name}" - "{component.name}"!\n' @@ -163,13 +167,13 @@ def open_draft(draft_id: str): engine = sgtk.platform.current_engine() if not engine.flow_host: - raise FlowError("Opening a draft must be done in a host.") + raise exceptions.FlowError("Opening a draft must be done in a host.") - if not is_local_draft(draft_id): + if not sandbox.is_local_draft(draft_id): msg = f'The draft "{draft_id}" is not in local sandbox.' raise InvalidDraftError(draft_id=draft_id, details=msg) - draft_info = read_draft_info(draft_id) + draft_info = sandbox.read_draft_info(draft_id) draft_path = draft_info.source_path if not os.path.exists(draft_path): msg = f'Corrupted draft folder. The file "{draft_path}" does not exist.' diff --git a/python/tk_multi_loader/medm/flowam_actions.py b/python/tk_multi_loader/medm/flowam_actions.py index 66d67408..e2f0edcd 100644 --- a/python/tk_multi_loader/medm/flowam_actions.py +++ b/python/tk_multi_loader/medm/flowam_actions.py @@ -14,14 +14,7 @@ import sgtk from sgtk import TankError from sgtk.platform.qt import QtGui -from tank_vendor.flow_integration_sdk.sandbox import ( - get_draft_folder, - is_local_draft, - is_new_asset, - read_draft_info, - checkout_revision, - discard_draft, -) +from tank_vendor.flow_integration_sdk import sandbox from ..build_asset_dialog import BuildAssetDialog from ..build_template_dialog import BuildTemplateDialog @@ -70,13 +63,13 @@ def _do_open(self, sg_publish_data: dict) -> None: ) raise TankError("No Revision ID found for this item {}.".format(item_id)) - if version_number == DRAFT_VERSION_IDENTIFIER and is_local_draft( + if version_number == DRAFT_VERSION_IDENTIFIER and sandbox.is_local_draft( sg_publish_data.get("sg_flow_revision_id") ): open_draft(flow_revision_id) elif version_number > DRAFT_VERSION_IDENTIFIER: # Checkout the revision to the local sandbox - checkout_revision(flow_revision_id) + sandbox.checkout_revision(flow_revision_id) else: raise TankError( f"Cannot open item {sg_publish_data['name']} with version number {version_number}. " @@ -218,9 +211,11 @@ def _discard_draft(self, sg_publish_data: dict) -> None: """ parent_window = self._get_dialog_parent() - draft_folder = get_draft_folder(sg_publish_data.get("sg_flow_revision_id")) + draft_folder = sandbox.get_draft_folder( + sg_publish_data.get("sg_flow_revision_id") + ) - if is_new_asset(sg_publish_data.get("sg_flow_revision_id")): + if sandbox.is_new_asset(sg_publish_data.get("sg_flow_revision_id")): # Case 1: new asset message = ( f"Discard the new unpublished asset {sg_publish_data.get('name')}?" @@ -229,7 +224,9 @@ def _discard_draft(self, sg_publish_data: dict) -> None: ) else: # Case 2: draft of existing asset - draft_info = read_draft_info(sg_publish_data.get("sg_flow_revision_id")) + draft_info = sandbox.read_draft_info( + sg_publish_data.get("sg_flow_revision_id") + ) version = draft_info.version message = ( f"Discard the draft of asset {sg_publish_data.get('name')} checked out from version {version}?" @@ -248,7 +245,7 @@ def _discard_draft(self, sg_publish_data: dict) -> None: ) if message_response == QtGui.QMessageBox.StandardButton.Yes: - discard_draft(sg_publish_data.get("sg_flow_revision_id")) + sandbox.discard_draft(sg_publish_data.get("sg_flow_revision_id")) QtGui.QMessageBox.information( parent_window, diff --git a/python/tk_multi_loader/medm/latestpublish_model.py b/python/tk_multi_loader/medm/latestpublish_model.py index dbca907b..1487d965 100644 --- a/python/tk_multi_loader/medm/latestpublish_model.py +++ b/python/tk_multi_loader/medm/latestpublish_model.py @@ -21,12 +21,7 @@ import sgtk from sgtk.platform.qt import QtCore, QtGui -from tank_vendor.flow_integration_sdk.objects import FlowAsset -from tank_vendor.flow_integration_sdk.sandbox import ( - DraftInfo, - get_asset_drafts, - get_drafts, -) +from tank_vendor.flow_integration_sdk import objects, sandbox from ..constants import DRAFT_VERSION_IDENTIFIER from .shared_cache import MedmSharedCache @@ -237,7 +232,7 @@ def _populate_model_from_selected_item( if child_asset.id in self._cache.drafts: raw_drafts = self._cache.drafts[child_asset.id] else: - raw_drafts = get_asset_drafts(child_asset.id) + raw_drafts = sandbox.get_asset_drafts(child_asset.id) self._cache.drafts[child_asset.id] = raw_drafts except Exception as e: self._app.log_debug( @@ -304,7 +299,7 @@ def _populate_model_from_selected_item( def _extract_asset_from_tree_item( self, item: QtGui.QStandardItem - ) -> Optional[FlowAsset]: + ) -> Optional[objects.FlowAsset]: """ Extract the FlowAM Asset object from a tree view QStandardItem. @@ -319,7 +314,7 @@ def _extract_asset_from_tree_item( asset_data = item.data(QtCore.Qt.UserRole + 1) return asset_data - def _fetch_asset_children(self, asset: FlowAsset) -> list[dict[str, Any]]: + def _fetch_asset_children(self, asset: objects.FlowAsset) -> list[dict[str, Any]]: """ Fetch all non-structural child assets and convert to sg_data dicts. @@ -370,7 +365,7 @@ def _fetch_asset_children(self, asset: FlowAsset) -> list[dict[str, Any]]: ) return children_asset_sg_dicts - def _asset_to_sg_dict(self, asset: FlowAsset) -> dict[str, Any]: + def _asset_to_sg_dict(self, asset: objects.FlowAsset) -> dict[str, Any]: """ Convert an FlowAM Asset to a Shotgun-compatible dictionary. @@ -428,7 +423,7 @@ def _asset_to_sg_dict(self, asset: FlowAsset) -> dict[str, Any]: return sg_dict def _draft_to_sg_dict( - self, draft_info: DraftInfo, asset: Optional[FlowAsset] = None + self, draft_info: sandbox.DraftInfo, asset: Optional[objects.FlowAsset] = None ) -> dict[str, Any]: """ Convert a local DraftInfo into a Shotgun-compatible dictionary suitable @@ -513,7 +508,7 @@ def _fetch_new_draft_items_for_parent( """ if self._NEW_DRAFTS_CACHE_KEY not in self._cache.drafts: try: - all_new_drafts = get_drafts(draft_type="new") + all_new_drafts = sandbox.get_drafts(draft_type="new") except Exception as e: self._app.log_warning(f"FlowAM: Could not fetch new-asset drafts: {e}") all_new_drafts = [] diff --git a/python/tk_multi_loader/medm/publishhistory_model.py b/python/tk_multi_loader/medm/publishhistory_model.py index 16095511..defdc9ee 100644 --- a/python/tk_multi_loader/medm/publishhistory_model.py +++ b/python/tk_multi_loader/medm/publishhistory_model.py @@ -21,8 +21,7 @@ import sgtk from sgtk.platform.qt import QtCore, QtGui -from tank_vendor.flow_integration_sdk.objects import FlowAsset, FlowVersion -from tank_vendor.flow_integration_sdk.sandbox import DraftInfo, get_asset_drafts +from tank_vendor.flow_integration_sdk import objects, sandbox from .. import utils from .shared_cache import MedmSharedCache @@ -155,7 +154,7 @@ def load_data(self, sg_data: dict[str, Any]) -> None: if asset_id in self._cache.drafts: drafts = self._cache.drafts[asset_id] else: - drafts = get_asset_drafts(asset_id) + drafts = sandbox.get_asset_drafts(asset_id) self._cache.drafts[asset_id] = drafts for draft_info in drafts: self._add_draft_as_qt_item(draft_info, medm_asset) @@ -209,7 +208,7 @@ def _initialize_project_info(self) -> None: ) def _add_version_as_qt_item( - self, asset_version: FlowVersion, asset: FlowAsset + self, asset_version: objects.FlowVersion, asset: objects.FlowAsset ) -> None: """ Convert an FlowVersion to a QStandardItem and add it to the history model. @@ -247,7 +246,7 @@ def get_sg_data(): self._app.log_debug(f"FlowAM History: Added version v{version_number}") def _version_to_sg_dict( - self, version: FlowVersion, asset: FlowAsset + self, version: objects.FlowVersion, asset: objects.FlowAsset ) -> dict[str, Any]: """ Convert a FlowAM FlowVersion to Shotgun-compatible dictionary. @@ -308,7 +307,7 @@ def _version_to_sg_dict( return sg_dict def _draft_to_sg_dict( - self, draft_info: DraftInfo, asset: Optional[FlowAsset] + self, draft_info: sandbox.DraftInfo, asset: Optional[objects.FlowAsset] ) -> dict[str, Any]: """ Convert a DraftInfo (CheckoutDraftInfo or NewDraftInfo) to a Shotgun-compatible @@ -352,7 +351,7 @@ def _draft_to_sg_dict( return sg_dict def _add_draft_as_qt_item( - self, draft_info: DraftInfo, asset: Optional[FlowAsset] + self, draft_info: sandbox.DraftInfo, asset: Optional[objects.FlowAsset] ) -> None: """ Convert a DraftInfo to a QStandardItem and insert it at the top of diff --git a/python/tk_multi_loader/medm/reference.py b/python/tk_multi_loader/medm/reference.py index 3b59b3dc..e8635779 100644 --- a/python/tk_multi_loader/medm/reference.py +++ b/python/tk_multi_loader/medm/reference.py @@ -13,11 +13,10 @@ import os import sgtk -from tank_vendor.flow_integration_sdk.exceptions import FlowError -from tank_vendor.flow_integration_sdk.objects import FlowVersion, FlowRevision +from tank_vendor.flow_integration_sdk import globals, exceptions, objects, storage -class CreateReferenceError(FlowError): +class CreateReferenceError(exceptions.FlowError): def __init__(self, *args, input_id: str = "", file_path: str = "", **kwargs): """ Args: @@ -60,13 +59,13 @@ def reference_revision(revision_id: str) -> str: raise CreateReferenceError(input_id=revision_id, details=msg) try: - if FlowVersion.is_version_id(revision_id): + if objects.FlowVersion.is_version_id(revision_id): input_type = "version" - revision = FlowVersion(revision_id).revision + revision = objects.FlowVersion(revision_id).revision else: input_type = "revision" - revision = FlowRevision.get_revision(revision_id) - except FlowError as exc: + revision = objects.FlowRevision.get_revision(revision_id) + except exceptions.FlowError as exc: msg = f"Could not retrieve {input_type} object." raise CreateReferenceError(input_id=revision_id, details=msg) from exc @@ -74,7 +73,10 @@ def reference_revision(revision_id: str) -> str: revision.fetch() # Get path to source path of revision in local storage - file_path = revision.get_storage_source_path() + # file_path = revision.get_storage_source_path() + file_path = storage.get_storage_component_path( + revision, component_purpose=globals.SOURCE_PURPOSE + ) if file_path is None: msg = "Revision does not have a source component to be referenced." raise CreateReferenceError(input_id=revision_id, details=msg) @@ -106,16 +108,16 @@ def copy_reference_link(revision_id: str) -> str: engine = sgtk.platform.current_engine() if engine.flow_host is None: - raise FlowError("Not running in a supported host.") + raise exceptions.FlowError("Not running in a supported host.") try: - if FlowVersion.is_version_id(revision_id): + if objects.FlowVersion.is_version_id(revision_id): input_type = "version" - revision = FlowVersion(revision_id).revision + revision = objects.FlowVersion(revision_id).revision else: input_type = "revision" - revision = FlowRevision.get_revision(revision_id) - except FlowError as exc: + revision = objects.FlowRevision.get_revision(revision_id) + except exceptions.FlowError as exc: msg = f"Could not retrieve {input_type} object." raise CreateReferenceError(input_id=revision_id, details=msg) from exc @@ -123,7 +125,10 @@ def copy_reference_link(revision_id: str) -> str: revision.fetch(component_purpose="") # TODO: component_purpose required # Get path to source path of revision in local storage - file_path = revision.get_storage_source_path() + # file_path = revision.get_storage_source_path() + file_path = storage.get_storage_component_path( + revision, component_purpose=globals.SOURCE_PURPOSE + ) if file_path is None: msg = "Revision does not have a source component to be referenced." raise CreateReferenceError(input_id=revision_id, details=msg) diff --git a/python/tk_multi_loader/medm/shared_cache.py b/python/tk_multi_loader/medm/shared_cache.py index 9a0d4b97..6de82ba5 100644 --- a/python/tk_multi_loader/medm/shared_cache.py +++ b/python/tk_multi_loader/medm/shared_cache.py @@ -25,7 +25,7 @@ from __future__ import annotations import dataclasses -from typing import Any, Dict, List, Optional +from typing import Any, Optional @dataclasses.dataclass @@ -65,17 +65,17 @@ class MedmSharedCache: """ # asset.id → list[Asset] - children: Dict[str, List[Any]] = dataclasses.field(default_factory=dict) + children: dict[str, list[Any]] = dataclasses.field(default_factory=dict) # asset.id → list[DraftInfo] - drafts: Dict[str, List[Any]] = dataclasses.field(default_factory=dict) + drafts: dict[str, list[Any]] = dataclasses.field(default_factory=dict) # asset.id → list[FlowVersion] - versions: Dict[str, List[Any]] = dataclasses.field(default_factory=dict) + versions: dict[str, list[Any]] = dataclasses.field(default_factory=dict) # medm_type_id_str → (sg_publish_type_id, display_name) - publish_types: Dict[str, tuple] = dataclasses.field(default_factory=dict) + publish_types: dict[str, tuple] = dataclasses.field(default_factory=dict) # revision_id → thumbnail URL (or None when the API returned nothing) - thumbnail_urls: Dict[str, Optional[str]] = dataclasses.field(default_factory=dict) + thumbnail_urls: dict[str, Optional[str]] = dataclasses.field(default_factory=dict) # URL → raw image bytes - thumbnail_data: Dict[str, bytes] = dataclasses.field(default_factory=dict) + thumbnail_data: dict[str, bytes] = dataclasses.field(default_factory=dict) # ------------------------------------------------------------------------- # Convenience clear helpers diff --git a/python/tk_multi_loader/medm/template_queries.py b/python/tk_multi_loader/medm/template_queries.py index 9ccf7273..53e3ff0b 100644 --- a/python/tk_multi_loader/medm/template_queries.py +++ b/python/tk_multi_loader/medm/template_queries.py @@ -15,16 +15,17 @@ from typing import Any, Optional import sgtk -from tank_vendor.flow_integration_sdk.objects import FlowAsset, FlowProject -from tank_vendor.flow_integration_sdk.schema import get_schema_id from sgtk.flowam.create import PIPELINE_STEP_TYPE +from tank_vendor.flow_integration_sdk import objects, schema -from ..constants import TEMPLATE_TYPE, TEMPLATE_FOLDER +from ..constants import TEMPLATE_FOLDER, TEMPLATE_TYPE logger = sgtk.platform.get_logger(__name__) -def get_template_pipeline_steps(project: FlowProject) -> list[FlowAsset]: +def get_template_pipeline_steps( + project: objects.FlowProject, +) -> list[objects.FlowAsset]: """Return pipeline steps available under the Templates folder. :param project: Flow AM ``FlowProject`` instance to query. @@ -34,23 +35,23 @@ def get_template_pipeline_steps(project: FlowProject) -> list[FlowAsset]: template_folder = project.find_child(TEMPLATE_FOLDER) if not template_folder: return [] - pipeline_step_type_id = get_schema_id(PIPELINE_STEP_TYPE) + pipeline_step_type_id = schema.get_schema_id(PIPELINE_STEP_TYPE) return template_folder.find_children(type_id=pipeline_step_type_id) -def get_templates(pipeline_step: FlowAsset) -> list[FlowAsset]: +def get_templates(pipeline_step: objects.FlowAsset) -> list[objects.FlowAsset]: """Return template assets available under a given pipeline step. :param pipeline_step: Flow AM ``Asset`` representing a pipeline step. :returns: List of template ``Asset`` objects under the pipeline step. """ - template_type_id = get_schema_id(TEMPLATE_TYPE) + template_type_id = schema.get_schema_id(TEMPLATE_TYPE) return pipeline_step.find_children(type_id=template_type_id) def find_template_pipeline_step( - project: FlowProject, pipeline_step_name: str -) -> Optional[FlowAsset]: + project: objects.FlowProject, pipeline_step_name: str +) -> Optional[objects.FlowAsset]: """Find a pipeline step by name under the Templates folder. :param project: Flow AM ``Project`` instance to query. diff --git a/python/tk_multi_loader/medm/thumbnail_service.py b/python/tk_multi_loader/medm/thumbnail_service.py index 3ebc8dd6..bf77ad08 100644 --- a/python/tk_multi_loader/medm/thumbnail_service.py +++ b/python/tk_multi_loader/medm/thumbnail_service.py @@ -29,7 +29,7 @@ import sgtk from sgtk.platform.qt import QtCore, QtGui -from tank_vendor.flow_integration_sdk.objects import FlowRevision +from tank_vendor.flow_integration_sdk import objects if TYPE_CHECKING: from .shared_cache import MedmSharedCache @@ -149,7 +149,7 @@ def _resolve_and_fetch( url = self._url_cache.get(revision_id) if url is None and revision_id not in self._url_cache: try: - rev = FlowRevision.get_revision(revision_id) + rev = objects.FlowRevision.get_revision(revision_id) url = rev.get_thumbnail_url() except Exception as exc: self._app.log_debug( diff --git a/python/tk_multi_loader/medm/utils.py b/python/tk_multi_loader/medm/utils.py index e4f069a6..054e32df 100644 --- a/python/tk_multi_loader/medm/utils.py +++ b/python/tk_multi_loader/medm/utils.py @@ -19,11 +19,7 @@ import os from typing import Any, Dict, Optional, Tuple -from tank_vendor.flow_integration_sdk.globals import FOLDER_TYPE_ID -from tank_vendor.flow_integration_sdk.schema import ( - get_schema_display_name, - get_schema_id, -) +from tank_vendor.flow_integration_sdk import globals, schema from sgtk.flowam.create import PIPELINE_STEP_TYPE from ..constants import DRAFT_VERSION_IDENTIFIER, CONTAINER_TYPE @@ -49,12 +45,13 @@ def is_structural_asset(asset: Any) -> bool: """ try: type_ids = set(getattr(asset, "type_ids", None) or []) - if FOLDER_TYPE_ID in type_ids: + if globals.FOLDER_TYPE_ID in type_ids: return True structural_types = (CONTAINER_TYPE, PIPELINE_STEP_TYPE) return any( - asset.find_component(type_id=get_schema_id(ct)) for ct in structural_types + asset.find_component(type_id=schema.get_schema_id(ct)) + for ct in structural_types ) except Exception: return False @@ -224,7 +221,7 @@ def resolve_publish_type( display_name = medm_type_id_str try: - schema_name = get_schema_display_name(medm_type_id_str) + schema_name = schema.get_schema_display_name(medm_type_id_str) if schema_name: display_name = schema_name except Exception as e: From 8dfe63d9ddd1fee3da3074c0a992a646ca450e85 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Wed, 17 Jun 2026 10:03:19 -0500 Subject: [PATCH 16/37] Tested reference link and download workflows --- python/tk_multi_loader/medm/flowam_actions.py | 5 ++--- python/tk_multi_loader/medm/reference.py | 12 +++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/python/tk_multi_loader/medm/flowam_actions.py b/python/tk_multi_loader/medm/flowam_actions.py index e2f0edcd..a3ce36ef 100644 --- a/python/tk_multi_loader/medm/flowam_actions.py +++ b/python/tk_multi_loader/medm/flowam_actions.py @@ -14,13 +14,12 @@ import sgtk from sgtk import TankError from sgtk.platform.qt import QtGui -from tank_vendor.flow_integration_sdk import sandbox +from tank_vendor.flow_integration_sdk import sandbox, exceptions from ..build_asset_dialog import BuildAssetDialog from ..build_template_dialog import BuildTemplateDialog from ..constants import DRAFT_VERSION_IDENTIFIER from .create import ( - CreateAssetError, CreateInputs, CreateMode, CreateTemplateInputs, @@ -180,7 +179,7 @@ def _on_build_scene_dialog_accepted( self._app.log_debug( f"Created a DCC workfile with the draft_id: {draft_info.draft_id}" ) - except CreateAssetError as exc: + except exceptions.CreateAssetError as exc: self._app.log_error(f"Create asset failed: {exc}\nInput data: {exc.data}") QtGui.QMessageBox.critical( diff --git a/python/tk_multi_loader/medm/reference.py b/python/tk_multi_loader/medm/reference.py index e8635779..3c173e8e 100644 --- a/python/tk_multi_loader/medm/reference.py +++ b/python/tk_multi_loader/medm/reference.py @@ -73,9 +73,8 @@ def reference_revision(revision_id: str) -> str: revision.fetch() # Get path to source path of revision in local storage - # file_path = revision.get_storage_source_path() - file_path = storage.get_storage_component_path( - revision, component_purpose=globals.SOURCE_PURPOSE + file_path = revision.get_storage_component_path( + component_purpose=globals.SOURCE_PURPOSE ) if file_path is None: msg = "Revision does not have a source component to be referenced." @@ -122,12 +121,11 @@ def copy_reference_link(revision_id: str) -> str: raise CreateReferenceError(input_id=revision_id, details=msg) from exc # Fetch source component of revision - revision.fetch(component_purpose="") # TODO: component_purpose required + revision.fetch(component_purpose=globals.SOURCE_PURPOSE) # Get path to source path of revision in local storage - # file_path = revision.get_storage_source_path() - file_path = storage.get_storage_component_path( - revision, component_purpose=globals.SOURCE_PURPOSE + file_path = revision.get_storage_component_path( + component_purpose=globals.SOURCE_PURPOSE ) if file_path is None: msg = "Revision does not have a source component to be referenced." From 7e5282611bf049072aa19fa94f669da885c0c3eb Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Wed, 17 Jun 2026 11:58:29 -0500 Subject: [PATCH 17/37] Finished testing build scene, open, reference, discard. --- python/tk_multi_loader/medm/create.py | 74 ++++++++----------- python/tk_multi_loader/medm/flowam_actions.py | 4 +- python/tk_multi_loader/medm/reference.py | 4 +- 3 files changed, 34 insertions(+), 48 deletions(-) diff --git a/python/tk_multi_loader/medm/create.py b/python/tk_multi_loader/medm/create.py index d412c95f..78f98f50 100644 --- a/python/tk_multi_loader/medm/create.py +++ b/python/tk_multi_loader/medm/create.py @@ -23,16 +23,7 @@ from typing import Callable import sgtk -from sgtk.flowam.create import ( - ASSET_FOLDER, - ASSET_TYPE, - GENERIC_FOLDER, - PIPELINE_STEP_TYPE, - SHOT_FOLDER, - SHOT_TYPE, - create_asset_hierarchy, -) -from sgtk.flowam.utils import BaseInputs, create_components_for_publish +from sgtk.flowam import create, utils from tank_vendor.flow_integration_sdk import ( exceptions, globals, @@ -59,7 +50,7 @@ class CreateMode(Enum): @dataclass -class CreateInputs(BaseInputs): +class CreateInputs(utils.BaseInputs): """Convenience structure to hold create inputs and allow them to be passed easily between helper functions. """ @@ -141,7 +132,7 @@ def validate(self): @dataclass -class CreateTemplateInputs(BaseInputs): +class CreateTemplateInputs(utils.BaseInputs): """Convenience structure to hold create inputs and allow them to be passed easily between helper functions. """ @@ -200,8 +191,10 @@ def get_template_source_path(template: objects.FlowAsset) -> str: Full path to template file in blob storage. """ revision = template.get_latest_revision() - revision.fetch() - return revision.get_storage_source_path() + revision.fetch(component_purpose=globals.SOURCE_PURPOSE) + return revision.get_storage_component_path( + component_purpose=globals.SOURCE_PURPOSE + ) # --------------------------------- @@ -230,21 +223,19 @@ def create_dcc_workfile(inputs: CreateInputs) -> sandbox.NewDraftInfo: raise exceptions.CreateAssetError(data=inputs.asdict(), details=msg) # Create any necessary hierarchy above current asset - parent = create_asset_hierarchy(inputs) + parent = create.create_asset_hierarchy(inputs) # Create the workfile asset in sandbox - draft_id = _create_dcc_workfile_asset(parent, inputs) + draft_info = _create_dcc_workfile_asset(parent, inputs) app.log_info("Creating DCC asset complete!") # Open the draft file - draft_info = sandbox.read_draft_info(draft_id) draft_path = draft_info.source_path app.log_info(f"Opening draft path: {draft_path}") flow_host().open_file(draft_path) - # Set asset context (before it was only FlowContext.draft_id) - app.context.set_flow_context(flow_host().current_file) + app.context.set_flow_context(flow_host().current_file()) return draft_info @@ -286,8 +277,7 @@ def create_template_workfile(inputs: CreateTemplateInputs) -> sandbox.NewDraftIn app.log_info(f"Opening draft path: {draft_path}") flow_host().open_file(draft_path) - # Set asset context (before it was only FlowContext.draft_id) - app.context.set_flow_context(flow_host().current_file) + app.context.set_flow_context(flow_host().current_file()) return draft_info @@ -336,44 +326,44 @@ def _get_or_create_root_folder(inputs: CreateInputs) -> objects.FlowAsset: # Create top-level folders if they don't already exist in project - if sg_entity_type == SHOT_TYPE: + if sg_entity_type == create.SHOT_TYPE: # Create shot folder or grab existing one if sg entity is a shot type - folder = project.find_child(SHOT_FOLDER) + folder = project.find_child(create.SHOT_FOLDER) if not folder: - app.log_info(f'Creating "{SHOT_FOLDER}" folder...') + app.log_info(f'Creating "{create.SHOT_FOLDER}" folder...') desc = "Folder for Shot assets." folder = publish.publish_new_asset( - name=SHOT_FOLDER, + name=create.SHOT_FOLDER, parent=project, - components=create_components_for_publish( + components=utils.create_components_for_publish( type_ids=[globals.FOLDER_TYPE_ID], ), description=desc, ) - elif sg_entity_type == ASSET_TYPE: + elif sg_entity_type == create.ASSET_TYPE: # Create asset folder or grab existing one if sg entity is an asset type - folder = project.find_child(ASSET_FOLDER) + folder = project.find_child(create.ASSET_FOLDER) if not folder: - app.log_info(f'Creating "{ASSET_FOLDER}" folder...') + app.log_info(f'Creating "{create.ASSET_FOLDER}" folder...') desc = "Folder for Asset Build assets." folder = publish.publish_new_asset( - name=ASSET_FOLDER, + name=create.ASSET_FOLDER, parent=project, - components=create_components_for_publish( + components=utils.create_components_for_publish( type_ids=[globals.FOLDER_TYPE_ID], ), description=desc, ) elif inputs.create_mode == CreateMode.GENERIC: # Create generic folder or grab existing one - folder = project.find_child(GENERIC_FOLDER) + folder = project.find_child(create.GENERIC_FOLDER) if not folder: - app.log_info(f'Creating "{GENERIC_FOLDER}" folder...') + app.log_info(f'Creating "{create.GENERIC_FOLDER}" folder...') desc = "Folder for Generic assets." folder = publish.publish_new_asset( - name=GENERIC_FOLDER, + name=create.GENERIC_FOLDER, parent=project, - components=create_components_for_publish( + components=utils.create_components_for_publish( type_ids=[globals.FOLDER_TYPE_ID], ), description=desc, @@ -452,18 +442,14 @@ def _create_dcc_workfile_asset(parent: objects.FlowAsset, inputs: CreateInputs) f'Creating a workfile asset of type "{workfile_type}" for sg entity "{inputs.sg_entity_name}" in sandbox...' ) desc = inputs.description - draft_id = publish.publish_new_asset( + return sandbox.create_asset_in_sandbox( name=name, description=desc, parent_id=parent.id, - components=create_components_for_publish( - type_ids=[type_id], - ), + type_ids=[type_id], source_path=temp_file, ) - return draft_id - def _create_template_hierarchy(inputs: CreateTemplateInputs) -> objects.FlowAsset: """Called when creating a template asset. @@ -513,7 +499,7 @@ def _create_template_hierarchy(inputs: CreateTemplateInputs) -> objects.FlowAsse folder = publish.publish_new_asset( name=TEMPLATE_FOLDER, parent=project, - components=create_components_for_publish( + components=utils.create_components_for_publish( type_ids=[globals.FOLDER_TYPE_ID], ), description=desc, @@ -524,11 +510,11 @@ def _create_template_hierarchy(inputs: CreateTemplateInputs) -> objects.FlowAsse pipeline_step = folder.find_child(sg_pipeline_step) if not pipeline_step: app.log_info(f'Creating pipeline step asset for "{sg_pipeline_step}"...') - pipeline_step_type_id = schema.get_schema_id(PIPELINE_STEP_TYPE) + pipeline_step_type_id = schema.get_schema_id(create.PIPELINE_STEP_TYPE) pipeline_step = publish.publish_new_asset( name=sg_pipeline_step, parent=folder, - components=create_components_for_publish( + components=utils.create_components_for_publish( type_ids=[pipeline_step_type_id], ), ) diff --git a/python/tk_multi_loader/medm/flowam_actions.py b/python/tk_multi_loader/medm/flowam_actions.py index a3ce36ef..7476e645 100644 --- a/python/tk_multi_loader/medm/flowam_actions.py +++ b/python/tk_multi_loader/medm/flowam_actions.py @@ -175,9 +175,9 @@ def _on_build_scene_dialog_accepted( ) try: - draft_info = create_dcc_workfile(create_inputs) + asset = create_dcc_workfile(create_inputs) self._app.log_debug( - f"Created a DCC workfile with the draft_id: {draft_info.draft_id}" + f"Created a DCC workfile asset: {asset}" ) except exceptions.CreateAssetError as exc: self._app.log_error(f"Create asset failed: {exc}\nInput data: {exc.data}") diff --git a/python/tk_multi_loader/medm/reference.py b/python/tk_multi_loader/medm/reference.py index 3c173e8e..7eabc9ff 100644 --- a/python/tk_multi_loader/medm/reference.py +++ b/python/tk_multi_loader/medm/reference.py @@ -13,7 +13,7 @@ import os import sgtk -from tank_vendor.flow_integration_sdk import globals, exceptions, objects, storage +from tank_vendor.flow_integration_sdk import globals, exceptions, objects class CreateReferenceError(exceptions.FlowError): @@ -70,7 +70,7 @@ def reference_revision(revision_id: str) -> str: raise CreateReferenceError(input_id=revision_id, details=msg) from exc # Fetch source component of revision - revision.fetch() + revision.fetch(component_purpose=globals.SOURCE_PURPOSE) # Get path to source path of revision in local storage file_path = revision.get_storage_component_path( From 3d30405069d171cb332da56eb6c9373ab23c48af Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Wed, 17 Jun 2026 12:19:56 -0500 Subject: [PATCH 18/37] Tested create template workflow --- python/tk_multi_loader/medm/create.py | 94 +++------------------------ 1 file changed, 10 insertions(+), 84 deletions(-) diff --git a/python/tk_multi_loader/medm/create.py b/python/tk_multi_loader/medm/create.py index 78f98f50..1eaf27ad 100644 --- a/python/tk_multi_loader/medm/create.py +++ b/python/tk_multi_loader/medm/create.py @@ -192,9 +192,7 @@ def get_template_source_path(template: objects.FlowAsset) -> str: """ revision = template.get_latest_revision() revision.fetch(component_purpose=globals.SOURCE_PURPOSE) - return revision.get_storage_component_path( - component_purpose=globals.SOURCE_PURPOSE - ) + return revision.get_storage_component_path(component_purpose=globals.SOURCE_PURPOSE) # --------------------------------- @@ -267,12 +265,11 @@ def create_template_workfile(inputs: CreateTemplateInputs) -> sandbox.NewDraftIn parent = _create_template_hierarchy(inputs) # Create the workfile asset in sandbox - draft_id = _create_template_workfile_asset(parent, inputs) + draft_info = _create_template_workfile_asset(parent, inputs) app.log_info("Creating template asset complete!") # Open the draft file - draft_info = sandbox.read_draft_info(draft_id) draft_path = draft_info.source_path app.log_info(f"Opening draft path: {draft_path}") flow_host().open_file(draft_path) @@ -304,78 +301,9 @@ def _has_workfile_type(parent: objects.FlowAsset, type_id: str) -> bool: return False -def _get_or_create_root_folder(inputs: CreateInputs) -> objects.FlowAsset: - """Retrieve top-level folder pertinent to new asset. If it doesn't exist, create it. - - Returns: - Folder asset object. - - Raises: - CreateAssetError - """ - app = sgtk.platform.current_bundle() - - am_project_id = inputs.am_project_id - sg_entity_type = inputs.sg_entity_type - - try: - project = objects.FlowProject(am_project_id) - except exceptions.FlowError as exc: - msg = f"Invalid Flow project id provided: {am_project_id}" - raise exceptions.CreateAssetError(data=inputs.asdict(), details=msg) from exc - - # Create top-level folders if they don't already exist in project - - if sg_entity_type == create.SHOT_TYPE: - # Create shot folder or grab existing one if sg entity is a shot type - folder = project.find_child(create.SHOT_FOLDER) - if not folder: - app.log_info(f'Creating "{create.SHOT_FOLDER}" folder...') - desc = "Folder for Shot assets." - folder = publish.publish_new_asset( - name=create.SHOT_FOLDER, - parent=project, - components=utils.create_components_for_publish( - type_ids=[globals.FOLDER_TYPE_ID], - ), - description=desc, - ) - elif sg_entity_type == create.ASSET_TYPE: - # Create asset folder or grab existing one if sg entity is an asset type - folder = project.find_child(create.ASSET_FOLDER) - if not folder: - app.log_info(f'Creating "{create.ASSET_FOLDER}" folder...') - desc = "Folder for Asset Build assets." - folder = publish.publish_new_asset( - name=create.ASSET_FOLDER, - parent=project, - components=utils.create_components_for_publish( - type_ids=[globals.FOLDER_TYPE_ID], - ), - description=desc, - ) - elif inputs.create_mode == CreateMode.GENERIC: - # Create generic folder or grab existing one - folder = project.find_child(create.GENERIC_FOLDER) - if not folder: - app.log_info(f'Creating "{create.GENERIC_FOLDER}" folder...') - desc = "Folder for Generic assets." - folder = publish.publish_new_asset( - name=create.GENERIC_FOLDER, - parent=project, - components=utils.create_components_for_publish( - type_ids=[globals.FOLDER_TYPE_ID], - ), - description=desc, - ) - else: - msg = f"Invalid entity type provided: {sg_entity_type}." - raise exceptions.CreateAssetError(data=inputs.asdict(), details=msg) - - return folder - - -def _create_dcc_workfile_asset(parent: objects.FlowAsset, inputs: CreateInputs) -> str: +def _create_dcc_workfile_asset( + parent: objects.FlowAsset, inputs: CreateInputs +) -> sandbox.NewDraftInfo: """Called when creating a new dcc workfile asset. This function will create the workfile asset in sandbox under the given parent. @@ -384,7 +312,7 @@ def _create_dcc_workfile_asset(parent: objects.FlowAsset, inputs: CreateInputs) See CreateInputs documentation. Returns: - The draft id of the workfile asset created. + The draft_info of the workfile asset created. Raises: CreateAssetError @@ -498,7 +426,7 @@ def _create_template_hierarchy(inputs: CreateTemplateInputs) -> objects.FlowAsse desc = "Folder for template assets." folder = publish.publish_new_asset( name=TEMPLATE_FOLDER, - parent=project, + parent_id=project.id, components=utils.create_components_for_publish( type_ids=[globals.FOLDER_TYPE_ID], ), @@ -513,7 +441,7 @@ def _create_template_hierarchy(inputs: CreateTemplateInputs) -> objects.FlowAsse pipeline_step_type_id = schema.get_schema_id(create.PIPELINE_STEP_TYPE) pipeline_step = publish.publish_new_asset( name=sg_pipeline_step, - parent=folder, + parent_id=folder.id, components=utils.create_components_for_publish( type_ids=[pipeline_step_type_id], ), @@ -524,7 +452,7 @@ def _create_template_hierarchy(inputs: CreateTemplateInputs) -> objects.FlowAsse def _create_template_workfile_asset( parent: objects.FlowAsset, inputs: CreateTemplateInputs -) -> str: +) -> sandbox.NewDraftInfo: """Called when creating a new template workfile asset. This function will create the workfile asset in sandbox under the given parent. @@ -562,12 +490,10 @@ def _create_template_workfile_asset( f'Creating a template asset of type "{workfile_type}" for pipeline step "{inputs.sg_pipeline_step}" in sandbox...' ) desc = inputs.description - draft_id = sandbox.create_asset_in_sandbox( + return sandbox.create_asset_in_sandbox( name=name, description=desc, parent_id=parent.id, type_ids=[workfile_type_id, template_type_id], # flag as template type source_path=temp_file, ) - - return draft_id From 7d66ea0c0aa6200170965aa801dde496b387a639 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Wed, 17 Jun 2026 12:44:18 -0500 Subject: [PATCH 19/37] Add tk_core_ref value for testing --- azure-pipelines.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1f83ab32..85e7a47a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -46,3 +46,4 @@ jobs: - name: tk-framework-qtwidgets - name: tk-framework-shotgunutils - name: tk-shell + tk_core_ref: origin/ticket/sg-43461/migrate-host-base From a7329ae1efc5f6515881cdcfdc3f88948ef50d53 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Wed, 17 Jun 2026 13:47:19 -0500 Subject: [PATCH 20/37] Format --- azure-pipelines.yml | 2 +- python/tk_multi_loader/medm/flowam_actions.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 85e7a47a..8b5b1780 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -46,4 +46,4 @@ jobs: - name: tk-framework-qtwidgets - name: tk-framework-shotgunutils - name: tk-shell - tk_core_ref: origin/ticket/sg-43461/migrate-host-base + tk_core_ref: ticket/sg-43461/migrate-host-base diff --git a/python/tk_multi_loader/medm/flowam_actions.py b/python/tk_multi_loader/medm/flowam_actions.py index 7476e645..230cbf80 100644 --- a/python/tk_multi_loader/medm/flowam_actions.py +++ b/python/tk_multi_loader/medm/flowam_actions.py @@ -176,9 +176,7 @@ def _on_build_scene_dialog_accepted( try: asset = create_dcc_workfile(create_inputs) - self._app.log_debug( - f"Created a DCC workfile asset: {asset}" - ) + self._app.log_debug(f"Created a DCC workfile asset: {asset}") except exceptions.CreateAssetError as exc: self._app.log_error(f"Create asset failed: {exc}\nInput data: {exc.data}") From 19a52980db49dd50ad6941d844b876cad33ecac7 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Wed, 17 Jun 2026 14:02:25 -0500 Subject: [PATCH 21/37] Fix fixture --- tests/fixtures/config/hooks/test_actions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/fixtures/config/hooks/test_actions.py b/tests/fixtures/config/hooks/test_actions.py index ff6f01bf..4e2e2b66 100644 --- a/tests/fixtures/config/hooks/test_actions.py +++ b/tests/fixtures/config/hooks/test_actions.py @@ -14,7 +14,7 @@ class TestActions(HookBaseClass): - def generate_actions(self, sg_publish_data, actions, ui_area): + def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): """ Returns a list of action instances for a particular publish. This method is called each time a user clicks a publish somewhere in the UI. @@ -95,7 +95,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area): return action_instances - def execute_multiple_actions(self, actions): + def execute_multiple_actions(self, actions, **kwargs): """ Executes the specified action on a list of items. @@ -127,9 +127,9 @@ def execute_multiple_actions(self, actions): name = single_action["name"] sg_publish_data = single_action["sg_publish_data"] params = single_action["params"] - self.execute_action(name, params, sg_publish_data) + self.execute_action(name, params, sg_publish_data, **kwargs) - def execute_action(self, name, params, sg_publish_data): + def execute_action(self, name, params, sg_publish_data, **kwargs): """ Execute a given action. The data sent to this be method will represent one of the actions enumerated by the generate_actions method. From 10c76151cf2160a5daf1752d17242aa9dceace38 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Wed, 17 Jun 2026 14:13:43 -0500 Subject: [PATCH 22/37] remove migrated methods --- python/tk_multi_loader/medm/create.py | 8 ++-- python/tk_multi_loader/medm/file.py | 7 ++- python/tk_multi_loader/medm/utils.py | 67 --------------------------- 3 files changed, 7 insertions(+), 75 deletions(-) diff --git a/python/tk_multi_loader/medm/create.py b/python/tk_multi_loader/medm/create.py index 1eaf27ad..7fe23c46 100644 --- a/python/tk_multi_loader/medm/create.py +++ b/python/tk_multi_loader/medm/create.py @@ -31,10 +31,10 @@ publish, sandbox, schema, + utils, ) from ..constants import TEMPLATE_FOLDER, TEMPLATE_TYPE -from .utils import cleanpath, fileext # --------------------------------- @@ -336,8 +336,8 @@ def _create_dcc_workfile_asset( # Prepare the source file and save to temporary location # By convention the source file will be named after the asset with tempfile.TemporaryDirectory() as temp_dir: - ext = fileext(inputs.source_path) or flow_host().FILE_TYPES[0] - temp_file = cleanpath(temp_dir, f"{name}.{ext}") + ext = utils.fileext(inputs.source_path) or flow_host().FILE_TYPES[0] + temp_file = utils.cleanpath(temp_dir, f"{name}.{ext}") if inputs.create_mode == CreateMode.NEW: # Clear scene flow_host().new_scene() @@ -478,7 +478,7 @@ def _create_template_workfile_asset( # By convention the source file will be named after the asset with tempfile.TemporaryDirectory() as temp_dir: ext = flow_host().FILE_TYPES[0] - temp_file = cleanpath(temp_dir, f"{name}.{ext}") + temp_file = utils.cleanpath(temp_dir, f"{name}.{ext}") if inputs.create_mode == CreateMode.NEW: # Clear scene flow_host().new_scene() diff --git a/python/tk_multi_loader/medm/file.py b/python/tk_multi_loader/medm/file.py index 3d8c8462..14cda491 100644 --- a/python/tk_multi_loader/medm/file.py +++ b/python/tk_multi_loader/medm/file.py @@ -19,10 +19,9 @@ sandbox, exceptions, schema, + utils, ) -from .utils import cleanpath - class DownloadRevisionError(exceptions.FlowError): def __init__( @@ -99,7 +98,7 @@ def download_revision( # Ensure directory exists if directory: - directory = cleanpath(directory) + directory = utils.cleanpath(directory) if os.path.isfile(directory): # Name collision between directory and existing file # NOTE: OS will not allow a directory to be created with same name @@ -130,7 +129,7 @@ def download_revision( if not result: engine.log_warning("Download operation cancelled.") return {} - directory = cleanpath(result[0]) + directory = utils.cleanpath(result[0]) else: msg = "No download location provided." raise DownloadRevisionError( diff --git a/python/tk_multi_loader/medm/utils.py b/python/tk_multi_loader/medm/utils.py index 054e32df..2eecae92 100644 --- a/python/tk_multi_loader/medm/utils.py +++ b/python/tk_multi_loader/medm/utils.py @@ -255,70 +255,3 @@ def resolve_publish_type( result: Tuple[Optional[int], str] = (sg_publish_type_id, display_name) cache.publish_types[medm_type_id_str] = result return result - - -def cleanpath(path: str, *extra: str) -> str: - """Return the same path, normalized and using only front slashes. - - Args: - path: String absolute or relative path. - *extra: Zero or more string arguments representing extra bits to - add to input path in given order. - - Returns: - str: Path that is the product of all input parameters joined. - - Examples: - >>> cleanpath('c:\\dev\\my_root', 'my_dir', 'my_file.ma') - 'c:/dev/my_root/my_dir/my_file.ma' - >>> cleanpath('/Users//smith/folder1/file1.txt') - '/Users/smith/folder1/file1.txt' - >>> cleanpath('C:/temp/some_dir/', '/some_folder/') - 'C:/temp/some_dir/some_folder' - >>> cleanpath('/Applications', '/\\some_app') - '/Applications/some_app' - >>> cleanpath('D:', 'MIM_Files') - 'D:/MIM_Files' - >>> cleanpath('D:\\\\', 'MIM_Files') - 'D:/MIM_Files' - >>> cleanpath('') - '' - >>> cleanpath('', 'blah', 'blah') - 'blah/blah' - >>> cleanpath('/path/to/dir/') - '/path/to/dir' - >>> cleanpath('/path/./to/../file.txt') - '/path/file.txt' - """ - # Add slash if first argument is a drive - # (os.path.join will not add one in this case) - if path.endswith(":"): - path += "/" - # Must strip any leading slashes from extra bits - extras = [] - for ext in extra: - extras.append(ext.lstrip("/\\")) - result = os.path.join(path, *extras) - if not result: - return "" - return os.path.normpath(result).replace("\\", "/") - - -def fileext(filepath: str): - """Return extension of given file path without the dot and in lower case. - Examples: - >>> fileext('c:/temp/file.txt') - 'txt' - >>> fileext('dir/another_dir/file.PNG') - 'png' - >>> fileext('dir/another_dir') - '' - >>> fileext('dir/another.dir/folder') - '' - >>> fileext('file.backup.tar.gz') - 'gz' - """ - filename = os.path.basename(filepath) - if "." not in filename: - return "" - return os.path.splitext(filename)[-1].strip(".").lower() From d04aa2381a7c926a66a700e0de8ea60083ed2f98 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Wed, 17 Jun 2026 14:22:31 -0500 Subject: [PATCH 23/37] Rename `medm` directory to `flowam` --- python/tk_multi_loader/api/manager.py | 2 +- python/tk_multi_loader/build_asset_dialog.py | 4 ++-- python/tk_multi_loader/build_template_dialog.py | 4 ++-- python/tk_multi_loader/dialog.py | 2 +- python/tk_multi_loader/{medm => flowam}/__init__.py | 0 python/tk_multi_loader/{medm => flowam}/create.py | 11 ++++++----- .../tk_multi_loader/{medm => flowam}/entity_model.py | 0 python/tk_multi_loader/{medm => flowam}/file.py | 0 .../{medm => flowam}/flowam_actions.py | 0 .../{medm => flowam}/latestpublish_model.py | 0 .../{medm => flowam}/publishhistory_model.py | 0 python/tk_multi_loader/{medm => flowam}/reference.py | 0 .../tk_multi_loader/{medm => flowam}/shared_cache.py | 0 .../{medm => flowam}/template_queries.py | 0 .../{medm => flowam}/thumbnail_service.py | 0 python/tk_multi_loader/{medm => flowam}/utils.py | 0 16 files changed, 12 insertions(+), 11 deletions(-) rename python/tk_multi_loader/{medm => flowam}/__init__.py (100%) rename python/tk_multi_loader/{medm => flowam}/create.py (98%) rename python/tk_multi_loader/{medm => flowam}/entity_model.py (100%) rename python/tk_multi_loader/{medm => flowam}/file.py (100%) rename python/tk_multi_loader/{medm => flowam}/flowam_actions.py (100%) rename python/tk_multi_loader/{medm => flowam}/latestpublish_model.py (100%) rename python/tk_multi_loader/{medm => flowam}/publishhistory_model.py (100%) rename python/tk_multi_loader/{medm => flowam}/reference.py (100%) rename python/tk_multi_loader/{medm => flowam}/shared_cache.py (100%) rename python/tk_multi_loader/{medm => flowam}/template_queries.py (100%) rename python/tk_multi_loader/{medm => flowam}/thumbnail_service.py (100%) rename python/tk_multi_loader/{medm => flowam}/utils.py (100%) diff --git a/python/tk_multi_loader/api/manager.py b/python/tk_multi_loader/api/manager.py index 59f2d1df..81a280a4 100644 --- a/python/tk_multi_loader/api/manager.py +++ b/python/tk_multi_loader/api/manager.py @@ -354,6 +354,6 @@ def _fix_timestamp(sg_data): def get_flowam_actions_instance(self) -> "FlowAMActions | None": """ """ if sgtk.platform.current_bundle().get_setting("enable_flowam", False): - from ..medm import FlowAMActions + from ..flowam import FlowAMActions return FlowAMActions() diff --git a/python/tk_multi_loader/build_asset_dialog.py b/python/tk_multi_loader/build_asset_dialog.py index fc5d4074..1a2a12e3 100644 --- a/python/tk_multi_loader/build_asset_dialog.py +++ b/python/tk_multi_loader/build_asset_dialog.py @@ -15,8 +15,8 @@ from tank_vendor.flow_integration_sdk.exceptions import FlowError from tank_vendor.flow_integration_sdk.objects import FlowProject -from .medm.template_queries import get_template_pipeline_steps, get_templates -from .medm.create import CreateMode, get_template_source_path +from .flowam.template_queries import get_template_pipeline_steps, get_templates +from .flowam.create import CreateMode, get_template_source_path from .ui.build_asset_dialog import Ui_BuildAssetDialog # Toolkit logger diff --git a/python/tk_multi_loader/build_template_dialog.py b/python/tk_multi_loader/build_template_dialog.py index 34b6c127..59df1b04 100644 --- a/python/tk_multi_loader/build_template_dialog.py +++ b/python/tk_multi_loader/build_template_dialog.py @@ -15,8 +15,8 @@ from tank_vendor.flow_integration_sdk.exceptions import FlowError from tank_vendor.flow_integration_sdk.objects import FlowProject -from .medm.template_queries import find_template_pipeline_step, get_templates -from .medm.create import CreateMode +from .flowam.template_queries import find_template_pipeline_step, get_templates +from .flowam.create import CreateMode from .ui.build_template_dialog import Ui_BuildTemplateDialog # Toolkit logger diff --git a/python/tk_multi_loader/dialog.py b/python/tk_multi_loader/dialog.py index 069850cc..94292c82 100644 --- a/python/tk_multi_loader/dialog.py +++ b/python/tk_multi_loader/dialog.py @@ -26,7 +26,7 @@ from .delegate_publish_thumb import SgPublishThumbDelegate from .framework_qtwidgets import ShotgunFilterMenu from .loader_action_manager import LoaderActionManager -from .medm import ( +from .flowam import ( MedmEntityModel, MedmLatestPublishModel, MedmPublishHistoryModel, diff --git a/python/tk_multi_loader/medm/__init__.py b/python/tk_multi_loader/flowam/__init__.py similarity index 100% rename from python/tk_multi_loader/medm/__init__.py rename to python/tk_multi_loader/flowam/__init__.py diff --git a/python/tk_multi_loader/medm/create.py b/python/tk_multi_loader/flowam/create.py similarity index 98% rename from python/tk_multi_loader/medm/create.py rename to python/tk_multi_loader/flowam/create.py index 7fe23c46..6f8875db 100644 --- a/python/tk_multi_loader/medm/create.py +++ b/python/tk_multi_loader/flowam/create.py @@ -23,7 +23,8 @@ from typing import Callable import sgtk -from sgtk.flowam import create, utils +from sgtk.flowam import create +from sgtk.flowam import utils as flowam_utils from tank_vendor.flow_integration_sdk import ( exceptions, globals, @@ -50,7 +51,7 @@ class CreateMode(Enum): @dataclass -class CreateInputs(utils.BaseInputs): +class CreateInputs(flowam_utils.BaseInputs): """Convenience structure to hold create inputs and allow them to be passed easily between helper functions. """ @@ -132,7 +133,7 @@ def validate(self): @dataclass -class CreateTemplateInputs(utils.BaseInputs): +class CreateTemplateInputs(flowam_utils.BaseInputs): """Convenience structure to hold create inputs and allow them to be passed easily between helper functions. """ @@ -427,7 +428,7 @@ def _create_template_hierarchy(inputs: CreateTemplateInputs) -> objects.FlowAsse folder = publish.publish_new_asset( name=TEMPLATE_FOLDER, parent_id=project.id, - components=utils.create_components_for_publish( + components=flowam_utils.create_components_for_publish( type_ids=[globals.FOLDER_TYPE_ID], ), description=desc, @@ -442,7 +443,7 @@ def _create_template_hierarchy(inputs: CreateTemplateInputs) -> objects.FlowAsse pipeline_step = publish.publish_new_asset( name=sg_pipeline_step, parent_id=folder.id, - components=utils.create_components_for_publish( + components=flowam_utils.create_components_for_publish( type_ids=[pipeline_step_type_id], ), ) diff --git a/python/tk_multi_loader/medm/entity_model.py b/python/tk_multi_loader/flowam/entity_model.py similarity index 100% rename from python/tk_multi_loader/medm/entity_model.py rename to python/tk_multi_loader/flowam/entity_model.py diff --git a/python/tk_multi_loader/medm/file.py b/python/tk_multi_loader/flowam/file.py similarity index 100% rename from python/tk_multi_loader/medm/file.py rename to python/tk_multi_loader/flowam/file.py diff --git a/python/tk_multi_loader/medm/flowam_actions.py b/python/tk_multi_loader/flowam/flowam_actions.py similarity index 100% rename from python/tk_multi_loader/medm/flowam_actions.py rename to python/tk_multi_loader/flowam/flowam_actions.py diff --git a/python/tk_multi_loader/medm/latestpublish_model.py b/python/tk_multi_loader/flowam/latestpublish_model.py similarity index 100% rename from python/tk_multi_loader/medm/latestpublish_model.py rename to python/tk_multi_loader/flowam/latestpublish_model.py diff --git a/python/tk_multi_loader/medm/publishhistory_model.py b/python/tk_multi_loader/flowam/publishhistory_model.py similarity index 100% rename from python/tk_multi_loader/medm/publishhistory_model.py rename to python/tk_multi_loader/flowam/publishhistory_model.py diff --git a/python/tk_multi_loader/medm/reference.py b/python/tk_multi_loader/flowam/reference.py similarity index 100% rename from python/tk_multi_loader/medm/reference.py rename to python/tk_multi_loader/flowam/reference.py diff --git a/python/tk_multi_loader/medm/shared_cache.py b/python/tk_multi_loader/flowam/shared_cache.py similarity index 100% rename from python/tk_multi_loader/medm/shared_cache.py rename to python/tk_multi_loader/flowam/shared_cache.py diff --git a/python/tk_multi_loader/medm/template_queries.py b/python/tk_multi_loader/flowam/template_queries.py similarity index 100% rename from python/tk_multi_loader/medm/template_queries.py rename to python/tk_multi_loader/flowam/template_queries.py diff --git a/python/tk_multi_loader/medm/thumbnail_service.py b/python/tk_multi_loader/flowam/thumbnail_service.py similarity index 100% rename from python/tk_multi_loader/medm/thumbnail_service.py rename to python/tk_multi_loader/flowam/thumbnail_service.py diff --git a/python/tk_multi_loader/medm/utils.py b/python/tk_multi_loader/flowam/utils.py similarity index 100% rename from python/tk_multi_loader/medm/utils.py rename to python/tk_multi_loader/flowam/utils.py From c489bc455d7c35fab4b3c9c0cf7ffbc06d3b9234 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Thu, 18 Jun 2026 09:34:49 -0500 Subject: [PATCH 24/37] Removed `enable_flowam` setting --- hooks/tk-desktop_actions.py | 3 +-- hooks/tk-houdini_actions.py | 6 ++---- hooks/tk-maya_actions.py | 6 ++---- hooks/tk-nuke_actions.py | 3 +-- info.yml | 7 ------- python/tk_multi_loader/api/manager.py | 2 +- python/tk_multi_loader/dialog.py | 11 +++++------ python/tk_multi_loader/flowam/__init__.py | 10 ++++------ 8 files changed, 16 insertions(+), 32 deletions(-) diff --git a/hooks/tk-desktop_actions.py b/hooks/tk-desktop_actions.py index dcda64c3..132de424 100644 --- a/hooks/tk-desktop_actions.py +++ b/hooks/tk-desktop_actions.py @@ -77,8 +77,7 @@ def generate_actions( action_instances = [] - enable_flowam = app.get_setting("enable_flowam", False) - if enable_flowam: + if self.parent.context.flow_project_id: flowam_actions = kwargs.get("flowam_actions") if not flowam_actions: raise Exception( diff --git a/hooks/tk-houdini_actions.py b/hooks/tk-houdini_actions.py index 102e6a4c..c2ad0f6c 100644 --- a/hooks/tk-houdini_actions.py +++ b/hooks/tk-houdini_actions.py @@ -103,8 +103,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): # ----------------------- # FlowAM specific actions # ----------------------- - enable_flowam = app.get_setting("enable_flowam", False) - if enable_flowam: + if self.parent.context.flow_project_id: flowam_actions = kwargs.get("flowam_actions") if not flowam_actions: raise Exception( @@ -250,8 +249,7 @@ def execute_action(self, name, params, sg_publish_data, **kwargs): # ----------------------- # FlowAM specific actions # ----------------------- - enable_flowam = app.get_setting("enable_flowam", False) - if enable_flowam: + if self.parent.context.flow_project_id: flowam_actions = kwargs.get("flowam_actions") if name == "open": diff --git a/hooks/tk-maya_actions.py b/hooks/tk-maya_actions.py index fe3fb597..dc0aef2b 100644 --- a/hooks/tk-maya_actions.py +++ b/hooks/tk-maya_actions.py @@ -130,8 +130,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): # ----------------------- # FlowAM specific actions # ----------------------- - enable_flowam = app.get_setting("enable_flowam", False) - if enable_flowam: + if self.parent.context.flow_project_id: flowam_actions = kwargs.get("flowam_actions") if not flowam_actions: raise Exception( @@ -298,8 +297,7 @@ def execute_action(self, name, params, sg_publish_data, **kwargs): # ----------------------- # FlowAM specific actions # ----------------------- - enable_flowam = app.get_setting("enable_flowam", False) - if enable_flowam: + if self.parent.context.flow_project_id: flowam_actions = kwargs.get("flowam_actions") if name == "reference_am": diff --git a/hooks/tk-nuke_actions.py b/hooks/tk-nuke_actions.py index 125ba5b0..fb4fbfba 100644 --- a/hooks/tk-nuke_actions.py +++ b/hooks/tk-nuke_actions.py @@ -116,8 +116,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): # ----------------------- # FlowAM specific actions # ----------------------- - enable_flowam = app.get_setting("enable_flowam", False) - if enable_flowam: + if self.parent.context.flow_project_id: flowam_actions = kwargs.get("flowam_actions") if not flowam_actions: raise Exception( diff --git a/info.yml b/info.yml index 4634cb89..4d6efaca 100644 --- a/info.yml +++ b/info.yml @@ -33,13 +33,6 @@ configuration: Published File Types). The legacy Published File Type filter widget cannot be used in combination with the Filter menu. - enable_flowam: - type: bool - default_value: false - description: Set to True to use Flow Asset Management data instead of Shotgun - data. When enabled, the loader fetches publish data from the Flow Asset - Management system. - # hooks actions_hook: type: hook diff --git a/python/tk_multi_loader/api/manager.py b/python/tk_multi_loader/api/manager.py index 81a280a4..a2594721 100644 --- a/python/tk_multi_loader/api/manager.py +++ b/python/tk_multi_loader/api/manager.py @@ -353,7 +353,7 @@ def _fix_timestamp(sg_data): def get_flowam_actions_instance(self) -> "FlowAMActions | None": """ """ - if sgtk.platform.current_bundle().get_setting("enable_flowam", False): + if sgtk.platform.current_bundle().context.flow_project_id: from ..flowam import FlowAMActions return FlowAMActions() diff --git a/python/tk_multi_loader/dialog.py b/python/tk_multi_loader/dialog.py index 94292c82..b26efce9 100644 --- a/python/tk_multi_loader/dialog.py +++ b/python/tk_multi_loader/dialog.py @@ -97,7 +97,7 @@ def __init__(self, action_manager, parent=None): # Hold a reference to the current animation to prevent GC mid-run self._current_animation = None - # FlowAM tree view - only created when enable_flowam is enabled + # FlowAM tree view - only created when FlowAM is enabled self._medm_tree_view = None # The loader app can be invoked from other applications with a custom @@ -166,11 +166,11 @@ def __init__(self, action_manager, parent=None): self._publish_history_model = SgPublishHistoryModel(self, self._task_manager) - # FlowAM objects are only instantiated when enable_flowam is enabled. + # FlowAM objects are only instantiated when enabled. self._medm_cache = None self._medm_thumbnail_service = None self._medm_history_model = None - if sgtk.platform.current_bundle().get_setting("enable_flowam", False): + if sgtk.platform.current_bundle().context.flow_project_id: self._medm_cache = MedmSharedCache() self._medm_thumbnail_service = MedmThumbnailService(self._medm_cache, self) @@ -374,14 +374,13 @@ def __init__(self, action_manager, parent=None): # Set up filtering app = sgtk.platform.current_bundle() - enable_flowam = app.get_setting("enable_flowam", False) if app.get_setting("use_legacy_published_file_type_filter", False): # Hide the Filter menu button. # The legacy filter functionality is always set up, since the filter menu still # requires some of that functionality. self._filter_menu = None self.ui.filter_menu_btn.hide() - elif enable_flowam: + elif app.context.flow_project_id: # Disable filter menu for Flow Asset Management mode - it expects ShotgunModel data self._filter_menu = None self.ui.filter_menu_btn.hide() @@ -427,7 +426,7 @@ def __init__(self, action_manager, parent=None): self._load_entity_presets() # Set up the FlowAM tree panel when Flow Asset Management is enabled - if enable_flowam: + if app.context.flow_project_id: self._setup_medm_tree_panel() ################################################# diff --git a/python/tk_multi_loader/flowam/__init__.py b/python/tk_multi_loader/flowam/__init__.py index 8d83bcc9..50aa667f 100644 --- a/python/tk_multi_loader/flowam/__init__.py +++ b/python/tk_multi_loader/flowam/__init__.py @@ -8,13 +8,11 @@ # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Shotgun Software Inc. -"""FlowAM integration models for the Loader app. +"""FlowAM integration for the Loader app. -This package provides Qt models that back the loader when ``enable_flowam`` -is enabled in the app configuration. All models share a single -:class:`~medm.shared_cache.MedmSharedCache` and -:class:`~medm.thumbnail_service.MedmThumbnailService` instance injected by -the dialog at construction time. +This package provides drop-in replacements for the standard Shotgun-based +Loader models and actions, backed by Flow Asset Management (FlowAM) instead +of the ShotGrid REST API. """ from .entity_model import MedmEntityModel From 97c5e03b2ca90a49255157904593bc19ca0e4842 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Thu, 18 Jun 2026 10:05:38 -0500 Subject: [PATCH 25/37] Get rid of `FlowAMActions` dependency injection and use app level module --- app.py | 15 +++++++++++++++ hooks/tk-3dsmaxplus_actions.py | 6 +++--- hooks/tk-desktop_actions.py | 17 +++++------------ hooks/tk-flame_actions.py | 6 +++--- hooks/tk-houdini_actions.py | 17 ++++++----------- hooks/tk-mari_actions.py | 6 +++--- hooks/tk-maya_actions.py | 17 ++++++----------- hooks/tk-motionbuilder_actions.py | 6 +++--- hooks/tk-nuke_actions.py | 17 ++++++----------- hooks/tk-photoshop_actions.py | 6 +++--- hooks/tk-photoshopcc_actions.py | 6 +++--- hooks/tk-shell_actions.py | 6 +++--- python/tk_multi_loader/api/manager.py | 11 ----------- python/tk_multi_loader/dialog.py | 1 - python/tk_multi_loader/loader_action_manager.py | 11 ----------- 15 files changed, 59 insertions(+), 89 deletions(-) diff --git a/app.py b/app.py index af39b3a4..6b5dd717 100644 --- a/app.py +++ b/app.py @@ -12,6 +12,8 @@ A loader application that lets you add new items to the scene. """ +from types import ModuleType + import sgtk import os @@ -28,6 +30,7 @@ def init_app(self): return tk_multi_loader = self.import_module("tk_multi_loader") + self._flowam = tk_multi_loader.flowam # the manager class provides the interface for loading. We store a # reference to it to enable the create_loader_action_manager method exposed on @@ -95,3 +98,15 @@ def create_loader_manager(self, bundle=None): :returns: A :class:`tk_multi_loader.LoaderManager` instance """ return self._manager_class(bundle or self) + + @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 Loader models and actions, + backed by Flow Asset Management (FlowAM) instead of the ShotGrid REST API. + + :returns: The FlowAM integration module for this app + :rtype: :mod:`tk_multi_loader.flowam` + """ + return self._flowam diff --git a/hooks/tk-3dsmaxplus_actions.py b/hooks/tk-3dsmaxplus_actions.py index 7903713f..33bd40fb 100644 --- a/hooks/tk-3dsmaxplus_actions.py +++ b/hooks/tk-3dsmaxplus_actions.py @@ -24,7 +24,7 @@ class MaxActions(HookBaseClass): ############################################################################################################## # public interface - to be overridden by deriving classes - def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): + def generate_actions(self, sg_publish_data, actions, ui_area): """ Returns a list of action instances for a particular publish. This method is called each time a user clicks a publish somewhere in the UI. @@ -101,7 +101,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): return action_instances - def execute_multiple_actions(self, actions, **kwargs): + def execute_multiple_actions(self, actions): """ Executes the specified action on a list of items. @@ -132,7 +132,7 @@ def execute_multiple_actions(self, actions, **kwargs): params = single_action["params"] self.execute_action(name, params, sg_publish_data) - def execute_action(self, name, params, sg_publish_data, **kwargs): + def execute_action(self, name, params, sg_publish_data): """ Execute a given action. The data sent to this be method will represent one of the actions enumerated by the generate_actions method. diff --git a/hooks/tk-desktop_actions.py b/hooks/tk-desktop_actions.py index 132de424..f773393a 100644 --- a/hooks/tk-desktop_actions.py +++ b/hooks/tk-desktop_actions.py @@ -31,7 +31,6 @@ def generate_actions( sg_publish_data: dict, actions: list, ui_area: str, - **kwargs, ) -> list: """ Return a list of action instances for a particular publish. @@ -77,13 +76,8 @@ def generate_actions( action_instances = [] - if self.parent.context.flow_project_id: - flowam_actions = kwargs.get("flowam_actions") - if not flowam_actions: - raise Exception( - "FlowAM is enabled but no Asset Management base object was passed to the action hook. " - "FlowAM specific actions will not be generated." - ) + if app.context.flow_project_id: + flowam_actions = app.flowam.FlowAMActions() if "download" in actions and sg_publish_data.get("type") == "PublishedFile": version_number = sg_publish_data.get("version_number") @@ -145,7 +139,7 @@ def generate_actions( return action_instances - def execute_multiple_actions(self, actions: list, **kwargs) -> None: + def execute_multiple_actions(self, actions: list) -> None: """ Executes the specified action on a list of items. @@ -178,14 +172,13 @@ def execute_multiple_actions(self, actions: list, **kwargs) -> None: name = single_action["name"] sg_publish_data = single_action["sg_publish_data"] params = single_action["params"] - self.execute_action(name, params, sg_publish_data, **kwargs) + self.execute_action(name, params, sg_publish_data) def execute_action( self, name: str, params: Any, sg_publish_data: dict, - **kwargs, ) -> None: """ Print out all actions. The data sent to this be method will @@ -201,7 +194,7 @@ def execute_action( "Execute action called for action %s. " "Parameters: %s. Publish Data: %s" % (name, params, sg_publish_data) ) - flowam_actions = kwargs.get("flowam_actions") + flowam_actions = app.flowam.FlowAMActions() if name == "create_generic_asset": # Right click a task the left panel diff --git a/hooks/tk-flame_actions.py b/hooks/tk-flame_actions.py index a75bcea1..8a6da4ab 100644 --- a/hooks/tk-flame_actions.py +++ b/hooks/tk-flame_actions.py @@ -44,7 +44,7 @@ class FlameActionError(Exception): class FlameActions(HookBaseClass): ############################################################################################################## # public interface - to be overridden by deriving classes - def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): + def generate_actions(self, sg_publish_data, actions, ui_area): """ Returns a list of action instances for a particular publish. This method is called each time a user clicks a publish somewhere in the UI. @@ -136,7 +136,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): return action_instances - def execute_multiple_actions(self, actions, **kwargs): + def execute_multiple_actions(self, actions): """ Executes the specified action on a list of items. @@ -167,7 +167,7 @@ def execute_multiple_actions(self, actions, **kwargs): params = single_action["params"] self.execute_action(name, params, sg_publish_data) - def execute_action(self, name, params, sg_publish_data, **kwargs): + def execute_action(self, name, params, sg_publish_data): """ Execute a given action. The data sent to this be method will represent one of the actions enumerated by the generate_actions method. diff --git a/hooks/tk-houdini_actions.py b/hooks/tk-houdini_actions.py index c2ad0f6c..5d0a25cc 100644 --- a/hooks/tk-houdini_actions.py +++ b/hooks/tk-houdini_actions.py @@ -26,7 +26,7 @@ class HoudiniActions(HookBaseClass): ############################################################################################################## # public interface - to be overridden by deriving classes - def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): + def generate_actions(self, sg_publish_data, actions, ui_area): """ Returns a list of action instances for a particular publish. This method is called each time a user clicks a publish somewhere in the UI. @@ -104,12 +104,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): # FlowAM specific actions # ----------------------- if self.parent.context.flow_project_id: - flowam_actions = kwargs.get("flowam_actions") - if not flowam_actions: - raise Exception( - "FlowAM is enabled but no Asset Management base object was passed to the action hook. " - "FlowAM specific actions will not be generated." - ) + flowam_actions = app.flowam.FlowAMActions() if "open" in actions and sg_publish_data.get("type") == "PublishedFile": if ( @@ -199,7 +194,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): return action_instances - def execute_multiple_actions(self, actions, **kwargs): + def execute_multiple_actions(self, actions): """ Executes the specified action on a list of items. @@ -228,9 +223,9 @@ def execute_multiple_actions(self, actions, **kwargs): name = single_action["name"] sg_publish_data = single_action["sg_publish_data"] params = single_action["params"] - self.execute_action(name, params, sg_publish_data, **kwargs) + self.execute_action(name, params, sg_publish_data) - def execute_action(self, name, params, sg_publish_data, **kwargs): + def execute_action(self, name, params, sg_publish_data): """ Execute a given action. The data sent to this be method will represent one of the actions enumerated by the generate_actions method. @@ -250,7 +245,7 @@ def execute_action(self, name, params, sg_publish_data, **kwargs): # FlowAM specific actions # ----------------------- if self.parent.context.flow_project_id: - flowam_actions = kwargs.get("flowam_actions") + flowam_actions = app.flowam.FlowAMActions() if name == "open": flowam_actions._do_open(sg_publish_data) diff --git a/hooks/tk-mari_actions.py b/hooks/tk-mari_actions.py index accd0ba0..64f44759 100644 --- a/hooks/tk-mari_actions.py +++ b/hooks/tk-mari_actions.py @@ -25,7 +25,7 @@ class MariActions(HookBaseClass): ############################################################################################################## # public interface - to be overridden by deriving classes - def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): + def generate_actions(self, sg_publish_data, actions, ui_area): """ Returns a list of action instances for a particular publish. This method is called each time a user clicks a publish somewhere in the UI. @@ -108,7 +108,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): return action_instances - def execute_multiple_actions(self, actions, **kwargs): + def execute_multiple_actions(self, actions): """ Executes the specified action on a list of items. @@ -139,7 +139,7 @@ def execute_multiple_actions(self, actions, **kwargs): params = single_action["params"] self.execute_action(name, params, sg_publish_data) - def execute_action(self, name, params, sg_publish_data, **kwargs): + def execute_action(self, name, params, sg_publish_data): """ Execute a given action. The data sent to this be method will represent one of the actions enumerated by the generate_actions method. diff --git a/hooks/tk-maya_actions.py b/hooks/tk-maya_actions.py index dc0aef2b..0c9c03e9 100644 --- a/hooks/tk-maya_actions.py +++ b/hooks/tk-maya_actions.py @@ -29,7 +29,7 @@ class MayaActions(HookBaseClass): ############################################################################################################## # public interface - to be overridden by deriving classes - def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): + def generate_actions(self, sg_publish_data, actions, ui_area): """ Returns a list of action instances for a particular publish. This method is called each time a user clicks a publish somewhere in the UI. @@ -131,12 +131,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): # FlowAM specific actions # ----------------------- if self.parent.context.flow_project_id: - flowam_actions = kwargs.get("flowam_actions") - if not flowam_actions: - raise Exception( - "FlowAM is enabled but no Asset Management base object was passed to the action hook. " - "FlowAM specific actions will not be generated." - ) + flowam_actions = app.flowam.FlowAMActions() if ( "open" in actions @@ -247,7 +242,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): return action_instances - def execute_multiple_actions(self, actions, **kwargs): + def execute_multiple_actions(self, actions): """ Executes the specified action on a list of items. @@ -276,9 +271,9 @@ def execute_multiple_actions(self, actions, **kwargs): name = single_action["name"] sg_publish_data = single_action["sg_publish_data"] params = single_action["params"] - self.execute_action(name, params, sg_publish_data, **kwargs) + self.execute_action(name, params, sg_publish_data) - def execute_action(self, name, params, sg_publish_data, **kwargs): + def execute_action(self, name, params, sg_publish_data): """ Execute a given action. The data sent to this be method will represent one of the actions enumerated by the generate_actions method. @@ -298,7 +293,7 @@ def execute_action(self, name, params, sg_publish_data, **kwargs): # FlowAM specific actions # ----------------------- if self.parent.context.flow_project_id: - flowam_actions = kwargs.get("flowam_actions") + flowam_actions = app.flowam.FlowAMActions() if name == "reference_am": flowam_actions._create_reference_am(sg_publish_data) diff --git a/hooks/tk-motionbuilder_actions.py b/hooks/tk-motionbuilder_actions.py index da28e656..990cec55 100644 --- a/hooks/tk-motionbuilder_actions.py +++ b/hooks/tk-motionbuilder_actions.py @@ -24,7 +24,7 @@ class MotionbuilderActions(HookBaseClass): ############################################################################################################## # public interface - to be overridden by deriving classes - def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): + def generate_actions(self, sg_publish_data, actions, ui_area): """ Returns a list of action instances for a particular publish. This method is called each time a user clicks a publish somewhere in the UI. @@ -81,7 +81,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): return action_instances - def execute_multiple_actions(self, actions, **kwargs): + def execute_multiple_actions(self, actions): """ Executes the specified action on a list of items. @@ -112,7 +112,7 @@ def execute_multiple_actions(self, actions, **kwargs): params = single_action["params"] self.execute_action(name, params, sg_publish_data) - def execute_action(self, name, params, sg_publish_data, **kwargs): + def execute_action(self, name, params, sg_publish_data): """ Execute a given action. The data sent to this be method will represent one of the actions enumerated by the generate_actions method. diff --git a/hooks/tk-nuke_actions.py b/hooks/tk-nuke_actions.py index fb4fbfba..ade21542 100644 --- a/hooks/tk-nuke_actions.py +++ b/hooks/tk-nuke_actions.py @@ -28,7 +28,7 @@ class NukeActions(HookBaseClass): ############################################################################################################## # public interface - to be overridden by deriving classes - def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): + def generate_actions(self, sg_publish_data, actions, ui_area): """ Returns a list of action instances for a particular publish. This method is called each time a user clicks a publish somewhere in the UI. @@ -117,12 +117,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): # FlowAM specific actions # ----------------------- if self.parent.context.flow_project_id: - flowam_actions = kwargs.get("flowam_actions") - if not flowam_actions: - raise Exception( - "FlowAM is enabled but no Asset Management base object was passed to the action hook. " - "FlowAM specific actions will not be generated." - ) + flowam_actions = app.flowam.FlowAMActions() if "build_new_script" in actions: action_instances.append( @@ -207,7 +202,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): return action_instances - def execute_multiple_actions(self, actions, **kwargs): + def execute_multiple_actions(self, actions): """ Executes the specified action on a list of items. @@ -236,9 +231,9 @@ def execute_multiple_actions(self, actions, **kwargs): name = single_action["name"] sg_publish_data = single_action["sg_publish_data"] params = single_action["params"] - self.execute_action(name, params, sg_publish_data, **kwargs) + self.execute_action(name, params, sg_publish_data) - def execute_action(self, name, params, sg_publish_data, **kwargs): + def execute_action(self, name, params, sg_publish_data): """ Execute a given action. The data sent to this be method will represent one of the actions enumerated by the generate_actions method. @@ -260,7 +255,7 @@ def execute_action(self, name, params, sg_publish_data, **kwargs): # ----------------------- use_medm_data = app.get_setting("use_medm_data", False) if use_medm_data: - flowam_actions = kwargs.get("flowam_actions") + flowam_actions = app.flowam.FlowAMActions() if name == "build_new_script": flowam_actions._build_new_scene(sg_publish_data) diff --git a/hooks/tk-photoshop_actions.py b/hooks/tk-photoshop_actions.py index 328e060c..c36a361a 100644 --- a/hooks/tk-photoshop_actions.py +++ b/hooks/tk-photoshop_actions.py @@ -34,7 +34,7 @@ class PhotoshopActions(HookBaseClass): ############################################################################################################## # public interface - to be overridden by deriving classes - def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): + def generate_actions(self, sg_publish_data, actions, ui_area): """ Returns a list of action instances for a particular publish. This method is called each time a user clicks a publish somewhere in the UI. @@ -101,7 +101,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): return action_instances - def execute_multiple_actions(self, actions, **kwargs): + def execute_multiple_actions(self, actions): """ Executes the specified action on a list of items. @@ -132,7 +132,7 @@ def execute_multiple_actions(self, actions, **kwargs): params = single_action["params"] self.execute_action(name, params, sg_publish_data) - def execute_action(self, name, params, sg_publish_data, **kwargs): + def execute_action(self, name, params, sg_publish_data): """ Execute a given action. The data sent to this be method will represent one of the actions enumerated by the generate_actions method. diff --git a/hooks/tk-photoshopcc_actions.py b/hooks/tk-photoshopcc_actions.py index 36c4fe8d..c3725d68 100644 --- a/hooks/tk-photoshopcc_actions.py +++ b/hooks/tk-photoshopcc_actions.py @@ -29,7 +29,7 @@ class PhotoshopActions(HookBaseClass): ############################################################################################################## # public interface - to be overridden by deriving classes - def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): + def generate_actions(self, sg_publish_data, actions, ui_area): """ Returns a list of action instances for a particular publish. This method is called each time a user clicks a publish somewhere in the UI. @@ -96,7 +96,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): return action_instances - def execute_multiple_actions(self, actions, **kwargs): + def execute_multiple_actions(self, actions): """ Executes the specified action on a list of items. @@ -129,7 +129,7 @@ def execute_multiple_actions(self, actions, **kwargs): params = single_action["params"] self.execute_action(name, params, sg_publish_data) - def execute_action(self, name, params, sg_publish_data, **kwargs): + def execute_action(self, name, params, sg_publish_data): """ Execute a given action. The data sent to this be method will represent one of the actions enumerated by the generate_actions method. diff --git a/hooks/tk-shell_actions.py b/hooks/tk-shell_actions.py index dca3a5db..0471a046 100644 --- a/hooks/tk-shell_actions.py +++ b/hooks/tk-shell_actions.py @@ -23,7 +23,7 @@ class ShellActions(HookBaseClass): Stub implementation of the shell actions, used for testing. """ - def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): + def generate_actions(self, sg_publish_data, actions, ui_area): """ Return a list of action instances for a particular publish. This method is called each time a user clicks a publish somewhere in the UI. @@ -111,7 +111,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area, **kwargs): ) return action_instances - def execute_multiple_actions(self, actions, **kwargs): + def execute_multiple_actions(self, actions): """ Executes the specified action on a list of items. @@ -146,7 +146,7 @@ def execute_multiple_actions(self, actions, **kwargs): params = single_action["params"] self.execute_action(name, params, sg_publish_data) - def execute_action(self, name, params, sg_publish_data, **kwargs): + def execute_action(self, name, params, sg_publish_data): """ Print out all actions. The data sent to this be method will represent one of the actions enumerated by the generate_actions method. diff --git a/python/tk_multi_loader/api/manager.py b/python/tk_multi_loader/api/manager.py index a2594721..c6665a85 100644 --- a/python/tk_multi_loader/api/manager.py +++ b/python/tk_multi_loader/api/manager.py @@ -130,7 +130,6 @@ def get_actions_for_publish(self, sg_data, ui_area): sg_publish_data=sg_data, actions=actions, ui_area=ui_area_str, - flowam_actions=self.get_flowam_actions_instance(), ) except Exception: self._logger.exception("Could not execute generate_actions hook.") @@ -244,7 +243,6 @@ def execute_action(self, sg_data, action): name=action["name"], params=action["params"], sg_publish_data=sg_data, - flowam_actions=self.get_flowam_actions_instance(), ) except Exception as e: self._logger.exception( @@ -268,7 +266,6 @@ def execute_multiple_actions(self, actions): "actions_hook", "execute_multiple_actions", actions=actions, - flowam_actions=self.get_flowam_actions_instance(), ) except Exception as e: self._logger.exception( @@ -310,7 +307,6 @@ def get_actions_for_entity(self, sg_data): sg_publish_data=sg_data, actions=actions, ui_area="main", - flowam_actions=self.get_flowam_actions_instance(), ) # folder options only found in main ui area except Exception: self._logger.exception("Could not execute generate_actions hook.") @@ -350,10 +346,3 @@ def _fix_timestamp(sg_data): unix_timestamp, shotgun_api3.sg_timezone.LocalTimezone() ) sg_data["created_at"] = sg_timestamp - - def get_flowam_actions_instance(self) -> "FlowAMActions | None": - """ """ - if sgtk.platform.current_bundle().context.flow_project_id: - from ..flowam import FlowAMActions - - return FlowAMActions() diff --git a/python/tk_multi_loader/dialog.py b/python/tk_multi_loader/dialog.py index b26efce9..c2bac601 100644 --- a/python/tk_multi_loader/dialog.py +++ b/python/tk_multi_loader/dialog.py @@ -1993,7 +1993,6 @@ def on_action_click(act): name=act["name"], params=act["params"], sg_publish_data=sg_data, - flowam_actions=self._action_manager.get_flowam_actions_instance(), ) action = QtGui.QAction(entity_action["caption"], view) diff --git a/python/tk_multi_loader/loader_action_manager.py b/python/tk_multi_loader/loader_action_manager.py index 8dcf8422..8ddb00a4 100644 --- a/python/tk_multi_loader/loader_action_manager.py +++ b/python/tk_multi_loader/loader_action_manager.py @@ -213,17 +213,6 @@ def get_actions_for_folder(self, sg_data): def get_actions_for_entity(self, sg_data): return self._loader_manager.get_actions_for_entity(sg_data) - def get_flowam_actions_instance(self) -> "FlowAMActions | None": - """ - Returns the base object for asset management actions, if available. - - This is used to provide context for actions related to asset management, - such as showing details in Shotgun or Media Center. - - :returns: The base object for asset management actions, or None if not available. - """ - return self._loader_manager.get_flowam_actions_instance() - ######################################################################################## # callbacks From 43df636f74feafacec8f14c5ad62b5c1e0db99f8 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Thu, 18 Jun 2026 13:14:05 -0500 Subject: [PATCH 26/37] Update core new definitions --- python/tk_multi_loader/build_asset_dialog.py | 3 +- .../tk_multi_loader/build_template_dialog.py | 2 +- python/tk_multi_loader/constants.py | 7 ---- python/tk_multi_loader/flowam/create.py | 39 +++++++------------ .../tk_multi_loader/flowam/flowam_actions.py | 2 +- .../flowam/template_queries.py | 3 +- python/tk_multi_loader/flowam/utils.py | 4 +- 7 files changed, 21 insertions(+), 39 deletions(-) diff --git a/python/tk_multi_loader/build_asset_dialog.py b/python/tk_multi_loader/build_asset_dialog.py index 1a2a12e3..735efd22 100644 --- a/python/tk_multi_loader/build_asset_dialog.py +++ b/python/tk_multi_loader/build_asset_dialog.py @@ -12,11 +12,12 @@ import sgtk from sgtk.platform.qt import QtGui +from sgtk.flowam.create import CreateMode from tank_vendor.flow_integration_sdk.exceptions import FlowError from tank_vendor.flow_integration_sdk.objects import FlowProject from .flowam.template_queries import get_template_pipeline_steps, get_templates -from .flowam.create import CreateMode, get_template_source_path +from .flowam.create import get_template_source_path from .ui.build_asset_dialog import Ui_BuildAssetDialog # Toolkit logger diff --git a/python/tk_multi_loader/build_template_dialog.py b/python/tk_multi_loader/build_template_dialog.py index 59df1b04..16d838ca 100644 --- a/python/tk_multi_loader/build_template_dialog.py +++ b/python/tk_multi_loader/build_template_dialog.py @@ -12,11 +12,11 @@ import sgtk from sgtk.platform.qt import QtGui +from sgtk.flowam.create import CreateMode from tank_vendor.flow_integration_sdk.exceptions import FlowError from tank_vendor.flow_integration_sdk.objects import FlowProject from .flowam.template_queries import find_template_pipeline_step, get_templates -from .flowam.create import CreateMode from .ui.build_template_dialog import Ui_BuildTemplateDialog # Toolkit logger diff --git a/python/tk_multi_loader/constants.py b/python/tk_multi_loader/constants.py index 917b6c76..ca94e8ee 100644 --- a/python/tk_multi_loader/constants.py +++ b/python/tk_multi_loader/constants.py @@ -103,10 +103,3 @@ # FlowAM versions starts with 0 and FlowPT versions starts with 1. # This is used to identify a FlowAM draft version in the UI. DRAFT_VERSION_IDENTIFIER = -1 - -# Folder names -TEMPLATE_FOLDER = "Templates" - -# Schema types -CONTAINER_TYPE = "type.container" -TEMPLATE_TYPE = "type.template" diff --git a/python/tk_multi_loader/flowam/create.py b/python/tk_multi_loader/flowam/create.py index 6f8875db..b968dd65 100644 --- a/python/tk_multi_loader/flowam/create.py +++ b/python/tk_multi_loader/flowam/create.py @@ -35,21 +35,10 @@ utils, ) -from ..constants import TEMPLATE_FOLDER, TEMPLATE_TYPE - # --------------------------------- # Classes # --------------------------------- -class CreateMode(Enum): - """Enum of modes for creating a new asset.""" - - NEW = "new" #: Create a DCC asset from a new scene as the source. - CURRENT = "current" #: Create a DCC asset from the current scene as the source. - TEMPLATE = "template" #: Create a DCC asset from template scene as the source. - GENERIC = "generic" #: Create a generic asset from a specified source file. - - @dataclass class CreateInputs(flowam_utils.BaseInputs): """Convenience structure to hold create inputs and allow them to be @@ -71,7 +60,7 @@ class CreateInputs(flowam_utils.BaseInputs): description: str = "" #: Determines which initial source file to use to create the asset. #: See `CreateMode` enum for valid values. - create_mode: CreateMode = CreateMode.CURRENT + create_mode: create.CreateMode = create.CreateMode.CURRENT #: Relevant only in some modes. #: * TEMPLATE -> path to the template file to be used to build the new asset #: * GENERIC -> path to the source file to copy directly to asset @@ -107,10 +96,10 @@ def validate(self): msg = "Incomplete sg context provided. Must provide sg_pipeline_step." raise exceptions.CreateAssetError(data=self.asdict(), details=msg) # If create mode is TEMPLATE or GENERIC, we need a source path - if self.create_mode == CreateMode.TEMPLATE and not self.source_path: + if self.create_mode == create.CreateMode.TEMPLATE and not self.source_path: msg = "No template source path provided." raise exceptions.CreateAssetError(data=self.asdict(), details=msg) - if self.create_mode == CreateMode.GENERIC and not self.source_path: + if self.create_mode == create.CreateMode.GENERIC and not self.source_path: msg = "No source path provided for generic asset." raise exceptions.CreateAssetError(data=self.asdict(), details=msg) # If pipeline step is provided, we expect task_name to be provided as well @@ -119,7 +108,7 @@ def validate(self): raise exceptions.CreateAssetError(data=self.asdict(), details=msg) # prep_scene_callback is only applicable when create_mode is NEW or TEMPLATE if ( - self.create_mode == CreateMode.GENERIC + self.create_mode == create.CreateMode.GENERIC and self.prep_scene_callback is not None ): msg = "prep_scene_callback is not applicable when create_mode is GENERIC." @@ -149,7 +138,7 @@ class CreateTemplateInputs(flowam_utils.BaseInputs): #: Determines which initial source file to use to create the asset. #: See `CreateMode` enum for valid values. #: In the case of template creation, only NEW and CURRENT are applicable. - create_mode: CreateMode = CreateMode.CURRENT + create_mode: create.CreateMode = create.CreateMode.CURRENT def validate(self): """Check that input combinations are valid. @@ -174,8 +163,8 @@ def validate(self): ) # Create mode TEMPLATE and GENERIC are not applicable for templates. if ( - self.create_mode == CreateMode.TEMPLATE - or self.create_mode == CreateMode.GENERIC + self.create_mode == create.CreateMode.TEMPLATE + or self.create_mode == create.CreateMode.GENERIC ): msg = f"Invalid CreateMode provided for template creation: {self.create_mode}." raise exceptions.CreateAssetError(data=self.asdict(), details=msg) @@ -339,10 +328,10 @@ def _create_dcc_workfile_asset( with tempfile.TemporaryDirectory() as temp_dir: ext = utils.fileext(inputs.source_path) or flow_host().FILE_TYPES[0] temp_file = utils.cleanpath(temp_dir, f"{name}.{ext}") - if inputs.create_mode == CreateMode.NEW: + if inputs.create_mode == create.CreateMode.NEW: # Clear scene flow_host().new_scene() - elif inputs.create_mode == CreateMode.TEMPLATE: + elif inputs.create_mode == create.CreateMode.TEMPLATE: # Open the template file if not os.path.exists(inputs.source_path): msg = f"Template path does not exist: {inputs.source_path}" @@ -421,12 +410,12 @@ def _create_template_hierarchy(inputs: CreateTemplateInputs) -> objects.FlowAsse raise exceptions.CreateAssetError(data=inputs.asdict(), details=msg) from exc # Create top-level folder if it doesn't already exist in project - folder = project.find_child(TEMPLATE_FOLDER) + folder = project.find_child(create.TEMPLATE_FOLDER) if not folder: - app.log_info(f'Creating "{TEMPLATE_FOLDER}" folder...') + app.log_info(f'Creating "{create.TEMPLATE_FOLDER}" folder...') desc = "Folder for template assets." folder = publish.publish_new_asset( - name=TEMPLATE_FOLDER, + name=create.TEMPLATE_FOLDER, parent_id=project.id, components=flowam_utils.create_components_for_publish( type_ids=[globals.FOLDER_TYPE_ID], @@ -471,7 +460,7 @@ def _create_template_workfile_asset( # and template designation workfile_type = flow_host().WORKFILE_TYPE workfile_type_id = schema.get_schema_id(workfile_type) - template_type_id = schema.get_schema_id(TEMPLATE_TYPE) + template_type_id = schema.get_schema_id(create.TEMPLATE_TYPE) name = inputs.template_name @@ -480,7 +469,7 @@ def _create_template_workfile_asset( with tempfile.TemporaryDirectory() as temp_dir: ext = flow_host().FILE_TYPES[0] temp_file = utils.cleanpath(temp_dir, f"{name}.{ext}") - if inputs.create_mode == CreateMode.NEW: + if inputs.create_mode == create.CreateMode.NEW: # Clear scene flow_host().new_scene() app.log_info(f"Saving temp file to: {temp_file}") diff --git a/python/tk_multi_loader/flowam/flowam_actions.py b/python/tk_multi_loader/flowam/flowam_actions.py index 230cbf80..8dcadbfd 100644 --- a/python/tk_multi_loader/flowam/flowam_actions.py +++ b/python/tk_multi_loader/flowam/flowam_actions.py @@ -14,6 +14,7 @@ import sgtk from sgtk import TankError from sgtk.platform.qt import QtGui +from sgtk.flowam.create import CreateMode from tank_vendor.flow_integration_sdk import sandbox, exceptions from ..build_asset_dialog import BuildAssetDialog @@ -21,7 +22,6 @@ from ..constants import DRAFT_VERSION_IDENTIFIER from .create import ( CreateInputs, - CreateMode, CreateTemplateInputs, create_dcc_workfile, create_template_workfile, diff --git a/python/tk_multi_loader/flowam/template_queries.py b/python/tk_multi_loader/flowam/template_queries.py index 53e3ff0b..1966394d 100644 --- a/python/tk_multi_loader/flowam/template_queries.py +++ b/python/tk_multi_loader/flowam/template_queries.py @@ -15,10 +15,9 @@ from typing import Any, Optional import sgtk -from sgtk.flowam.create import PIPELINE_STEP_TYPE +from sgtk.flowam.create import PIPELINE_STEP_TYPE, TEMPLATE_FOLDER, TEMPLATE_TYPE from tank_vendor.flow_integration_sdk import objects, schema -from ..constants import TEMPLATE_FOLDER, TEMPLATE_TYPE logger = sgtk.platform.get_logger(__name__) diff --git a/python/tk_multi_loader/flowam/utils.py b/python/tk_multi_loader/flowam/utils.py index 2eecae92..b05e6d8a 100644 --- a/python/tk_multi_loader/flowam/utils.py +++ b/python/tk_multi_loader/flowam/utils.py @@ -20,9 +20,9 @@ from typing import Any, Dict, Optional, Tuple from tank_vendor.flow_integration_sdk import globals, schema -from sgtk.flowam.create import PIPELINE_STEP_TYPE +from sgtk.flowam.create import CONTAINER_TYPE, PIPELINE_STEP_TYPE -from ..constants import DRAFT_VERSION_IDENTIFIER, CONTAINER_TYPE +from ..constants import DRAFT_VERSION_IDENTIFIER def is_structural_asset(asset: Any) -> bool: From f5c1d84afb676ae5b4f2a761908ba493c9342fc1 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Thu, 18 Jun 2026 13:30:16 -0500 Subject: [PATCH 27/37] Fix format --- python/tk_multi_loader/flowam/template_queries.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/tk_multi_loader/flowam/template_queries.py b/python/tk_multi_loader/flowam/template_queries.py index 1966394d..3092d183 100644 --- a/python/tk_multi_loader/flowam/template_queries.py +++ b/python/tk_multi_loader/flowam/template_queries.py @@ -18,7 +18,6 @@ from sgtk.flowam.create import PIPELINE_STEP_TYPE, TEMPLATE_FOLDER, TEMPLATE_TYPE from tank_vendor.flow_integration_sdk import objects, schema - logger = sgtk.platform.get_logger(__name__) From 8e4a4af91e985a22b8bc8184327351148f4fee23 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Fri, 19 Jun 2026 09:26:58 -0500 Subject: [PATCH 28/37] Handle error --- python/tk_multi_loader/flowam/create.py | 2 +- .../tk_multi_loader/flowam/flowam_actions.py | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/python/tk_multi_loader/flowam/create.py b/python/tk_multi_loader/flowam/create.py index b968dd65..ca9ca93d 100644 --- a/python/tk_multi_loader/flowam/create.py +++ b/python/tk_multi_loader/flowam/create.py @@ -9,7 +9,7 @@ # not expressly granted therein are reserved by Shotgun Software Inc. """ -This module contains utilities for creating medm assets. Some of these functions +This module contains utilities for creating Flow AM assets. Some of these functions create assets only in sandbox, while others create and publish them straight to medm. """ diff --git a/python/tk_multi_loader/flowam/flowam_actions.py b/python/tk_multi_loader/flowam/flowam_actions.py index 8dcadbfd..5e52e897 100644 --- a/python/tk_multi_loader/flowam/flowam_actions.py +++ b/python/tk_multi_loader/flowam/flowam_actions.py @@ -185,7 +185,6 @@ def _on_build_scene_dialog_accepted( "Error", str(exc), ) - return def _prep_scene(self, sg_publish_data: dict) -> None: """ @@ -331,10 +330,20 @@ def _on_build_template_dialog_accepted( template_name=dialog.template, create_mode=dialog.mode, ) - draft_info = create_template_workfile(create_inputs) - self._app.log_debug( - f"Created a Template workfile with the draft_id: {draft_info.draft_id}" - ) + + try: + draft_info = create_template_workfile(create_inputs) + self._app.log_debug( + f"Created a Template workfile with the draft_id: {draft_info.draft_id}" + ) + except exceptions.CreateAssetError as exc: + self._app.log_error(f"Create asset failed: {exc}\nInput data: {exc.data}") + + QtGui.QMessageBox.critical( + parent_window, + "Error", + str(exc), + ) def _get_flowam_id(self) -> str: """ From fa1d044775c7f0e87c72af929983402bfa1a08e4 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Fri, 19 Jun 2026 09:29:08 -0500 Subject: [PATCH 29/37] Remove dead code --- hooks/tk-desktop_actions.py | 2 -- hooks/tk-shell_actions.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/hooks/tk-desktop_actions.py b/hooks/tk-desktop_actions.py index f773393a..b47d52b5 100644 --- a/hooks/tk-desktop_actions.py +++ b/hooks/tk-desktop_actions.py @@ -164,8 +164,6 @@ def execute_multiple_actions(self, actions: list) -> None: :param list actions: Action dictionaries. """ - app = self.parent - app.log_info("Executing action '%s' on the selection") # Helps to visually scope selections # Execute each action. for single_action in actions: diff --git a/hooks/tk-shell_actions.py b/hooks/tk-shell_actions.py index 0471a046..4b7848e4 100644 --- a/hooks/tk-shell_actions.py +++ b/hooks/tk-shell_actions.py @@ -136,8 +136,6 @@ def execute_multiple_actions(self, actions): :param list actions: Action dictionaries. """ - app = self.parent - app.log_info("Executing action '%s' on the selection") # Helps to visually scope selections # Execute each action. for single_action in actions: From 9becc41d9908ee8fdec6b4292eca8070bd8fe5cf Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Fri, 19 Jun 2026 12:00:55 -0500 Subject: [PATCH 30/37] Update references, rename variables --- hooks/tk-houdini_actions.py | 6 +-- hooks/tk-maya_actions.py | 6 +-- hooks/tk-nuke_actions.py | 6 +-- python/tk_multi_loader/flowam/file.py | 15 +------- .../tk_multi_loader/flowam/flowam_actions.py | 38 ++++++++++++------- 5 files changed, 34 insertions(+), 37 deletions(-) diff --git a/hooks/tk-houdini_actions.py b/hooks/tk-houdini_actions.py index 5d0a25cc..44a5984b 100644 --- a/hooks/tk-houdini_actions.py +++ b/hooks/tk-houdini_actions.py @@ -16,7 +16,7 @@ import re import sgtk -from tank_vendor.flow_integration_sdk.sandbox import is_local_draft, is_new_asset +from tank_vendor.flow_integration_sdk.sandbox import is_new_asset HookBaseClass = sgtk.get_hook_baseclass() @@ -108,7 +108,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area): if "open" in actions and sg_publish_data.get("type") == "PublishedFile": if ( - is_local_draft(sg_publish_data.get("sg_flow_revision_id")) + flowam_actions.is_local_draft_by_revision(sg_publish_data.get("sg_flow_revision_id")) or sg_publish_data.get( "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) @@ -143,7 +143,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area): if "discard_draft" in actions: draft_id = sg_publish_data.get("sg_flow_revision_id") - if is_local_draft( + if flowam_actions.is_local_draft_by_revision( sg_publish_data.get("sg_flow_revision_id") ) and is_new_asset(draft_id): action_instances.append( diff --git a/hooks/tk-maya_actions.py b/hooks/tk-maya_actions.py index 0c9c03e9..0a032da2 100644 --- a/hooks/tk-maya_actions.py +++ b/hooks/tk-maya_actions.py @@ -19,7 +19,7 @@ import maya.cmds as cmds import maya.mel as mel import sgtk -from tank_vendor.flow_integration_sdk.sandbox import is_local_draft, is_new_asset +from tank_vendor.flow_integration_sdk.sandbox import is_new_asset HookBaseClass = sgtk.get_hook_baseclass() @@ -137,7 +137,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area): "open" in actions and sg_publish_data.get("type") == "PublishedFile" and ( - is_local_draft(sg_publish_data.get("sg_flow_revision_id")) + flowam_actions.is_local_draft_by_revision(sg_publish_data.get("sg_flow_revision_id")) or sg_publish_data.get( "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) @@ -175,7 +175,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area): if "discard_draft" in actions: draft_id = sg_publish_data.get("sg_flow_revision_id") - if is_local_draft( + if flowam_actions.is_local_draft_by_revision( sg_publish_data.get("sg_flow_revision_id") ) and is_new_asset(draft_id): action_instances.append( diff --git a/hooks/tk-nuke_actions.py b/hooks/tk-nuke_actions.py index ade21542..d455a5bf 100644 --- a/hooks/tk-nuke_actions.py +++ b/hooks/tk-nuke_actions.py @@ -15,10 +15,8 @@ import glob import os import re -import sys import sgtk -from tank_vendor.flow_integration_sdk.sandbox import is_local_draft HookBaseClass = sgtk.get_hook_baseclass() @@ -142,7 +140,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area): # 1. Local drafts (version_number == -1 and is_local_draft) # 2. Published revisions (version_number > -1) if ( - is_local_draft(sg_publish_data.get("sg_flow_revision_id")) + flowam_actions.is_local_draft_by_revision(sg_publish_data.get("sg_flow_revision_id")) or sg_publish_data.get( "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) @@ -157,7 +155,7 @@ def generate_actions(self, sg_publish_data, actions, ui_area): } ) - if "discard_draft" in actions and is_local_draft( + if "discard_draft" in actions and flowam_actions.is_local_draft_by_revision( sg_publish_data.get("sg_flow_revision_id") ): action_instances.append( diff --git a/python/tk_multi_loader/flowam/file.py b/python/tk_multi_loader/flowam/file.py index 14cda491..cd9fcede 100644 --- a/python/tk_multi_loader/flowam/file.py +++ b/python/tk_multi_loader/flowam/file.py @@ -37,17 +37,6 @@ def __init__( self.directory = directory -class InvalidDraftError(exceptions.FlowError): - def __init__(self, *args, draft_id: str, **kwargs): - """ - Args: - draft_id: Id that uniquely identifies a draft in local sandbox. - """ - message = f'Draft id "{draft_id}" is invalid.' - super().__init__(message, *args, **kwargs) - self.draft_id = draft_id - - def download_revision( revision_id: str, component_purpose: str = globals.SOURCE_PURPOSE, @@ -170,13 +159,13 @@ def open_draft(draft_id: str): if not sandbox.is_local_draft(draft_id): msg = f'The draft "{draft_id}" is not in local sandbox.' - raise InvalidDraftError(draft_id=draft_id, details=msg) + raise exceptions.InvalidDraftError(draft_id=draft_id, details=msg) draft_info = sandbox.read_draft_info(draft_id) draft_path = draft_info.source_path if not os.path.exists(draft_path): msg = f'Corrupted draft folder. The file "{draft_path}" does not exist.' - raise InvalidDraftError(draft_id=draft_id, details=msg) + raise exceptions.InvalidDraftError(draft_id=draft_id, details=msg) # Open file engine.log_info(f"Opening file: {draft_path}") diff --git a/python/tk_multi_loader/flowam/flowam_actions.py b/python/tk_multi_loader/flowam/flowam_actions.py index 5e52e897..41267aa0 100644 --- a/python/tk_multi_loader/flowam/flowam_actions.py +++ b/python/tk_multi_loader/flowam/flowam_actions.py @@ -117,14 +117,14 @@ def _build_new_scene(self, sg_publish_data: dict) -> None: :param sg_publish_data: Shotgun data dictionary with all the standard publish fields. """ parent_window = self._get_dialog_parent() - sg_flow_am_id = self._get_flowam_id() + flow_am_id = self._get_flowam_id() # Get the pipeline step from the task task = sg_publish_data.get("task") task_id = task.get("id") if task else None task_pipeline_step = self._get_task_pipeline_step(task_id) if task_id else None # Open the build scene dialog build_scene_dialog = BuildAssetDialog( - project_id=sg_flow_am_id, + project_id=flow_am_id, parent=parent_window, pipeline_step=task_pipeline_step, ) @@ -145,7 +145,7 @@ def _on_build_scene_dialog_accepted( parent_window = self._get_dialog_parent() - sg_flow_am_id = self._get_flowam_id() + flow_am_id = self._get_flowam_id() if dialog.build == CreateMode.TEMPLATE: template_path = dialog.template_source_path @@ -168,7 +168,7 @@ def _on_build_scene_dialog_accepted( "name", "" ), # Layout, Animation, etc. sg_task_name=sg_publish_data["content"], - am_project_id=sg_flow_am_id, + am_project_id=flow_am_id, create_mode=dialog.build, source_path=template_path, prep_scene_callback=functools.partial(self._prep_scene, sg_publish_data), @@ -292,12 +292,12 @@ def _build_new_template(self, sg_publish_data: dict) -> None: :param sg_publish_data: Shotgun data dictionary with all the standard publish fields. """ # Get the sg_flow_am_id from the Project - sg_flow_am_id = self._get_flowam_id() + flow_am_id = self._get_flowam_id() parent_window = self._get_dialog_parent() build_template_dialog = BuildTemplateDialog( - sg_flow_am_id, self._get_pipeline_steps(), parent_window + flow_am_id, self._get_pipeline_steps(), parent_window ) build_template_dialog.accepted.connect( lambda: self._on_build_template_dialog_accepted( @@ -322,11 +322,11 @@ def _on_build_template_dialog_accepted( QtGui.QMessageBox.critical(parent_window, "Error", message) return - sg_flow_am_id = self._get_flowam_id() + flow_am_id = self._get_flowam_id() create_inputs = CreateTemplateInputs( sg_pipeline_step=dialog.step, - am_project_id=sg_flow_am_id, + am_project_id=flow_am_id, template_name=dialog.template, create_mode=dialog.mode, ) @@ -352,16 +352,16 @@ def _get_flowam_id(self) -> str: :returns: The Flow AM project ID or None if not found. """ parent_window = self._get_dialog_parent() - sg_flow_am_id = self._app.context.project.get("sg_flow_am_id") - if not sg_flow_am_id: + flow_am_id = self._app.context.project.get("sg_flow_am_id") + if not flow_am_id: project = self._app.shotgun.find_one( "Project", [["id", "is", self._app.context.project["id"]]], ["sg_flow_am_id"], ) - sg_flow_am_id = project.get("sg_flow_am_id") + flow_am_id = project.get("sg_flow_am_id") - if not sg_flow_am_id: + if not flow_am_id: err_details = { "Context project": self._app.context.project, "Project ID": ( @@ -369,7 +369,7 @@ def _get_flowam_id(self) -> str: if self._app.context.project else "None" ), - "sg_flow_am_id value": sg_flow_am_id, + "sg_flow_am_id value": flow_am_id, } details_str = "\n".join([f" {k}: {v}" for k, v in err_details.items()]) message = ( @@ -385,7 +385,7 @@ def _get_flowam_id(self) -> str: ) raise TankError(message) - return sg_flow_am_id + return flow_am_id def _get_dialog_parent(self) -> QtGui.QWidget | None: """ @@ -420,3 +420,13 @@ def _download_asset_revision(self, sg_publish_data: dict) -> None: msg_lines.append(f" • Blob {i}: {file_path}") msg = "\n".join(msg_lines) QtGui.QMessageBox.information(None, "Download Result", msg) + + def is_local_draft_by_revision(revision_id: str) -> bool: + """ + Helper method to determine if a given draft id represents a local draft. + + :param draft_id: Id that uniquely identifies a draft within local sandbox. + :returns: True if the given draft id represents a local draft, False otherwise. + """ + draft_id = sandbox.get_draft_id(revision_id) + return sandbox.is_local_draft(draft_id) From 71eedd485a3df4b35f2a3a4e5744671123a74d6f Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Fri, 19 Jun 2026 12:08:40 -0500 Subject: [PATCH 31/37] Fix discard draft logic --- python/tk_multi_loader/flowam/flowam_actions.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/python/tk_multi_loader/flowam/flowam_actions.py b/python/tk_multi_loader/flowam/flowam_actions.py index 41267aa0..ad16bdfd 100644 --- a/python/tk_multi_loader/flowam/flowam_actions.py +++ b/python/tk_multi_loader/flowam/flowam_actions.py @@ -241,7 +241,14 @@ def _discard_draft(self, sg_publish_data: dict) -> None: ) if message_response == QtGui.QMessageBox.StandardButton.Yes: - sandbox.discard_draft(sg_publish_data.get("sg_flow_revision_id")) + draft_id = sg_publish_data.get("sg_flow_revision_id") + current_engine = sgtk.platform.current_engine() + clear_scene = current_engine.context.flow_draft_id == draft_id + + sandbox.discard_draft(draft_id) + + if clear_scene: + current_engine.flow_host.new_scene(force=True) QtGui.QMessageBox.information( parent_window, From 12e841ff4cd249dc2acad5bc4606a780fc587591 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Fri, 19 Jun 2026 12:14:29 -0500 Subject: [PATCH 32/37] Format --- hooks/tk-houdini_actions.py | 4 +++- hooks/tk-maya_actions.py | 4 +++- hooks/tk-nuke_actions.py | 11 ++++++++--- python/tk_multi_loader/flowam/flowam_actions.py | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/hooks/tk-houdini_actions.py b/hooks/tk-houdini_actions.py index 44a5984b..9a8b90aa 100644 --- a/hooks/tk-houdini_actions.py +++ b/hooks/tk-houdini_actions.py @@ -108,7 +108,9 @@ def generate_actions(self, sg_publish_data, actions, ui_area): if "open" in actions and sg_publish_data.get("type") == "PublishedFile": if ( - flowam_actions.is_local_draft_by_revision(sg_publish_data.get("sg_flow_revision_id")) + flowam_actions.is_local_draft_by_revision( + sg_publish_data.get("sg_flow_revision_id") + ) or sg_publish_data.get( "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) diff --git a/hooks/tk-maya_actions.py b/hooks/tk-maya_actions.py index 0a032da2..662bbe68 100644 --- a/hooks/tk-maya_actions.py +++ b/hooks/tk-maya_actions.py @@ -137,7 +137,9 @@ def generate_actions(self, sg_publish_data, actions, ui_area): "open" in actions and sg_publish_data.get("type") == "PublishedFile" and ( - flowam_actions.is_local_draft_by_revision(sg_publish_data.get("sg_flow_revision_id")) + flowam_actions.is_local_draft_by_revision( + sg_publish_data.get("sg_flow_revision_id") + ) or sg_publish_data.get( "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) diff --git a/hooks/tk-nuke_actions.py b/hooks/tk-nuke_actions.py index d455a5bf..3bc166fd 100644 --- a/hooks/tk-nuke_actions.py +++ b/hooks/tk-nuke_actions.py @@ -140,7 +140,9 @@ def generate_actions(self, sg_publish_data, actions, ui_area): # 1. Local drafts (version_number == -1 and is_local_draft) # 2. Published revisions (version_number > -1) if ( - flowam_actions.is_local_draft_by_revision(sg_publish_data.get("sg_flow_revision_id")) + flowam_actions.is_local_draft_by_revision( + sg_publish_data.get("sg_flow_revision_id") + ) or sg_publish_data.get( "version_number", flowam_actions.DRAFT_VERSION_IDENTIFIER ) @@ -155,8 +157,11 @@ def generate_actions(self, sg_publish_data, actions, ui_area): } ) - if "discard_draft" in actions and flowam_actions.is_local_draft_by_revision( - sg_publish_data.get("sg_flow_revision_id") + if ( + "discard_draft" in actions + and flowam_actions.is_local_draft_by_revision( + sg_publish_data.get("sg_flow_revision_id") + ) ): action_instances.append( { diff --git a/python/tk_multi_loader/flowam/flowam_actions.py b/python/tk_multi_loader/flowam/flowam_actions.py index ad16bdfd..71d02ef5 100644 --- a/python/tk_multi_loader/flowam/flowam_actions.py +++ b/python/tk_multi_loader/flowam/flowam_actions.py @@ -337,7 +337,7 @@ def _on_build_template_dialog_accepted( template_name=dialog.template, create_mode=dialog.mode, ) - + try: draft_info = create_template_workfile(create_inputs) self._app.log_debug( From 113d9464ce7dd01b6fc94e02e4c09f8bc89346b9 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Fri, 19 Jun 2026 14:07:24 -0500 Subject: [PATCH 33/37] Update reference sequence --- python/tk_multi_loader/flowam/reference.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/python/tk_multi_loader/flowam/reference.py b/python/tk_multi_loader/flowam/reference.py index 7eabc9ff..b9d58aaa 100644 --- a/python/tk_multi_loader/flowam/reference.py +++ b/python/tk_multi_loader/flowam/reference.py @@ -13,7 +13,7 @@ import os import sgtk -from tank_vendor.flow_integration_sdk import globals, exceptions, objects +from tank_vendor.flow_integration_sdk import exceptions, globals, objects, schema, utils class CreateReferenceError(exceptions.FlowError): @@ -79,10 +79,18 @@ def reference_revision(revision_id: str) -> str: if file_path is None: msg = "Revision does not have a source component to be referenced." raise CreateReferenceError(input_id=revision_id, details=msg) + file_seq_comp = revision.find_component( + type_id=schema.get_schema_id(globals.FILE_SEQ_TYPE) + ) if not os.path.exists(file_path): msg = f"Source file does not exist in storage: {file_path}. " msg += "Fetching the revision was not successful!" raise CreateReferenceError(input_id=revision_id, details=msg) + elif file_seq_comp: + # Return a file path with embedded frame padding + file_path = utils.cleanpath( + revision.get_storage_dir(), file_seq_comp.properties["fileFormat"] + ) # Create reference depdata = engine.flow_host.create_reference(file_path, namespace=revision.name) @@ -130,9 +138,18 @@ def copy_reference_link(revision_id: str) -> str: if file_path is None: msg = "Revision does not have a source component to be referenced." raise CreateReferenceError(input_id=revision_id, details=msg) - if not os.path.exists(file_path): + + file_seq_comp = revision.find_component( + type_id=schema.get_schema_id(globals.FILE_SEQ_TYPE) + ) + if not file_seq_comp and not os.path.exists(file_path): msg = f"Source file does not exist in storage: {file_path}" raise CreateReferenceError(input_id=revision_id, details=msg) + elif file_seq_comp: + # Return a file path with embedded frame padding + file_path = utils.cleanpath( + revision.get_storage_dir(), file_seq_comp.properties["fileFormat"] + ) # Copy to clipboard engine.flow_host.copy_to_clipboard(file_path) From b74125e269ddec007d0a42d2a0fd2755c1e4d295 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Fri, 19 Jun 2026 14:12:04 -0500 Subject: [PATCH 34/37] Improved `CreateReferenceError` --- python/tk_multi_loader/flowam/reference.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/python/tk_multi_loader/flowam/reference.py b/python/tk_multi_loader/flowam/reference.py index b9d58aaa..dca388a3 100644 --- a/python/tk_multi_loader/flowam/reference.py +++ b/python/tk_multi_loader/flowam/reference.py @@ -17,21 +17,17 @@ class CreateReferenceError(exceptions.FlowError): - def __init__(self, *args, input_id: str = "", file_path: str = "", **kwargs): + def __init__(self, *args, input_id: str, **kwargs): """ Args: input_id: Id of revision or version being referenced. - file_path: File being referenced. """ if input_id: message = f"Could not create reference to {input_id}." - elif file_path: - message = f"Could not create reference of file: {file_path}." else: message = "Could not create reference." super().__init__(message, *args, **kwargs) self.input_id = input_id - self.file_path = file_path def reference_revision(revision_id: str) -> str: From 2974c6b4950a7ab6c40f810f4539ddfff2000cda Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Fri, 19 Jun 2026 14:38:48 -0500 Subject: [PATCH 35/37] CR fixes --- python/tk_multi_loader/flowam/reference.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/python/tk_multi_loader/flowam/reference.py b/python/tk_multi_loader/flowam/reference.py index dca388a3..83ccb18d 100644 --- a/python/tk_multi_loader/flowam/reference.py +++ b/python/tk_multi_loader/flowam/reference.py @@ -22,12 +22,8 @@ def __init__(self, *args, input_id: str, **kwargs): Args: input_id: Id of revision or version being referenced. """ - if input_id: - message = f"Could not create reference to {input_id}." - else: - message = "Could not create reference." - super().__init__(message, *args, **kwargs) self.input_id = input_id + super().__init__(f"Could not create reference to {input_id}.", *args, **kwargs) def reference_revision(revision_id: str) -> str: @@ -78,7 +74,7 @@ def reference_revision(revision_id: str) -> str: file_seq_comp = revision.find_component( type_id=schema.get_schema_id(globals.FILE_SEQ_TYPE) ) - if not os.path.exists(file_path): + if not file_seq_comp and not os.path.exists(file_path): msg = f"Source file does not exist in storage: {file_path}. " msg += "Fetching the revision was not successful!" raise CreateReferenceError(input_id=revision_id, details=msg) From ce26987e45803e08a92c0635c0b1d8e07032fffc Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Mon, 22 Jun 2026 08:31:55 -0500 Subject: [PATCH 36/37] Fix method signature --- python/tk_multi_loader/flowam/flowam_actions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/tk_multi_loader/flowam/flowam_actions.py b/python/tk_multi_loader/flowam/flowam_actions.py index 71d02ef5..66d3ecfb 100644 --- a/python/tk_multi_loader/flowam/flowam_actions.py +++ b/python/tk_multi_loader/flowam/flowam_actions.py @@ -428,12 +428,12 @@ def _download_asset_revision(self, sg_publish_data: dict) -> None: msg = "\n".join(msg_lines) QtGui.QMessageBox.information(None, "Download Result", msg) - def is_local_draft_by_revision(revision_id: str) -> bool: + def is_local_draft_by_revision(self, revision_id: str) -> bool: """ Helper method to determine if a given draft id represents a local draft. - :param draft_id: Id that uniquely identifies a draft within local sandbox. - :returns: True if the given draft id represents a local draft, False otherwise. + :param revision_id: Id that uniquely identifies a revision within local sandbox. + :returns: True if the given revision id represents a local draft, False otherwise. """ draft_id = sandbox.get_draft_id(revision_id) return sandbox.is_local_draft(draft_id) From 38e0e20ea592ca457c3e4c5936b7181635f2cc60 Mon Sep 17 00:00:00 2001 From: Carlos Villavicencio Date: Mon, 22 Jun 2026 09:05:59 -0500 Subject: [PATCH 37/37] Import flowam module just to be safe --- python/tk_multi_loader/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/tk_multi_loader/__init__.py b/python/tk_multi_loader/__init__.py index c6abd81f..d793ef23 100644 --- a/python/tk_multi_loader/__init__.py +++ b/python/tk_multi_loader/__init__.py @@ -8,6 +8,7 @@ # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Shotgun Software Inc. +from . import flowam # noqa: F401 from .api import LoaderManager # noqa: F401 from .open_publish_form import open_publish_browser # noqa: F401