Skip to content
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
9a9f76e
SG-43217 Integrate Flow Data SDK as a vendored beta library
stevelittlefish May 13, 2026
13366d6
SG-43217 Drop _patch_flow_data_sdk_version now that upstream is fixed
stevelittlefish May 14, 2026
176b338
SG-43217 Drop _load_packages_from_zip required flag
stevelittlefish May 14, 2026
b907871
Removed unused import
stevelittlefish May 14, 2026
f96c7d6
SG-43217 Put shared zips ahead of pkgs.zip on sys.path
stevelittlefish May 15, 2026
8c006ca
SG-43217 Release importlib.metadata file handles after loading vendor…
stevelittlefish May 15, 2026
b6388cf
SG-43217 Gate importlib.metadata handle release to Windows
stevelittlefish May 15, 2026
5aca09a
SG-43217 Address Copilot review feedback
stevelittlefish May 15, 2026
f41a952
SG-43217 Address review feedback
stevelittlefish May 19, 2026
37d75bc
SG-43166 Integrate MEDM authentication into Toolkit bootstrap
stevelittlefish May 20, 2026
27d1e01
SG-43217 Address Julien's review feedback
stevelittlefish May 20, 2026
8381203
Put the comments back that Claude decided were not worthy of existing
stevelittlefish May 20, 2026
01b05b9
Merge remote-tracking branch 'origin/ticket/SG-43217/flow-data-sdk' i…
stevelittlefish May 21, 2026
9f63cfe
SG-43166 Confirm sg_flow_am_id field name
stevelittlefish May 21, 2026
cffb0e7
SG-43166 Apply Black formatting
stevelittlefish May 21, 2026
7fcd1f8
SG-43166 Use production APS base URL
stevelittlefish May 21, 2026
dac96ba
SG-43166 Address Julien's review feedback
stevelittlefish May 22, 2026
7a07f9b
Make the linter happy
stevelittlefish May 22, 2026
872d88f
Rebuild pkgs.zip for all Python versions and fix namespace package as…
stevelittlefish May 22, 2026
779a57a
Reworked flow auth to store the auth token in a file
stevelittlefish May 26, 2026
648a105
Updated vendor packages
stevelittlefish May 26, 2026
1bb1200
Linter
stevelittlefish May 26, 2026
44319fe
Set correct app id and auth url
stevelittlefish May 27, 2026
d24b615
SDK entrypoint (get_flow_client())
stevelittlefish May 27, 2026
c6c84f3
Made the linter happy
stevelittlefish May 27, 2026
0dc9568
Fixed flow data import
stevelittlefish May 27, 2026
af6b3f5
One more stray import
stevelittlefish May 27, 2026
9b834e3
CR feedback - empty line in imports
stevelittlefish May 27, 2026
e601157
Sorted imports alphabetically
stevelittlefish May 27, 2026
176e8ac
Potential fix for pull request finding
stevelittlefish Jun 2, 2026
a434383
Restrict OAuth callback server to loopback addresses
stevelittlefish Jun 2, 2026
63878d8
Shut down OAuth callback server after auth code is received
stevelittlefish Jun 2, 2026
f66326e
Changed description from test value
stevelittlefish Jun 2, 2026
d64191f
Ran adsk_auth package through isort
stevelittlefish Jun 3, 2026
091db05
add flow am id to project context while authenticating
yungsiow Jun 3, 2026
c40bc53
Update python/tank_vendor/adsk_auth/pkce.py
stevelittlefish Jun 4, 2026
790cc28
Revert "add flow am id to project context while authenticating"
stevelittlefish Jun 4, 2026
009cca4
SG-43167 Add flow_am_project_id to Toolkit Context
stevelittlefish Jun 4, 2026
c1ff6b2
Avoided unnecessary query of flow am project id
stevelittlefish Jun 4, 2026
7ec58ba
SG-43167 Fix entity=None guard and update flow auth tests
stevelittlefish Jun 4, 2026
d106fba
SG-43167 Fix bootstrap AM check to use sg_connection directly
stevelittlefish Jun 4, 2026
85b0652
adjustments to storing flow project id in context
yungsiow Jun 4, 2026
214c8e9
SG-43167 Fix broken tests after context flow_am_project_id refactor
stevelittlefish Jun 5, 2026
de92967
Cleaned up imports
stevelittlefish Jun 5, 2026
e150a3a
Merge branch 'master' into ticket/SG-43167/flow-am-in-context
stevelittlefish Jun 5, 2026
3ad0d5d
Fix botched merge conflict resolution in manager.py
stevelittlefish Jun 5, 2026
0210b60
Cleaned up another lazy import
stevelittlefish Jun 5, 2026
8482103
Re-added my refactoring to manager.py
stevelittlefish Jun 8, 2026
0319c04
Cautious check when setting flow am project id
stevelittlefish Jun 8, 2026
c0105ac
Got rid of yet another lazy import!
stevelittlefish Jun 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions python/tank/authentication/flow_auth/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def get_authentication_token(self) -> str:
return get_flow_access_token()


