8000 start BotContext tests, refactor BotFrameworkAdapter and BotContext · southworks/botbuilder-python@708fea0 · GitHub < 8000 /head>
[go: up one dir, main page]

Skip to content

Commit 708fea0

Browse files
committed
start BotContext tests, refactor BotFrameworkAdapter and BotContext
1 parent 3b545a0 commit 708fea0

File tree

5 files changed

+187
-54
lines changed

5 files changed

+187
-54
lines changed

libraries/botbuilder-core/botbuilder/core/bot_context.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,39 @@
88
from typing import List, Callable, Iterable, Tuple
99
from botbuilder.schema import Activity, ActivityTypes, ConversationReference, ResourceResponse
1010

11-
# from .bot_adapter import BotAdapter
12-
1311

1412
class BotContext(object):
1513
def __init__(self, adapter, request: Activity):
1614
self.adapter = adapter
17-
self.request: Activity = request
15+
self.activity = request
1816
self.responses: List[Activity] = []
1917
self._services: dict = {}
20-
self._responded: bool = False
21-
self._on_send_activity: Callable[[]] = []
18+
self._on_send_activities: Callable[[]] = []
2219
self._on_update_activity: Callable[[]] = []
2320
self._on_delete_activity: Callable[[]] = []
21+
self.__responded: bool = False
2422

25-
if not self.request:
23+
if self.adapter is None:
24+
raise TypeError('BotContext must be instantiated with an adapter.')
25+
if self.activity is None:
2626
raise TypeError('BotContext must be instantiated with a request parameter of type Activity.')
2727

28+
def copy_to(self, context: 'BotContext') -> None:
29+
for attribute in ['adapter', 'activity', 'responded', '_services',
30+
'_on_send_activities', '_on_update_activity', '_on_delete_activity']:
31+
setattr(context, attribute, getattr(self, attribute))
32+
33+
@property
34+
def responded(self):
35+
return self.__responded
36+
37+
@responded.setter
38+
def responded(self, value):
39+
if type(value) != bool or value is False:
40+
raise ValueError('BotContext.responded(): cannot set BotContext.responded to False.')
41+
else:
42+
self.__responded = value
43+
2844
def get(self, key: str) -> object:
2945
if not key or not isinstance(key, str):
3046
raise TypeError('"key" must be a valid string.')
@@ -56,7 +72,7 @@ def set(self, key: str, value: object) -> None:
5672
self._services[key] = value
5773

5874
async def send_activity(self, *activity_or_text: Tuple[Activity, str]):
59-
reference = BotContext.get_conversation_reference(self.request)
75+
reference = BotContext.get_conversation_reference(self.activity)
6076
output = [BotContext.apply_conversation_reference(
6177
Activity(text=a, type='message') if isinstance(a, str) else a, reference)
6278
for a in activity_or_text]
@@ -68,7 +84,7 @@ async def callback(context: 'BotContext', output):
6884
context._responded = True
6985
return responses
7086

71-
await self._emit(self._on_send_activity, output, callback(self, output))
87+
await self._emit(self._on_send_activities, output, callback(self, output))
7288

7389
async def update_activity(self, activity: Activity):
7490
return await self._emit(self._on_update_activity, activity, self.adapter.update_activity(self, activity))

libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py

Lines changed: 82 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44
import asyncio
55
from typing import List, Callable
6-
from botbuilder.schema import Activity, ChannelAccount, ConversationReference, ConversationsResult
6+
from botbuilder.schema import (Activity, ChannelAccount,
7+
ConversationAccount,
8+
ConversationParameters, ConversationReference,
9+
ConversationsResult, ConversationResourceResponse)
710
from botframework.connector import ConnectorClient
811
from botframework.connector.auth import (MicrosoftAppCredentials,
912
JwtTokenValidation, SimpleCredentialProvider)
@@ -29,12 +32,56 @@ def __init__(self, settings: BotFrameworkAdapterSettings):
2932
self._credentials = MicrosoftAppCredentials(self.settings.app_id, self.settings.app_password)
3033
self._credential_provider = SimpleCredentialProvider(self.settings.app_id, self.settings.app_password)
3134

