8000 add MemoryStorage, Storage classes and tests; refactor a little · rsliang/botbuilder-python@9cb287f · GitHub
[go: up one dir, main page]

Skip to content

Commit 9cb287f

Browse files
committed
add MemoryStorage, Storage classes and tests; refactor a little
1 parent 95b5c03 commit 9cb287f

File tree

9 files changed

+374
-12
lines changed

9 files changed

+374
-12
lines changed

libraries/botbuilder-core/botbuilder/core/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,20 @@
1010
from .bot_adapter import BotAdapter
1111
from .bot_framework_adapter import BotFrameworkAdapter, BotFrameworkAdapterSettings
1212
from .bot_context import BotContext
13+
from .memory_storage import MemoryStorage
1314
from .middleware_set import AnonymousReceiveMiddleware, Middleware, MiddlewareSet
15+
from .storage import Storage, StoreItem
1416
from .test_adapter import TestAdapter
1517

1618
__all__ = ['AnonymousReceiveMiddleware',
1719
'BotAdapter',
1820
'BotContext',
1921
'BotFrameworkAdapter',
2022
'BotFrameworkAdapterSettings',
23+
'MemoryStorage',
2124
'Middleware',
2225
'MiddlewareSet',
26+
'Storage',
27+
'StoreItem',
2328
'TestAdapter',
2429
'__version__',]

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

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@
22
# Licensed under the MIT License.
33

44
import asyncio
5-
import sys
6-
from copy import deepcopy, copy
5+
from copy import copy
76
from uuid import uuid4
8-
from typing import List, Callable, Iterable, Union
9-
from botbuilder.schema import Activity, ActivityTypes, ConversationReference, ResourceResponse
7+
from typing import List, Callable, Union
8+
from botbuilder.schema import Activity, ConversationReference, ResourceResponse
109

1110

1211
class BotContext(object):
1312
def __init__(self, adapter_or_context, request: Activity=None):
13+
"""
14+
Creates a new BotContext instance.
15+
:param adapter_or_context:
16+
:param request:
17+
"""
1418
if isinstance(adapter_or_context, BotContext):
1519
adapter_or_context.copy_to(self)
1620
else:
@@ -29,23 +33,42 @@ de 67ED f __init__(self, adapter_or_context, request: Activity=None):
2933
raise TypeError('BotContext must be instantiated with a request parameter of type Activity.')
3034

3135
def copy_to(self, context: 'BotContext') -> None:
36+
"""
37+
Called when this TurnContext instance is passed into the constructor of a new TurnContext
38+
instance. Can be overridden in derived classes.
39+
:param context:
40+
:return:
41+
"""
3242
for attribute in ['adapter', 'activity', '_responded', '_services',
3343
'_on_send_activities', '_on_update_activity', '_on_delete_activity']:
3444
setattr(context, attribute, getattr(self, attribute))
3545

3646
@property
3747
def activity(self):
48+
"""
49+
The received activity.
50+
:return:
51+
"""
3852
return self._activity
3953

4054
@activity.setter
4155
def activity(self, value):
56+
"""
57+
Used to set BotContext._activity when a context object is created. Only takes instances of Activities.
58+
:param value:
59+
:return:
60+
"""
4261
if not isinstance(value, Activity):
4362
raise TypeError('BotContext: cannot set `activity` to a type other than Activity.')
4463
else:
4564
self._activity = value
4665

4766
@property
4867
def responded(self):
68+
"""
69+
If `true` at least one response has been sent for the current turn of conversation.
70+
:return:
71+
"""
4972
return self._responded['responded']
5073

5174
@responded.setter
@@ -85,7 +108,12 @@ def set(self, key: str, value: object) -> None:
85108

86109
self._services[key] = value
87110