# TODO: replace with proper client entry point
def get_flow_client(endpoint_url=None):
"""Return a ready-to-use Flow GQL SDK client with authentication wired in.

Expand Down
12 changes: 6 additions & 6 deletions python/tank/bootstrap/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from .errors import TankBootstrapError
from .configuration import Configuration
from .resolver import ConfigurationResolver
from ..authentication import ShotgunAuthenticator
from ..authentication import ShotgunAuthenticator, flow_auth
from ..pipelineconfig import PipelineConfiguration
from .. import LogManager
from ..errors import TankError
Expand All @@ -33,12 +33,12 @@ class ToolkitManager(object):
# Constants used to make the manager bootstrapping:
# - download and cache the config dependencies needed to run the engine being started in a specific environment.
# - download and cache all the config dependencies needed to run the engine in any environment.
(CACHE_SPARSE, CACHE_FULL) = range(2)
CACHE_SPARSE, CACHE_FULL = range(2)

# Constants used to indicate that the manager is:
# - bootstrapping the toolkit (with method bootstrap_toolkit),
# - starting up the engine (with method _start_engine).
(TOOLKIT_BOOTSTRAP_PHASE, ENGINE_STARTUP_PHASE) = range(2)
TOOLKIT_BOOTSTRAP_PHASE, ENGINE_STARTUP_PHASE = range(2)

# List of constants representing the status of the progress bar when these event occurs during bootstrap.
_RESOLVING_PROJECT_RATE = 0.0
Expand Down Expand Up @@ -962,8 +962,6 @@ def _check_and_trigger_am_auth(self, entity, progress_callback):
Set to ``None`` to use the default callback function.
:rtype: None
"""
from ..authentication import flow_auth

if entity is None:
return

Expand Down Expand Up @@ -1090,7 +1088,9 @@ def _get_configuration(self, entity, progress_callback):

elif self._do_shotgun_config_lookup:
# do the full resolve where we connect to shotgun etc.
log.debug("Checking for pipeline configuration overrides in Flow Production Tracking.")
log.debug(
"Checking for pipeline configuration overrides in Flow Production Tracking."
)
log.debug(
"In order to turn this off, set do_shotgun_config_lookup to False"
)
Expand Down
41 changes: 33 additions & 8 deletions python/tank/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from tank_vendor import yaml
from . import authentication
from .authentication import flow_auth

from .util import login
from .util import shotgun_entity
Expand Down Expand Up @@ -403,6 +404,22 @@ def additional_entities(self):
"""
return self.__additional_entities

@property
def flow_am_project_id(self):
"""
The FlowAM project ID for this context, or ``None`` if the project is
not FlowAM-enabled or there is no project in this context.

The value is read from the ``sg_flow_am_id`` field on the project dict.
It is populated by the bootstrap manager after it queries ShotGrid.

:returns: A string containing the FlowAM project ID, or ``None``.
:rtype: str or None
"""
if self.project:
return self.project.get(flow_auth.AM_READY_PROJECT_FIELD)
Comment thread
stevelittlefish marked this conversation as resolved.
return None

@property
def entity_locations(self):
"""
Expand Down Expand Up @@ -815,8 +832,8 @@ def deserialize(cls, context_str):
def to_dict(self):
"""
Converts the context into a dictionary with keys ``project``,
``entity``, ``user``, ``step``, ``task``, ``additional_entities`` and
``source_entity``.
``entity``, ``user``, ``step``, ``task``, ``additional_entities``,
``source_entity``, and ``flow_am_project_id``.

.. note ::
Contrary to :meth:`Context.serialize`, this method discards information
Expand All @@ -835,6 +852,7 @@ def to_dict(self):
self._cleanup_entity(entity) for entity in self.additional_entities
],
"source_entity": self._cleanup_entity(self.source_entity),
"flow_am_project_id": self.flow_am_project_id,
}

def _cleanup_entity(self, entity):
Expand Down Expand Up @@ -894,7 +912,7 @@ def _from_dict(cls, data):

:returns: :class:`Context`
"""
return Context(
ctx = Context(
tk=data.get("tk"),
project=data.get("project"),
entity=data.get("entity"),
Expand All @@ -904,6 +922,9 @@ def _from_dict(cls, data):
additional_entities=data.get("additional_entities"),
source_entity=data.get("source_entity"),
)
if ctx.project is not None and "flow_am_project_id" in data:
ctx.project[flow_auth.AM_READY_PROJECT_FIELD] = data["flow_am_project_id"]
Comment thread
stevelittlefish marked this conversation as resolved.
return ctx

################################################################################################
# private methods
Expand Down Expand Up @@ -1305,7 +1326,8 @@ def _from_entity_type_and_id(tk, entity, source_entity=None):

if sg_entity is None:
raise TankError(
"Entity %s with id %s not found in Flow Production Tracking!" % (entity_type, entity_id)
"Entity %s with id %s not found in Flow Production Tracking!"
% (entity_type, entity_id)
)

if sg_entity.get("task"):
Expand Down Expand Up @@ -1719,7 +1741,7 @@ def context_yaml_representer(dumper, context):
# pipeline config path as part of the dict
context_dict["_pc_path"] = context.tank.pipeline_configuration.get_path()

return dumper.represent_mapping(u"!TankContext", context_dict)
return dumper.represent_mapping("!TankContext", context_dict)


def context_yaml_constructor(loader, node):
Expand Down Expand Up @@ -1750,7 +1772,7 @@ def context_yaml_constructor(loader, node):


yaml.add_representer(Context, context_yaml_representer)
yaml.add_constructor(u"!TankContext", context_yaml_constructor)
yaml.add_constructor("!TankContext", context_yaml_constructor)

################################################################################################
# utility methods
Expand Down Expand Up @@ -1807,7 +1829,9 @@ def _task_from_sg(tk, task_id, additional_fields=None):
"Task", [["id", "is", task_id]], standard_fields + additional_fields
)
if not task:
raise TankError("Unable to locate Task with id %s in Flow Production Tracking" % task_id)
raise TankError(
"Unable to locate Task with id %s in Flow Production Tracking" % task_id
)