32-
async def process_request(self, req, auth_header: str, logic: Callable):
33-
request = await self.parse_request(req)
35+
async def continue_conversation(self, reference: ConversationReference, logic):
36+
"""
37+
Continues a conversation with a user. This is often referred to as the bots "Proactive Messaging"
38+
flow as its lets the bot proactively send messages to a conversation or user that its already
39+
communicated with. Scenarios like sending notifications or coupons to a user are enabled by this
40+
method.
41+
:param reference:
42+
:param logic:
43+
:return:
44+
"""
45+
request = BotContext.apply_conversation_reference(Activity(), reference, is_incoming=True)
46+
context = self.create_context(request)
47+
return await self.run_middleware(context, logic)
48+
49+
async def create_conversation(self, reference: ConversationReference, logic):
50+
try:
51+
if reference.service_url is None:
52+
raise TypeError('BotFrameworkAdapter.create_conversation(): reference.service_url cannot be None.')
53+
54+
# Create conversation
55+
parameters = ConversationParameters(bot=reference.bot)
56+
client = self.create_connector_client(reference.service_url)
57+
58+
resource_response = await client.conversations.create_conversation_async(parameters)
59+
request = BotContext.apply_conversation_reference(Activity(), reference, is_incoming=True)
60+
request.conversation = ConversationAccount(id=resource_response.id)
61+
if resource_response.service_url:
62+
request.service_url = resource_response.service_url
63+
64+
context = self.create_context(request)
65+
return await self.run_middleware(context, logic)
66+
67+
except Exception as e:
68+
raise e
69+
70+
async def process_activity(self, req, auth_header: str, logic: Callable):
71+
"""
72+
Processes an activity received by the bots web server. This includes any messages sent from a
73+
user and is the method that drives what's often referred to as the bots "Reactive Messaging"
74+
flow.
75+
:param req:
76+
:param auth_header:
77+
:param logic:
78+
:return:
79+
"""
80+
activity = await self.parse_request(req)
3481
auth_header = auth_header or ''
3582

36-
await self.authenticate_request(request, auth_header)
37-
context = self.create_context(request)
83+
await self.authenticate_request(activity, auth_header)
84+
context = self.create_context(activity)
3885

3986
return await self.run_middleware(context, logic)
4087

@@ -84,22 +131,34 @@ async def validate_activity(activity: Activity):
84131
return req
85132

