From 55bccf7c0ba2613582ab54554a528802cab6f6b9 Mon Sep 17 00:00:00 2001 From: chenm1 Date: Tue, 26 May 2026 20:38:29 -0700 Subject: [PATCH 1/7] feat: [SG-43421] Add Flow AM publish hooks to hooks/flowam/ Move Flow AM publish hooks from tk-config-flowam POC into upstream tk-desktop as a lift-and-shift with no logic changes. Added hooks/flowam/: - collector_desktop.py: Desktop-specific collector extending base collector - publish_to_flow_desktop.py: Desktop-specific publish to Flow AM hook --- hooks/flowam/collector_desktop.py | 150 ++++++++++++++++++++ hooks/flowam/publish_to_flow_desktop.py | 174 ++++++++++++++++++++++++ 2 files changed, 324 insertions(+) create mode 100644 hooks/flowam/collector_desktop.py create mode 100644 hooks/flowam/publish_to_flow_desktop.py diff --git a/hooks/flowam/collector_desktop.py b/hooks/flowam/collector_desktop.py new file mode 100644 index 00000000..1427535e --- /dev/null +++ b/hooks/flowam/collector_desktop.py @@ -0,0 +1,150 @@ +# Copyright (c) 2025 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +import os +import sgtk + +HookBaseClass = sgtk.get_hook_baseclass() + + +class FlowDesktopFileCollector(HookBaseClass): + def process_file(self, settings, parent_item, path): + """ + Analyzes the given file or folder and creates publish items. + Blocks DCC-specific files that must be published from within their applications. + + Args: + settings (dict): Configured settings for this collector + parent_item: Root item instance + path: Path of the file + + Returns: + The created file item, or None if path is a folder or blocked DCC file + """ + + # Check if this is a DCC file that should not be published from desktop + file_info = self.parent.util.get_file_path_components(path) + extension = file_info["extension"] + + # Define DCC file extensions that cannot be published from desktop + # These files require their specific DCC application to be open for proper publishing + dcc_extensions = [ + # Maya + "ma", + "mb", + # Nuke + "nk", + "nkple", + # Houdini + "hip", + "hipnc", + "hiplc", + # 3ds Max + "max", + # Hiero + "hrox", + # Photoshop + "psd", + "psb", + # VRED + "vpb", + "vpe", + "osb", + # Alias + "wire", + # After Effects + "aep", + "aet", + ] + + if extension in dcc_extensions: + # Get the file type display name + file_type = "Unknown" + for display_name, type_info in self.common_file_info.items(): + if extension in type_info["extensions"]: + file_type = display_name + break + + # Log an error message that will be visible to the user + self.logger.error( + "Cannot publish {file_type} files from Desktop. " + "Please publish from within the application instead.".format( + file_type=file_type + ), + extra={ + "action_show_more_info": { + "label": "Learn More", + "text": ( + "DCC files must be published from within their application.

" + "Files like Maya scenes (.ma, .mb), Nuke scripts (.nk, .nkple), Houdini scenes " + "(.hip, .hipnc, .hiplc), 3ds Max scenes (.max), Photoshop images (.psd, .psb), " + "and other DCC-specific formats contain application-specific data that requires " + "the DCC to be open for proper publishing.