# add task so it can be processed with other shotgun entities
task["task"] = {"type": "Task", "id": task_id, "name": task["content"]}
Expand Down Expand Up @@ -1867,7 +1891,8 @@ def _entity_from_sg(tk, entity_type, entity_id):

if not data:
raise TankError(
"Unable to locate %s with id %s in Flow Production Tracking" % (entity_type, entity_id)
"Unable to locate %s with id %s in Flow Production Tracking"
% (entity_type, entity_id)
)

# create context
Expand Down
54 changes: 54 additions & 0 deletions tests/core_tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,7 @@ def test_dict_cleanup(self):
{"type": "Sequence", "name": "seq_name", "id": self.seq["id"]}
],
"user": {"type": "HumanUser", "id": self.user["id"], "name": USER_NAME},
"flow_am_project_id": None,
Comment thread
stevelittlefish marked this conversation as resolved.
}

ctx = context.Context(**self.kws)
Expand Down Expand Up @@ -1438,3 +1439,56 @@ def test_non_primary_path(self, get_current_user):

self.assertIsNone(result.step)
self.assertIsNone(result.task)


class TestContextFlowAmProjectId(TankTestBase):
"""Tests for Context.flow_am_project_id."""

def setUp(self):
super().setUp()
self.setup_fixtures()

def test_returns_none_for_non_am_project(self):
"""flow_am_project_id is None when sg_flow_am_id is not set on the project."""
ctx = context.Context(self.tk, project=self.project)
self.assertIsNone(ctx.flow_am_project_id)

def test_returns_none_for_empty_context(self):
"""flow_am_project_id is None when the context has no project."""
ctx = context.create_empty(self.tk)
self.assertIsNone(ctx.flow_am_project_id)

def test_returns_value_when_set(self):
"""flow_am_project_id returns sg_flow_am_id when set on the project dict."""
project = dict(self.project, sg_flow_am_id="am-project-abc")
ctx = context.Context(self.tk, project=project)
self.assertEqual(ctx.flow_am_project_id, "am-project-abc")

def test_survives_serialization_roundtrip(self):
"""flow_am_project_id is preserved through serialize/deserialize."""
project = dict(self.project, sg_flow_am_id="am-project-abc")
ctx = context.Context(self.tk, project=project)
serialized = ctx.serialize()
restored = context.deserialize(serialized)
self.assertEqual(restored.flow_am_project_id, "am-project-abc")

def test_set_on_project_dict(self):
"""flow_am_project_id can be set by writing sg_flow_am_id onto the project dict."""
ctx = context.Context(self.tk, project=dict(self.project))
ctx.project["sg_flow_am_id"] = "explicit-am-id"
self.assertEqual(ctx.flow_am_project_id, "explicit-am-id")

def test_included_in_to_dict(self):
"""flow_am_project_id appears in the dict returned by to_dict()."""
project = dict(self.project, sg_flow_am_id="explicit-am-id")
ctx = context.Context(self.tk, project=project)
d = ctx.to_dict()
self.assertIn("flow_am_project_id", d)
self.assertEqual(d["flow_am_project_id"], "explicit-am-id")

def test_deepcopy_preserves_value(self):
"""Deepcopying a context preserves flow_am_project_id."""
project = dict(self.project, sg_flow_am_id="am-project-abc")
ctx = context.Context(self.tk, project=project)
ctx_copy = copy.deepcopy(ctx)
self.assertEqual(ctx_copy.flow_am_project_id, "am-project-abc")