86133
async def update_activity(self, context: BotContext, activity: Activity):
134+
"""
135+
Replaces an activity that was previously sent to a channel. It should be noted that not all
136+
channels support this feature.
137+
:param context:
138+
:param activity:
139+
:return:
140+
"""
87141
try:
88-
connector_client = ConnectorClient(self._credentials, activity.service_url)
89-
connector_client.config.add_user_agent(USER_AGENT)
90-
return await connector_client.conversations.update_activity_async(
142+
client = self.create_connector_client(activity.service_url)
143+
return await client.conversations.update_activity_async(
91144
activity.conversation.id,
92145
activity.conversation.activity_id,
93146
activity)
94147
except Exception as e:
95148
raise e
96149

97150
async def delete_activity(self, context: BotContext, conversation_reference: ConversationReference):
151+
"""
152+
Deletes an activity that was previously sent to a channel. It should be noted that not all
153+
channels support this feature.
154+
:param context:
155+
:param conversation_reference:
156+
:return:
157+
"""
98158
try:
99-
connector_client = ConnectorClient(self._credentials, conversation_reference.service_url)
100-
connector_client.config.add_user_agent(USER_AGENT)
101-
await connector_client.conversations.delete_activity_async(conversation_reference.conversation.id,
102-
conversation_reference.activity_id)
159+
client = self.create_connector_client(conversation_reference.service_url)
160+
await client.conversations.delete_activity_async(conversation_reference.conversation.id,
161+
conversation_reference.activity_id)
103162
except Exception as e:
104163
raise e
105164

@@ -116,9 +175,8 @@ async def send_activity(self, context: BotContext, activities: List[Activity]):
116175
else:
117176
await asyncio.sleep(delay_in_ms)
118177
else:
119-
connector_client = ConnectorClient(self._credentials, activity.service_url)
120-
connector_client.config.add_user_agent(USER_AGENT)
121-
await connector_client.conversations.send_to_conversation_async(activity.conversation.id, activity)
178+
client = self.create_connector_client(activity.service_url)
179+
await client.conversations.send_to_conversation_async(activity.conversation.id, activity)
122180
except Exception as e:
123181
raise e
124182

@@ -137,8 +195,7 @@ async def delete_conversation_member(self, context: BotContext, member_id: str)
137195
'conversation.id')
138196
service_url = context.request.service_url
139197
conversation_id = context.request.conversation.id
140-
client = ConnectorClient(self._credentials, service_url)
141-
client.config.add_user_agent(USER_AGENT)
198+
client = self.create_connector_client(service_url)
142199
return await client.conversations.delete_conversation_member_async(conversation_id, member_id)
143200
except AttributeError as attr_e:
144201
raise attr_e
@@ -164,8 +221,7 @@ async def get_activity_members(self, context: BotContext, activity_id: str):
164221
'context.activity.id')
165222
service_url = context.request.service_url
166223
conversation_id = context.request.conversation.id
167-
client = ConnectorClient(self._credentials, service_url)
168-
client.config.add_user_agent(USER_AGENT)
224+
client = self.create_connector_client(service_url)
169225
return await client.conversations.get_activity_members_async(conversation_id, activity_id)
170226
except Exception as e:
171227
raise e
@@ -184,8 +240,7 @@ async def get_conversation_members(self, context: BotContext):
184240
'conversation.id')
185241
service_url = context.request.service_url
186242
conversation_id = context.request.conversation.id
187-
client = ConnectorClient(self._credentials, service_url)
188-
client.config.add_user_agent(USER_AGENT)
243+
client = self.create_connector_client(service_url)
189244
return await client.conversations.get_conversation_members_async(conversation_id)
190245
except Exception as e:
191246
raise e
@@ -199,6 +254,10 @@ async def get_conversations(self, service_url: str, continuation_token: str=None
199254
:param continuation_token:
200255
:return:
201256
"""
202-
client = ConnectorClient(self._credentials, service_url)
203-
client.config.add_user_agent(USER_AGENT)
257+
client = self.create_connector_client(service_url)
204258
return await client.conversations.get_conversations_async(continuation_token)
259+
260+
def create_connector_client(self, service_url: str) -> ConnectorClient:
261+
client = ConnectorClient(self._credentials, base_url=service_url)
262+
client.config.add_user_agent(USER_AGENT)
263+
return client
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import pytest
5+
6+
from botbuilder.schema import Activity
7+
from botbuilder.core import BotContext, TestAdapter
8+
9+
ACTIVITY = Activity(id='1', type='message', text='test')
10+
ADAPTER = TestAdapter(None)
11+
12+
13+
class TestBotContext:
14+
def test_should_create_context_with_request_and_adapter(self):
15+
context = BotContext(ADAPTER, ACTIVITY)
16+
17+
def test_should_not_create_context_without_request(self):
18+
try:
19+
context = BotContext(ADAPTER, None)
20+
except TypeError:
21+
pass
22+
except Exception as e:
23+
raise e
24+
25+
def test_should_not_create_context_without_adapter(self):
26+
try:
27+
context = BotContext(None, ACTIVITY)
28+
except TypeError:
29+
pass
30+
except Exception as e:
31+
raise e
32+
33+
def test_copy_to_should_copy_references(self):
34+
old_adapter = TestAdapter(None)
35+
old_activity = Activity(id='2', type='message', text='test copy')
36+
old_context = BotContext(old_adapter, old_activity)
37+
old_context.responded = True
38+
39+
adapter = TestAdapter(None)
40+
new_context = BotContext(adapter, ACTIVITY)
41+
old_context.copy_to(new_context)
42+
43+
assert new_context.adapter == old_adapter
44+
assert new_context.activity == old_activity
45+
assert new_context.responded is True
46+
47+
def test_should_not_be_able_to_set_responded_to_True(self):
48+
context = BotContext(ADAPTER, ACTIVITY)
49+
context.responded = True
50+
51+
def test_should_not_be_able_to_set_responded_to_False(self):
52+
context = BotContext(ADAPTER, ACTIVITY)
53+
try:
54+
context.responded = False
55+
except ValueError:
56+
pass
57+
except Exception as e:
58+
raise e

libraries/botbuilder-core/tests/test_test_adapter.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,47 +16,47 @@ class TestTestAdapter:
1616
async def test_should_call_bog_logic_when_receive_activity_is_called(self):
1717
async def logic(context: BotContext):
1818
assert context
19-
assert context.request
20-
assert context.request.type == 'message'
21-
assert context.request.text == 'test'
22-
assert context.request.id
23-
assert context.request.from_property
24-
assert context.request.recipient
25-
assert context.request.conversation
26-
assert context.request.channel_id
27-
assert context.request.service_url
19+
assert context.activity
20+
assert context.activity.type == 'message'
21+
assert context.activity.text == 'test'
22+
assert context.activity.id
23+
assert context.activity.from_property
24+
assert context.activity.recipient
25+
assert context.activity.conversation
26+
assert context.activity.channel_id
27+
assert context.activity.service_url
2828
adapter = TestAdapter(logic)
2929
await adapter.receive_activity('test')
3030

3131
@pytest.mark.asyncio
3232
async def test_should_support_receive_activity_with_activity(self):
3333
async def logic(context: BotContext):
34-
assert context.request.type == 'message'
35-
assert context.request.text == 'test'
34+
assert context.activity.type == 'message'
35+
assert context.activity.text == 'test'
3636
adapter = TestAdapter(logic)
3737
await adapter.receive_activity(Activity(type='message', text='test'))
3838

3939
@pytest.mark.asyncio
4040
async def test_should_set_activity_type_when_receive_activity_receives_activity_without_type(self):
4141
async def logic(context: BotContext):
42-
assert context.request.type == 'message'
43-
assert context.request.text == 'test'
42+
assert context.activity.type == 'message'
43+
assert context.activity.text == 'test'
4444
adapter = TestAdapter(logic)
4545
await adapter.receive_activity(Activity(text='test'))
4646

4747
@pytest.mark.asyncio
4848
async def test_should_support_custom_activity_id_in_receive_activity(self):
4949
async def logic(context: BotContext):
50-
assert context.request.id == 'myId'
51-
assert context.request.type == 'message'
52-
assert context.request.text == 'test'
50+
assert context.activity.id == 'myId'
51+
assert context.activity.type == 'message'
52+
assert context.activity.text == 'test'
5353
adapter = TestAdapter(logic)
5454
await adapter.receive_activity(Activity(type='message', text='test', id='myId'))
5555

5656
@pytest.mark.asyncio
5757
async def test_should_call_bot_logic_when_send_is_called(self):
5858
async def logic(context: BotContext):
59-
assert context.request.text == 'test'
59+
assert context.activity.text == 'test'
6060
adapter = TestAdapter(logic)
6161
await adapter.send('test')
6262

samples/Echo.Connector.Bot.Adapter/main.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ async def create_reply_activity(request_activity, text) -> Activity:
2424

2525

2626
async def handle_message(context: BotContext) -> web.Response:
27-
response = await create_reply_activity(context.request, 'You said %s.' % context.request.text)
27+
response = await create_reply_activity(context.activity, 'You said %s.' % context.activity.text)
2828
await context.send_activity(response)
2929
return web.Response(status=202)
3030

3131

3232
async def handle_conversation_update(context: BotContext) -> web.Response:
33-
if context.request.members_added[0].id != context.request.recipient.id:
34-
response = await create_reply_activity(context.request, 'Welcome to the Echo Adapter Bot!')
33+
if context.activity.members_added[0].id != context.activity.recipient.id:
34+
response = await create_reply_activity(context.activity, 'Welcome to the Echo Adapter Bot!')
3535
await context.send_activity(response)
3636
return web.Response(status=200)
3737

@@ -41,9 +41,9 @@ async def unhandled_activity() -> web.Response:
4141

4242

4343
async def request_handler(context: BotContext) -> web.Response:
44-
if context.request.type == 'message':
44+
if context.activity.type == 'message':
4545
return await handle_message(context)
46-
elif context.request.type == 'conversationUpdate':
46+
elif context.activity.type == 'conversationUpdate':
4747
return await handle_conversation_update(context)
4848
else:
4949
return await unhandled_activity()
@@ -54,7 +54,7 @@ async def messages(req: web.web_request) -> web.Response:
5454
activity = Activity().deserialize(body)
5555
auth_header = req.headers['Authorization'] if 'Authorization' in req.headers else ''
5656
try:
57-
return await ADAPTER.process_request(activity, auth_header, request_handler)
57+
return await ADAPTER.process_activity(activity, auth_header, request_handler)
5858
except Exception as e:
5959
raise e
6060

0 commit comments

Comments
 (0)
0