" + "The Desktop Publisher is designed for publishing rendered images, textures, " + "Alembic caches, and other standalone files." + ), + } + }, + ) + return None + + return self._collect_file(parent_item, path) + + def process_current_session(self, settings, parent_item): + """ + Collect publishable items for desktop publishing from the current session. + + This method performs the following: + 1. Captures the current app context and locks it to this publisher instance + 2. Reads the revision_id from environment variables (if publishing a new revision of an existing asset) + 3. Collects publishable items based on the desktop workflow + + Args: + settings (dict): Configured settings for this collector + parent_item: Root item instance + """ + + # Get app context and set it to the parent item of current publish session + app_context = self.parent.context + parent_item.context = app_context + + # Determine the appropriate env var based on context level + task = parent_item.context.task + revision_id_env_var = None + + if task: + # Task-level context + task_id = task["id"] + revision_id_env_var = f"TK_FLOWAM_REVISION_ID_{task_id}" + else: + # Project-level context + project = parent_item.context.project + if project: + project_id = project["id"] + revision_id_env_var = f"TK_FLOWAM_REVISION_ID_PROJECT_{project_id}" + + # Capture revision_id from environment variable and store it on root item (parent_item) + # so all child file items can access it via item.parent.properties. This identifies + # the existing AM asset we're publishing new revisions to. + if revision_id_env_var and revision_id_env_var in os.environ: + revision_id = os.environ[revision_id_env_var] + parent_item.properties["am_revision_id"] = revision_id + + context_type = "task" if task else "project" + context_id = task["id"] if task else project["id"] + self.logger.debug( + f"Captured revision_id {revision_id} from environment variable for {context_type} {context_id}" + ) + + # Clean up environment variable after capturing + os.environ.pop(revision_id_env_var) + self.logger.debug( + f"Cleaned up environment variable {revision_id_env_var} after capturing revision_id" + ) diff --git a/hooks/flowam/publish_to_flow_desktop.py b/hooks/flowam/publish_to_flow_desktop.py new file mode 100644 index 00000000..4e963937 --- /dev/null +++ b/hooks/flowam/publish_to_flow_desktop.py @@ -0,0 +1,174 @@ +# Copyright (c) 2025 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +import pprint +import os + +import sgtk + +HookBaseClass = sgtk.get_hook_baseclass() + + +class DesktopFlowPublishPlugin(HookBaseClass): + def accept(self, settings, item): + """ + We tell the publisher to skip publishing Maya files + by checking the file extension of the item being published. + These files won't be displayed in the UI. + + PS: This can be also defined in `publish_to_flow.py`. + """ + path = item.get_property("path") + if path is None: + raise AttributeError("'PublishData' object has no attribute 'path'") + + # Get the extension of the file + ext = os.path.splitext(path)[-1].lower() + if ext in [".ma", ".mb"]: + self.logger.warning("Maya dependencies will not be tracked in Flow AM.") + return {"accepted": False} + + return super().accept(settings, item) + + def validate(self, settings, item): + """ + Desktop-specific validation for publishing to Flow AM. + Does not require a draft - supports both new asset creation and revision publishing. + """ + if not super().validate(settings, item): + return False + + # Desktop publishing uses self.flow_module set by parent class + am = self.flow_module.asset_management + + # Read from parent item - revision_id applies to entire publish session + revision_id = None + if item.parent: + revision_id = item.parent.properties.get("am_revision_id") + + if revision_id: + asset_id = self.flow_module.data.Asset.get_asset_id(revision_id) + is_generic_asset, error_msg = am.validate_generic_asset(asset_id) + if not is_generic_asset: + self.logger.error( + "Cannot publish new revision of this asset", + extra={ + "action_show_more_info": { + "label": "Error Details", + "text": "
" f"{error_msg}\n" "
", + } + }, + ) + return False + return True + + def _get_generic_inputs(self, item) -> dict: + sg_flow_am_id = sgtk.platform.current_engine().context.project["sg_flow_am_id"] + entity = item.context.entity or item.context.project + entity_type = entity["type"] + # When creating from project context, sg entity related parameters are not relevant + sg_entity_type = entity_type if entity_type != "Project" else None + sg_entity_name = entity["name"] if entity_type != "Project" else None + sg_pipeline_step = ( + item.context.step["name"] if entity_type != "Project" else None + ) + sg_task_name = item.context.task["name"] if entity_type != "Project" else None + + return dict( + sg_entity_type=sg_entity_type, + sg_entity_name=sg_entity_name, + sg_pipeline_step=sg_pipeline_step, + sg_task_name=sg_task_name, + am_project_id=sg_flow_am_id, + source_path=item.get_property("path"), + ) + + def _publish_revision(self, item, flow_args, revision_id): + """Publish new revision of existing generic asset.""" + self.logger.info( + f"Publishing new revision of existing generic asset (revision_id: {revision_id})" + ) + + # Prepare GenericPublishInputs + flow_args.update( + { + "am_asset_id": revision_id, # Revision id can be used as an asset id in MEDM + "source_path": item.get_property("path"), + } + ) + publish_inputs = self.flow_module.asset_management.GenericPublishInputs( + **flow_args + ) + + self.logger.debug( + "Calling publish_generic_revision with:", + extra={ + "action_show_more_info": { + "label": "See contents", + "text": "
" f"{pprint.pformat(flow_args)}\n" "
", + } + }, + ) + + # Note: If this fails, the exception propagates to the parent publish() method + # which handles error logging. + pub_info = self.flow_module.asset_management.publish_generic_revision( + publish_inputs, + ) + return pub_info + + def _create_new_asset(self, item, flow_args): + """Create new generic asset.""" + self.logger.info("Creating new generic asset") + + create_args = self._get_generic_inputs(item) + create_args.update( + { + "thumbnail_path": flow_args.get("thumbnail_path", ""), + "comment": flow_args.get("comment", ""), + } + ) + create_inputs = self.flow_module.asset_management.CreateGenericInputs( + **create_args + ) + + self.logger.debug( + "Calling create_generic_workfile with:", + extra={ + "action_show_more_info": { + "label": "See contents", + "text": "
" f"{pprint.pformat(create_inputs)}\n" "
", + } + }, + ) + + # Note: If this fails, the exception propagates to the parent publish() method + # which handles error logging. + pub_info = self.flow_module.asset_management.create_generic_workfile( + create_inputs, + ) + return pub_info + + def _publish_to_flow(self, settings, item): + flow_args = self._get_flow_args(item) + + # Read from parent item - revision_id applies to entire publish session + revision_id = None + if item.parent: + revision_id = item.parent.properties.get("am_revision_id") + + # If revision_id is present, we are publishing a new revision of an existing asset, otherwise we are creating a new asset + if revision_id: + pub_info = self._publish_revision(item, flow_args, revision_id) + else: + pub_info = self._create_new_asset(item, flow_args) + + # Return framework data + return pub_info From 0e523baecbdf60b311d873cbe48ffc605452aa90 Mon Sep 17 00:00:00 2001 From: chenm1 Date: Wed, 27 May 2026 21:33:46 -0700 Subject: [PATCH 2/7] Address comment to rename hooks --- hooks/flowam/{collector_desktop.py => collector.py} | 0 hooks/flowam/{publish_to_flow_desktop.py => publish_to_flow.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename hooks/flowam/{collector_desktop.py => collector.py} (100%) rename hooks/flowam/{publish_to_flow_desktop.py => publish_to_flow.py} (100%) diff --git a/hooks/flowam/collector_desktop.py b/hooks/flowam/collector.py similarity index 100% rename from hooks/flowam/collector_desktop.py rename to hooks/flowam/collector.py diff --git a/hooks/flowam/publish_to_flow_desktop.py b/hooks/flowam/publish_to_flow.py similarity index 100% rename from hooks/flowam/publish_to_flow_desktop.py rename to hooks/flowam/publish_to_flow.py From ce193efe34877924c2569f0ebef0d7f05b3a18cb Mon Sep 17 00:00:00 2001 From: chenm1 Date: Thu, 28 May 2026 11:24:05 -0700 Subject: [PATCH 3/7] refactor: [SG-43421] Move Flow AM hooks under hooks/tk-multi-publish2/ Restructure the Flow AM publisher hooks to live under hooks/tk-multi-publish2/flowam/ instead of hooks/flowam/. This matches the layout used by other Toolkit engines (tk-nuke, tk-maya), which group their app-specific hooks by consumer app first, then by variant. Also add class-level docstrings to both hooks documenting their dependency on the tk-multi-publish2 base hooks and the expected hook chain string in the config. Changes: - hooks/flowam/collector.py -> hooks/tk-multi-publish2/flowam/collector.py - hooks/flowam/publish_to_flow.py -> hooks/tk-multi-publish2/flowam/publish_to_flow.py --- hooks/{ => tk-multi-publish2}/flowam/collector.py | 10 ++++++++++ .../{ => tk-multi-publish2}/flowam/publish_to_flow.py | 11 +++++++++++ 2 files changed, 21 insertions(+) rename hooks/{ => tk-multi-publish2}/flowam/collector.py (94%) rename hooks/{ => tk-multi-publish2}/flowam/publish_to_flow.py (93%) diff --git a/hooks/flowam/collector.py b/hooks/tk-multi-publish2/flowam/collector.py similarity index 94% rename from hooks/flowam/collector.py rename to hooks/tk-multi-publish2/flowam/collector.py index 1427535e..0468221a 100644 --- a/hooks/flowam/collector.py +++ b/hooks/tk-multi-publish2/flowam/collector.py @@ -15,6 +15,16 @@ class FlowDesktopFileCollector(HookBaseClass): + """ + Collector that operates on the Flow Production Tracking Desktop publish + workflow. Should inherit from the basic collector hook in the + ``tk-multi-publish2`` app. The collector setting for this hook should look + something like this:: + + collector: "{self}/collector.py:{engine}/tk-multi-publish2/flowam/collector.py" + + """ + def process_file(self, settings, parent_item, path): """ Analyzes the given file or folder and creates publish items. diff --git a/hooks/flowam/publish_to_flow.py b/hooks/tk-multi-publish2/flowam/publish_to_flow.py similarity index 93% rename from hooks/flowam/publish_to_flow.py rename to hooks/tk-multi-publish2/flowam/publish_to_flow.py index 4e963937..5f17f66e 100644 --- a/hooks/flowam/publish_to_flow.py +++ b/hooks/tk-multi-publish2/flowam/publish_to_flow.py @@ -17,6 +17,17 @@ class DesktopFlowPublishPlugin(HookBaseClass): + """ + Plugin for publishing files from Flow Production Tracking Desktop to + Flow Asset Management. This hook relies on functionality found in the + Flow AM base publish hook in the ``tk-multi-publish2`` app and should + inherit from it in the configuration. The hook setting for this plugin + should look something like this:: + + hook: "{self}/publish_file.py:{self}/flowam/publish_to_flow.py:{engine}/tk-multi-publish2/flowam/publish_to_flow.py" + + """ + def accept(self, settings, item): """ We tell the publisher to skip publishing Maya files From 5e0e49720944be6523cd57396ee950dee08b7067 Mon Sep 17 00:00:00 2001 From: chenm1 Date: Thu, 28 May 2026 21:33:08 -0700 Subject: [PATCH 4/7] Address Julien's comment --- hooks/tk-multi-publish2/flowam/collector.py | 1 + hooks/tk-multi-publish2/flowam/publish_to_flow.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/hooks/tk-multi-publish2/flowam/collector.py b/hooks/tk-multi-publish2/flowam/collector.py index 0468221a..f19ccb84 100644 --- a/hooks/tk-multi-publish2/flowam/collector.py +++ b/hooks/tk-multi-publish2/flowam/collector.py @@ -9,6 +9,7 @@ # not expressly granted therein are reserved by Shotgun Software Inc. import os + import sgtk HookBaseClass = sgtk.get_hook_baseclass() diff --git a/hooks/tk-multi-publish2/flowam/publish_to_flow.py b/hooks/tk-multi-publish2/flowam/publish_to_flow.py index 5f17f66e..d80a8321 100644 --- a/hooks/tk-multi-publish2/flowam/publish_to_flow.py +++ b/hooks/tk-multi-publish2/flowam/publish_to_flow.py @@ -8,8 +8,8 @@ # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Shotgun Software Inc. -import pprint import os +import pprint import sgtk From 9789c2675c11693340fd9344d5741a2d900341be Mon Sep 17 00:00:00 2001 From: chenm1 Date: Sat, 30 May 2026 10:15:40 -0700 Subject: [PATCH 5/7] Enhance DesktopFlowPublishPlugin with detailed docstrings and added type annotation for newly added methods. --- .../flowam/publish_to_flow.py | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/hooks/tk-multi-publish2/flowam/publish_to_flow.py b/hooks/tk-multi-publish2/flowam/publish_to_flow.py index d80a8321..567a2446 100644 --- a/hooks/tk-multi-publish2/flowam/publish_to_flow.py +++ b/hooks/tk-multi-publish2/flowam/publish_to_flow.py @@ -81,6 +81,11 @@ def validate(self, settings, item): return True def _get_generic_inputs(self, item) -> dict: + """ + Build the SG entity inputs required by the Flow AM SDK for generic + asset creation. Called by ``_create_new_asset`` to populate + ``CreateGenericInputs``. + """ sg_flow_am_id = sgtk.platform.current_engine().context.project["sg_flow_am_id"] entity = item.context.entity or item.context.project entity_type = entity["type"] @@ -101,8 +106,13 @@ def _get_generic_inputs(self, item) -> dict: source_path=item.get_property("path"), ) - def _publish_revision(self, item, flow_args, revision_id): - """Publish new revision of existing generic asset.""" + def _publish_revision(self, item, flow_args: dict, revision_id: str): + """ + Publish a new revision of an existing generic asset via the Flow AM SDK. + Called by ``_publish_to_flow`` when a revision ID is present on the + parent item, indicating we are updating an existing asset rather than + creating a new one. + """ self.logger.info( f"Publishing new revision of existing generic asset (revision_id: {revision_id})" ) @@ -135,8 +145,13 @@ def _publish_revision(self, item, flow_args, revision_id): ) return pub_info - def _create_new_asset(self, item, flow_args): - """Create new generic asset.""" + def _create_new_asset(self, item, flow_args: dict): + """ + Create a new generic asset via the Flow AM SDK. Called by + ``_publish_to_flow`` when no revision ID is present on the parent + item, indicating this is a first-time publish rather than a revision. + Delegates SG entity resolution to ``_get_generic_inputs``. + """ self.logger.info("Creating new generic asset") create_args = self._get_generic_inputs(item) @@ -167,7 +182,13 @@ def _create_new_asset(self, item, flow_args): ) return pub_info - def _publish_to_flow(self, settings, item): + def _publish_to_flow(self, item): + """ + Implements ``FlowPublishPlugin._publish_to_flow`` (defined in + ``tk-multi-publish2/hooks/flowam/publish_to_flow.py``) for the desktop + context. Delegates to ``_publish_revision`` or ``_create_new_asset`` + depending on whether a revision ID is present on the parent item. + """ flow_args = self._get_flow_args(item) # Read from parent item - revision_id applies to entire publish session From 0fa087733c532dffa3c2963afdada195d7de8f91 Mon Sep 17 00:00:00 2001 From: chenm1 Date: Sun, 31 May 2026 23:58:24 -0700 Subject: [PATCH 6/7] refactor: [SG-43421] Make desktop publish plugin self-contained Rewrite DesktopFlowPublishPlugin to subclass publish_file.py directly, eliminating the dependency on FlowPublishPlugin from tk-multi-publish2. Previously the hook chain included a shared base from tk-multi-publish2: {self}/publish_file.py:{self}/flowam/publish_to_flow.py:{engine}/tk-multi-publish2/flowam/publish_to_flow.py Now the desktop plugin is fully self-contained: {self}/publish_file.py:{engine}/tk-multi-publish2/flowam/publish_to_flow.py All shared logic (properties, accept, validate, publish, finalize, get_publish_user) is inlined. _get_flow_args is inlined into _publish_to_flow. Also add icons/flow.png so self.disk_location resolves the icon correctly. --- hooks/tk-multi-publish2/flowam/icons/flow.png | Bin 0 -> 1788 bytes .../flowam/publish_to_flow.py | 246 ++++++++++++++---- 2 files changed, 199 insertions(+), 47 deletions(-) create mode 100644 hooks/tk-multi-publish2/flowam/icons/flow.png diff --git a/hooks/tk-multi-publish2/flowam/icons/flow.png b/hooks/tk-multi-publish2/flowam/icons/flow.png new file mode 100644 index 0000000000000000000000000000000000000000..ef451528ec621b6de2c6e55f46e318e3d8731b3b GIT binary patch literal 1788 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}EvXTnX}-P; zT0k}j11qBt12aeo5Hc`IF|dN!3=Ce3(r|VVqXtwB69YqgCIbspO%#v@0S_Ps>W0$H z3m6e5E?|PIR#?D{V1u;9@8SOdq&N#aB8wRqxP?KOkzv*x37~0_nIRD+5xzcF$@#f@ zi7EL>sd^Q;1t47vHWgMtW^QUpqC!P(PF}H9g{=};g%ywu64qBz04piUwpEJo4N!2- zFG^J~(=*UBP_pAvP*AWbN=dT{a&d!d2l8x{GD=Dctn~HE%ggo3jrH=2()A53EiLs8 zjP#9+bb%^#i!1X=5-W7`ij^UTz|3(;Elw`VEGWs$&r<-Io0ybeT4JlD1hNPYAnq*5 zOhed|R}A$Q(1ZFQ8GS=N1AVyJK&>_)Q7iwV%v7MwAoJ}EZNMr~#Gv-r=z}araty?$ zU{Rn~?YM08;lXCdB^mdSoq>U6ji-xaNCfBJaNmrWP?3M{YTutdoigc`i7#`gKv$TF zX6%j^ht4)|EK&&L%W3ByRofjevhFMN4Iat2lnV!EVZ0)r^@!ws{JC-Hh zG;=zaX4z1)%u zk*Qr;?a0wvGWDHnZ!b4KxJ&-%yuBZa+&Zc`!swOir>+G7>qs^?~m*)A}-v z-mDEh%gg&8{os~gyg>JePGHWZ1w6l5Zy&V3F8<-i_V)?X^^!U*J=RV-lgRUyb@BH- zd+KGPB&K!dbl+w^d2Cjc`y{E~H=cfbe4zICY`3W0hncHqWXZ34)vB~YgyFUC#bZ2K z$EA#Qj!QLM`SB2~Nrh>|!TBk2G&)z=gzR^h& zX<_z4m5n7l3dYJOFX_zm@QK#{`@!zt+IfqPwoClv+R9hu&7VAT<9h!!S7u2!JZM=G ztrYZI<-CgegQOMtEGbuIbMvj3&rZwDTQ=EW##z~dYxa^aj|JZU{bYGg@BHHP?;K+f zY@MjbxI~bv;Zx)BsF*o({t2e9HrBYccZIR|+a=x%nJI#n2GvjVTLL}eUZ^m4f6a1o zs-OD*Hj9F(bb+{y-%on7i@a|?{bKsf`yHIIX$sxm4-?)S=(VKUcr2Hj ztM@@-+T#l4=bNf7GpPJ5V(KagSI=~R^DrwX+0)2j=j7sjsS-@f)4`$jWx z?%mYC#kXhDx+zX_GIKlbbbWd-!CGN*4a1xT9!5sJ=RJJGv$;35UNTF}GG1J1_Pa!` z+qP14trypVBk@lr*tnf2TAUl*DtF&#tCftMUPXQS=iPa{`cA7K3fFSkGbXg}``FvZ z{nxc(Yumi}e@~y8H$QB5lR~9x>Dy2q51vQCO%rcdm~wMAh`xK#B>J}F?~Bd$b${>L z*QdUlI(L_d=Iw2hvdcu*-nc20c`AeHK$njGC+YKbg+H2ue;uv9f6v&UW5YZ4qC={i zN<|~u<0nm0QInReD*dwJWvKitHuKP^WjPtqQ70XO<`ykY@(t5k7`=AhisQBX6MnK+ zKFPg$f75QZ|Mo}jbg7r>mdKI;Vst0O?l0 A!~g&Q literal 0 HcmV?d00001 diff --git a/hooks/tk-multi-publish2/flowam/publish_to_flow.py b/hooks/tk-multi-publish2/flowam/publish_to_flow.py index 567a2446..1ecc684f 100644 --- a/hooks/tk-multi-publish2/flowam/publish_to_flow.py +++ b/hooks/tk-multi-publish2/flowam/publish_to_flow.py @@ -18,48 +18,136 @@ class DesktopFlowPublishPlugin(HookBaseClass): """ - Plugin for publishing files from Flow Production Tracking Desktop to - Flow Asset Management. This hook relies on functionality found in the - Flow AM base publish hook in the ``tk-multi-publish2`` app and should - inherit from it in the configuration. The hook setting for this plugin - should look something like this:: + Self-contained desktop publish plugin for Flow Asset Management integration. - hook: "{self}/publish_file.py:{self}/flowam/publish_to_flow.py:{engine}/tk-multi-publish2/flowam/publish_to_flow.py" + Subclasses ``publish_file.py`` directly via the hook chain:: + hook: "{self}/publish_file.py:{engine}/tk-multi-publish2/flowam/publish_to_flow.py" + + Handles the full desktop publish workflow: framework loading, project + validation, revision validation, and publishing via + ``create_generic_workfile`` (new asset) or ``publish_generic_revision`` + (existing asset) in the Flow AM SDK. + + The DCC counterpart lives in + ``tk-multi-publish2/hooks/flowam/publish_to_flow.py`` + (``DccFlowPublishPlugin``). Shared logic (properties, ``publish``, + ``finalize``, ``get_publish_user``) is duplicated between the two so + each plugin can evolve independently across separate release cycles. + When updating shared logic, apply the change to both files. """ + def __init__(self, *args, **kwargs): + """Initialize the plugin.""" + super().__init__(*args, **kwargs) + self.flow_module = None + self.sg_flow_am_id = None + + ############################################################################ + # standard publish plugin properties + + @property + def icon(self): + return os.path.join(self.disk_location, "icons", "flow.png") + + @property + def name(self): + return "Publish to Flow AM" + + @property + def description(self): + return """ + Publishes the file to Flow Production Tracking and Asset Manager. A Publish entry + will be created in Flow Production Tracking which will include a reference + to the file's current path on disk. Other users will be able to access the + published file via the Loader so long as they have + access to the file's location on disk. + +