88-
async def send_activity(self, *activity_or_text: Union[Activity, str]):
111+
async def send_activity(self, *activity_or_text: Union[Activity, str]) -> ResourceResponse:
112+
"""
113+
Sends a single activity or message to the user.
114+
:param activity_or_text:
115+
:return:
116+
"""
89117
reference = BotContext.get_conversation_reference(self.activity)
90118
output = [BotContext.apply_conversation_reference(
91119
Activity(text=a, type='message') if isinstance(a, str) else a, reference)
@@ -101,9 +129,19 @@ async def callback(context: 'BotContext', output):
101129
await self._emit(self._on_send_activities, output, callback(self, output))
102130

103131
async def update_activity(self, activity: Activity):
132+
"""
133+
Replaces an existing activity.
134+
:param activity:
135+
:return:
136+
"""
104137
return await self._emit(self._on_update_activity, activity, self.adapter.update_activity(self, activity))
105138

106139
async def delete_activity(self, reference: Union[str, ConversationReference]):
140+
"""
141+
Deletes an existing activity.
142+
:param reference:
143+
:return:
144+
"""
107145
return await self._emit(self._on_delete_activity, reference, self.adapter.delete_activity(self, reference))
108146

109147
def on_send_activities(self, handler) -> 'BotContext':
@@ -192,4 +230,3 @@ def apply_conversation_reference(activity: Activity,
192230
activity.reply_to_id = reference.activity_id
193231

194232
return activity
195-

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ async def continue_conversation(self, reference: ConversationReference, logic):
4747
return await self.run_middleware(context, logic)
4848

4949
async def create_conversation(self, reference: ConversationReference, logic):
50+
"""
51+
Starts a new conversation with a user. This is typically used to Direct Message (DM) a member
52+
of a group.
53+
:param reference:
54+
:param logic:
55+
:return:
56+
"""
5057
try:
5158
if reference.service_url is None:
5259
raise TypeError('BotFrameworkAdapter.create_conversation(): reference.service_url cannot be None.')
@@ -86,9 +93,20 @@ async def process_activity(self, req, auth_header: str, logic: Callable):
8693
return await self.run_middleware(context, logic)
8794

8895
async def authenticate_request(self, request: Activity, auth_header: str):
96+
"""
97+
Allows for the overriding of authentication in unit tests.
98+
:param request:
99+
:param auth_header:
100+
:return:
101+
"""
89102
await JwtTokenValidation.assert_valid_activity(request, auth_header, self._credential_provider)
90103

91104
def create_context(self, activity):
105+
"""
106+
Allows for the overriding of the context object in unit tests and derived adapters.
107+
:param activity:
108+
:return:
109+
"""
92110
return BotContext(self, activity)
93111

94112
@staticmethod
@@ -258,6 +276,11 @@ async def get_conversations(self, service_url: str, continuation_token: str=None
258276
return await client.conversations.get_conversations_async(continuation_token)
259277

260278
def create_connector_client(self, service_url: str) -> ConnectorClient:
279+
"""
280+
Allows for mocking of the connector client in unit tests.
281+
:param service_url:
282+
:return:
283+
"""
261284
client = ConnectorClient(self._credentials, base_url=service_url)
262285
client.config.add_user_agent(USER_AGENT)
263286
return client
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from typing import Dict, List
5+
from copy import deepcopy
6+
from .storage import Storage, StoreItem
7+
8+
9+
class MemoryStorage(Storage):
10+
def __init__(self, dictionary=None):
11+
super(MemoryStorage, self).__init__()
12+
self.memory = dictionary or {}
13+
self._e_tag = 0
14+
15+
async def delete(self, keys: List[str]):
16+
try:
17+
for key in keys:
18+
if key in self.memory:
19+
del self.memory[key]
20+
except Exception as e:
21+
raise e
22+
23+
async def read(self, keys: List[str]):
24+
data = {}
25+
try:
26+
for key in keys:
27+
if key in self.memory:
28+
if self.memory:
29+
data[key] = deepcopy(self.memory[key])
30+
else:
31+
data[key] = None
32+
except Exception as e:
33+
raise e
34+
35+
return data
36+
37+
async def write(self, changes: Dict[str, StoreItem]): # This needs to be reworked
38+
try:
39+
40+
# iterate over the changes
41+
for (key, change_key) in enumerate(changes):
42+
new_value = changes[change_key]
43+
old_value = None
44+
45+
# Check if the a matching key already exists in self.memory
46+
# If it exists then we want to cache its original value from memory
47+
if change_key in self.memory:
48+
old_value = self.memory[change_key]
49+
50+
write_changes = self.__should_write_changes(old_value, new_value)
51+
52+
if write_changes:
53+
new_store_item = new_value
54+
if new_store_item is not None:
55+
self._e_tag += 1
56+
new_store_item.e_tag = str(self._e_tag)
57+
self.memory[change_key] = new_store_item
58+
else:
59+
raise KeyError("MemoryStorage.write(): `e_tag` conflict or changes do not implement ABC"
60+
" `StoreItem`.")
61+
except Exception as e:
62+
raise e
63+
64+
def __should_write_changes(self, old_value: StoreItem, new_value: StoreItem) -> bool:
65+
"""
66+
Compares two StoreItems and their e_tags and returns True if the new_value should overwrite the old_value.
67+
Otherwise returns False.
68+
:param old_value:
69+
:param new_value:
70+
:return:
71+
"""
72+
73+
# If old_value is none or if the new_value's e_tag is '*', then we return True
74+
if old_value is None or (hasattr(new_value, 'e_tag') and new_value.e_tag == '*'):
75+
return True
76+
# If none of the above cases, we verify that e_tags exist on both arguments
77+
elif hasattr(new_value, 'e_tag') and hasattr(old_value, 'e_tag'):
78+
# And then we do a comparing between the old and new e_tag values to decide if the new data will be written
79+
if old_value.e_tag == new_value.e_tag or int(old_value.e_tag) <= int(new_value.e_tag):
80+
return True
81+
else:
82+
return False
83+
else:
84+
return False

libraries/botbuilder-core/botbuilder/core/middleware_set.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,18 @@ def __init__(self):
3232
super(MiddlewareSet, self).__init__()
3333
self._middleware = []
3434

35-
def use(self, middleware: Middleware):
35+
def use(self, *middleware: Middleware):
3636
"""
3737
Registers middleware plugin(s) with the bot or set.
3838
:param middleware :
3939
:return:
4040
"""
41-
if hasattr(middleware, 'on_process_request') and callable(middleware.on_process_request):
42-
self._middleware.append(middleware)
43-
return self
44-
else:
45-
raise TypeError('MiddlewareSet.use(): invalid middleware being added.')
41+
for (idx, m) in enumerate(middleware):
42+
if hasattr(m, 'on_process_request') and callable(m.on_process_request):
43+
self._middleware.append(m)
44+
return self
45+
else:
46+
raise TypeError('MiddlewareSet.use(): invalid middleware at index "%s" being added.' % idx)
4647

4748
async def receive_activity(self, context: BotContext):
4849
await self.receive_activity_internal(context, None)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from abc import ABC, abstractmethod
5+
from typing import List
6+
7+
8+
class Storage(ABC):
9+
@abstractmethod
10+
async def read(self, keys: List[str]):
11+
"""
12+
Loads store items from storage.
13+
:param keys:
14+
:return:
15+
"""
16+
raise NotImplementedError()
17+
18+
@abstractmethod
19+
async def write(self, changes):
20+
"""
21+
Saves store items to storage.
22+
:param changes:
23+
:return:
24+
"""
25+
raise NotImplementedError()
26+
27+
@abstractmethod
28+
async def delete(self, keys: List[str]):
29+
"""
30+
Removes store items from storage.
31+
:param keys:
32+
:return:
33+
"""
34+
raise NotImplementedError()
35+
36+
37+
class StoreItem(ABC):
38+
"""
39+
Object which is stored in Storage with an optional eTag.
40+
"""
41+
def __init__(self):
42+
self.e_tag = None

libraries/botbuilder-core/tests/test_bot_context.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ async def delete_activity(self, context, reference):
3838
assert reference is not None
3939
assert reference.activity_id == '1234'
4040

41+
4142
class TestBotContext:
4243
def test_should_create_context_with_request_and_adapter(self):
4344
context = BotContext(SimpleAdapter(), ACTIVITY)

0 commit comments

Comments
 (0)
0