From 1a14df4073cfe72a6bd4ffe106e34d34e960f1c3 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 28 Jul 2026 09:28:42 -0700 Subject: [PATCH] Lazy creation of Teams ApiClient --- .../hosting/msteams/_teams_api_client.py | 22 +++-------- .../hosting/msteams/teams_agent_extension.py | 11 +++--- .../hosting/msteams/teams_turn_context.py | 7 +++- .../mocks/mock_user_token_client.py | 4 +- tests/hosting_msteams/test_internal.py | 38 ------------------- .../test_teams_agent_extension.py | 1 - .../test_teams_turn_context.py | 29 ++++++++++++++ 7 files changed, 45 insertions(+), 67 deletions(-) diff --git a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py index dbbd0c7c..f022fa8c 100644 --- a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py +++ b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py @@ -17,23 +17,9 @@ ) -def _get_teams_api_client(context: TurnContext) -> ApiClient: - """ - Get the cached Teams API client from the context. - - :param context: The turn context. - :return: The cached Teams API client. - :raises ValueError: If the Teams API client is not found. - """ - api_client = context.services.get(ApiClient) - if isinstance(api_client, ApiClient): - return api_client - raise ValueError("Unable to retrieve Teams API client.") - - def _set_teams_api_client( context: TurnContext, connection_manager: Connections -) -> None: +) -> ApiClient: """ Set the Teams API client in the context if it is not already set. @@ -41,8 +27,9 @@ def _set_teams_api_client( :param connection_manager: The connection manager. """ - if context.services.has(ApiClient): - return + api_client = context.services.get(ApiClient) + if api_client is not None: + return api_client headers = { "Accept": "application/json", @@ -74,3 +61,4 @@ async def token_factory() -> str: ) context.services.set(ApiClient, api_client) + return api_client diff --git a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_agent_extension.py b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_agent_extension.py index 3070843b..620ce313 100644 --- a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_agent_extension.py +++ b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_agent_extension.py @@ -57,10 +57,7 @@ _common_get_app_graph_client_for_connection, ) -from ._teams_api_client import ( - _get_teams_api_client, - _set_teams_api_client, -) +from ._teams_api_client import _set_teams_api_client from ._utils import _try_get_channel_data from .teams_activity import TeamsActivity @@ -124,7 +121,6 @@ def _configure_app(self): async def on_before_turn(context: TurnContext, state: StateT) -> bool: if context.activity.channel_id == Channels.ms_teams: - _set_teams_api_client(context, self._app.connection_manager) # caches the deserialized version of ChannelData context.activity.channel_data = _try_get_channel_data(context.activity) return True @@ -301,7 +297,10 @@ def get_teams_api_client(self, context: TurnContext) -> ApiClient: :return: The Teams API client. """ - return _get_teams_api_client(context) + api_client = context.services.get(ApiClient) + if not api_client: + return _set_teams_api_client(context, self._app.connection_manager) + return api_client def get_graph_client( self, diff --git a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_turn_context.py b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_turn_context.py index 81509842..beef6051 100644 --- a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_turn_context.py +++ b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_turn_context.py @@ -28,7 +28,7 @@ _common_get_app_graph_client, _common_get_app_graph_client_for_connection, ) -from ._teams_api_client import _get_teams_api_client, _set_teams_api_client +from ._teams_api_client import _set_teams_api_client from .teams_activity import TeamsActivity @@ -96,7 +96,10 @@ def activity(self) -> TeamsActivity: @property def api_client(self) -> ApiClient: """Get the API client for the Teams turn context.""" - return _get_teams_api_client(self) + api_client = self._services.get(ApiClient) + if not api_client: + return _set_teams_api_client(self, self._app.connection_manager) + return api_client @staticmethod def _make_targeted_activity(activity: Activity) -> None: diff --git a/tests/_common/testing_objects/mocks/mock_user_token_client.py b/tests/_common/testing_objects/mocks/mock_user_token_client.py index 273b69c2..5bd6495c 100644 --- a/tests/_common/testing_objects/mocks/mock_user_token_client.py +++ b/tests/_common/testing_objects/mocks/mock_user_token_client.py @@ -65,9 +65,7 @@ async def get_token_or_sign_in_resource( state, ) - mock_user_token_client.get_user_token = mocker.AsyncMock( - side_effect=get_user_token - ) + mock_user_token_client.get_user_token = mocker.AsyncMock(side_effect=get_user_token) mock_user_token_client.sign_out_user = mocker.AsyncMock(side_effect=sign_out_user) mock_user_token_client.exchange_token = mocker.AsyncMock(side_effect=exchange_token) mock_user_token_client.get_token_or_sign_in_resource = mocker.AsyncMock( diff --git a/tests/hosting_msteams/test_internal.py b/tests/hosting_msteams/test_internal.py index 1ea3a2f9..3a8201fe 100644 --- a/tests/hosting_msteams/test_internal.py +++ b/tests/hosting_msteams/test_internal.py @@ -13,49 +13,11 @@ ) if is_supported_version: - from microsoft_teams.api import ApiClient - - from microsoft_agents.hosting.msteams._teams_api_client import ( - _get_teams_api_client, - ) from microsoft_agents.hosting.msteams.errors.error_resources import ( TeamsErrorResources, ) -class _FakeServices: - def __init__(self, values=None): - self._values = values or {} - - def get(self, key): - return self._values.get(key) - - -class _FakeContext: - """Minimal stand-in exposing only the ``services`` accessor reads.""" - - def __init__(self, services): - self.services = services - - -class TestGetTeamsApiClient: - - def test_returns_cached_api_client(self): - client = ApiClient("https://smba.trafficmanager.net/teams/") - ctx = _FakeContext(_FakeServices({ApiClient: client})) - assert _get_teams_api_client(ctx) is client - - def test_raises_when_missing(self): - ctx = _FakeContext(_FakeServices()) - with pytest.raises(ValueError, match="Teams API client"): - _get_teams_api_client(ctx) - - def test_raises_when_wrong_type(self): - ctx = _FakeContext(_FakeServices({ApiClient: object()})) - with pytest.raises(ValueError, match="Teams API client"): - _get_teams_api_client(ctx) - - class TestTeamsErrorResources: def _error_messages(self): diff --git a/tests/hosting_msteams/test_teams_agent_extension.py b/tests/hosting_msteams/test_teams_agent_extension.py index c8da5a82..74cb2982 100644 --- a/tests/hosting_msteams/test_teams_agent_extension.py +++ b/tests/hosting_msteams/test_teams_agent_extension.py @@ -125,7 +125,6 @@ async def test_teams_channel_deserializes_channel_data(self): assert result is True assert isinstance(activity.channel_data, ChannelData) assert activity.channel_data.channel.id == "c1" - assert ctx.services.has(ApiClient) @pytest.mark.asyncio async def test_teams_channel_without_channel_data_sets_none(self): diff --git a/tests/hosting_msteams/test_teams_turn_context.py b/tests/hosting_msteams/test_teams_turn_context.py index 73df5720..a2e6009f 100644 --- a/tests/hosting_msteams/test_teams_turn_context.py +++ b/tests/hosting_msteams/test_teams_turn_context.py @@ -3,6 +3,8 @@ """Tests for TeamsTurnContext helpers that can be exercised without a live adapter.""" +from types import SimpleNamespace + import pytest from .helpers import is_supported_version @@ -17,11 +19,19 @@ Activity, ActivityTreatmentTypes, Entity, + ResourceResponse, ) + from microsoft_teams.api import ApiClient + from microsoft_agents.hosting.core import TurnContext from microsoft_agents.hosting.msteams import TeamsTurnContext +class _StubAdapter: + async def send_activities(self, context, activities): + return [ResourceResponse()] * len(activities) + + class TestMakeTargetedActivity: """``_make_targeted_activity`` mutates the supplied activity in place (returns None) by appending a TARGETED activity-treatment entity.""" @@ -52,3 +62,22 @@ def test_each_call_appends_another_treatment(self): if getattr(e, "treatment", None) == ActivityTreatmentTypes.TARGETED ] assert len(treatments) == 2 + + +class TestTeamsApiClient: + + def test_api_client_returns_cached_client(self): + activity = Activity( + type="message", + channel_id="msteams", + service_url="https://smba.trafficmanager.net/teams/", + ) + context = TurnContext(_StubAdapter(), activity) + client = object.__new__(ApiClient) + context.services.set(ApiClient, client) + + teams_context = TeamsTurnContext( + context, SimpleNamespace(connection_manager=object()) + ) + + assert teams_context.api_client is client