Overwriting an existing publish

+ A file can be published multiple times however only the most recent + publish will be available to other users. Warnings will be provided + during validation if there are previous publishes. + """ + + @property + def item_filters(self): + """ + List of item types that this plugin is interested in. + + Only items matching entries in this list will be presented to the + accept() method. Strings can contain glob patters such as *, for example + ["maya.*", "file.maya"] + """ + return ["file.*"] + + ############################################################################ + # standard publish plugin methods + def accept(self, settings, item): """ - We tell the publisher to skip publishing Maya files - by checking the file extension of the item being published. - These files won't be displayed in the UI. + Method called by the publisher to determine if an item is of any + interest to this plugin. Only items matching the filters defined via the + item_filters property will be presented to this method. - PS: This can be also defined in `publish_to_flow.py`. + Maya files (.ma/.mb) are rejected - Maya dependencies are not tracked + in Flow AM from the desktop context. """ path = item.get_property("path") if path is None: raise AttributeError("'PublishData' object has no attribute 'path'") - # Get the extension of the file ext = os.path.splitext(path)[-1].lower() if ext in [".ma", ".mb"]: self.logger.warning("Maya dependencies will not be tracked in Flow AM.") return {"accepted": False} - return super().accept(settings, item) + # log the accepted file and display a button to reveal it in the fs + self.logger.info( + "File publisher plugin accepted: %s" % (path,), + extra={"action_show_folder": {"path": path}}, + ) + + return {"accepted": True} def validate(self, settings, item): """ - Desktop-specific validation for publishing to Flow AM. - Does not require a draft - supports both new asset creation and revision publishing. + Validates project configuration, AM project ID, and (if present) + revision asset type. Combines base project validation with desktop- + specific revision validation - no super() call needed. """ - if not super().validate(settings, item): + # FlowAM framework import + # TODO: We have an issue on FPTR desktop where the `adsk` cannot be found + flow_am_fw = self.load_framework("tk-framework-flowam_v1.x.x") + self.flow_module = flow_am_fw.import_module("flow") + + publisher = self.parent + + # Get the project's sg_flow_am_id + sg_flow_project_id = sgtk.platform.current_engine().context.project["id"] + project = publisher.shotgun.find_one( + "Project", [["id", "is", sg_flow_project_id]], ["sg_flow_am_id"] + ) + self.sg_flow_am_id = project.get("sg_flow_am_id") + if not self.sg_flow_am_id: + self.logger.error( + "Project {} has no sg_flow_am_id set. " + "Please set the sg_flow_am_id field on the project.".format( + project["name"] + ) + ) return False - # Desktop publishing uses self.flow_module set by parent class + self.logger.info("Validating AM Project ID") am = self.flow_module.asset_management + project_valid, project_err = am.validate_project(self.sg_flow_am_id) + if not project_valid: + self.logger.error( + f"No Flow project associated with current SG project: {project_err}" + ) + return False - # Read from parent item - revision_id applies to entire publish session + # Desktop publishing: validate revision asset type if publishing to + # an existing asset (revision_id present on parent item) revision_id = None if item.parent: revision_id = item.parent.properties.get("am_revision_id") @@ -78,8 +166,95 @@ def validate(self, settings, item): }, ) return False + return True + def publish(self, settings, item): + """Publish the item to Flow AM.""" + try: + pub_info = self._publish_to_flow(item) + + # Check if user cancelled (child process return None) + if pub_info is None: + raise self.parent.base_hooks.PublishCancelledException( + "User cancelled the publish to Flow AM." + ) + self.logger.info("Publish to Flow AM successful") + + # Store publish info for downstream plugins (e.g., alembic derivative) + item.properties["am_publish_info"] = pub_info + item.properties["entity"] = item.context.entity or item.context.project + item.properties["task"] = item.context.task + + self.logger.info("Publish registered!") + self.logger.debug( + "Flow AM Publish info...", + extra={ + "action_show_more_info": { + "label": "Flow AM Publish Info", + "tooltip": "Show the complete Flow AM Publish info", + "text": "
%s
" % (pprint.pformat(pub_info.__dict__),), + } + }, + ) + except self.parent.base_hooks.PublishCancelledException: + # Re-raise cancellation exception without logging as error + # The dialog will handle this and show "Publish Cancelled" + raise + except Exception as e: + self.logger.error( + "Failed to publish to Flow AM", + extra={ + "action_show_more_info": { + "label": "Error Details", + "text": "
" f"{e}\n" "
", + } + }, + ) + raise + + def finalize(self, settings, item): + pass + + def get_publish_user(self, settings, item): + """ + Get the user that will be associated with this publish. + + If publish_user is not defined as a ``property`` or ``local_property``, + this method will return ``None``. + + :param settings: This plugin instance's configured settings + :param item: The item to determine the publish template for + + :return: A user entity dictionary or ``None`` if not defined. + """ + return item.context.user + + ############################################################################ + # protected methods + + def _publish_to_flow(self, item): + """ + Delegates to ``_publish_revision`` or ``_create_new_asset`` depending + on whether a revision ID is present on the parent item. + """ + flow_args = dict( + comment=item.description or "", + thumbnail_path=item.get_thumbnail_as_path(), + ) + + # Read from parent item - revision_id applies to entire publish session + revision_id = None + if item.parent: + revision_id = item.parent.properties.get("am_revision_id") + + if revision_id: + pub_info = self._publish_revision(item, flow_args, revision_id) + else: + pub_info = self._create_new_asset(item, flow_args) + + return pub_info + def _get_generic_inputs(self, item) -> dict: """ Build the SG entity inputs required by the Flow AM SDK for generic @@ -98,11 +273,11 @@ def _get_generic_inputs(self, item) -> dict: sg_task_name = item.context.task["name"] if entity_type != "Project" else None return dict( - sg_entity_type=sg_entity_type, + am_project_id=sg_flow_am_id, sg_entity_name=sg_entity_name, + sg_entity_type=sg_entity_type, sg_pipeline_step=sg_pipeline_step, sg_task_name=sg_task_name, - am_project_id=sg_flow_am_id, source_path=item.get_property("path"), ) @@ -138,8 +313,8 @@ def _publish_revision(self, item, flow_args: dict, revision_id: str): }, ) - # Note: If this fails, the exception propagates to the parent publish() method - # which handles error logging. + # Note: If this fails, the exception propagates to publish() which + # handles error logging. pub_info = self.flow_module.asset_management.publish_generic_revision( publish_inputs, ) @@ -157,8 +332,8 @@ def _create_new_asset(self, item, flow_args: dict): create_args = self._get_generic_inputs(item) create_args.update( { - "thumbnail_path": flow_args.get("thumbnail_path", ""), "comment": flow_args.get("comment", ""), + "thumbnail_path": flow_args.get("thumbnail_path", ""), } ) create_inputs = self.flow_module.asset_management.CreateGenericInputs( @@ -175,32 +350,9 @@ def _create_new_asset(self, item, flow_args: dict): }, ) - # Note: If this fails, the exception propagates to the parent publish() method - # which handles error logging. + # Note: If this fails, the exception propagates to publish() which + # handles error logging. pub_info = self.flow_module.asset_management.create_generic_workfile( create_inputs, ) return pub_info - - def _publish_to_flow(self, item): - """ - Implements ``FlowPublishPlugin._publish_to_flow`` (defined in - ``tk-multi-publish2/hooks/flowam/publish_to_flow.py``) for the desktop - context. Delegates to ``_publish_revision`` or ``_create_new_asset`` - depending on whether a revision ID is present on the parent item. - """ - flow_args = self._get_flow_args(item) - - # Read from parent item - revision_id applies to entire publish session - revision_id = None - if item.parent: - revision_id = item.parent.properties.get("am_revision_id") - - # If revision_id is present, we are publishing a new revision of an existing asset, otherwise we are creating a new asset - if revision_id: - pub_info = self._publish_revision(item, flow_args, revision_id) - else: - pub_info = self._create_new_asset(item, flow_args) - - # Return framework data - return pub_info From 30ddbf02f922a093e28b87ad4eb497bbcd912a69 Mon Sep 17 00:00:00 2001 From: chenm1 Date: Tue, 2 Jun 2026 14:08:13 -0700 Subject: [PATCH 7/7] Remove redundant override. --- hooks/tk-multi-publish2/flowam/publish_to_flow.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/hooks/tk-multi-publish2/flowam/publish_to_flow.py b/hooks/tk-multi-publish2/flowam/publish_to_flow.py index 1ecc684f..cb16ef1e 100644 --- a/hooks/tk-multi-publish2/flowam/publish_to_flow.py +++ b/hooks/tk-multi-publish2/flowam/publish_to_flow.py @@ -69,17 +69,6 @@ def description(self): during validation if there are previous publishes. """ - @property - def item_filters(self): - """ - List of item types that this plugin is interested in. - - Only items matching entries in this list will be presented to the - accept() method. Strings can contain glob patters such as *, for example - ["maya.*", "file.maya"] - """ - return ["file.*"] - ############################################################################ # standard publish plugin methods @@ -214,6 +203,8 @@ def publish(self, settings, item): raise def finalize(self, settings, item): + # Override base class no-op: Flow AM publishes do not produce + # sg_publish_data, so the base finalize (which reads it) would fail. pass def get_publish_user(self, settings, item):