Skip to content

Commit 9bdd0c4

Browse files
authored
Axsuarez/autosave middleware (#284)
* AutoSaveMiddleware added
1 parent f415ddd commit 9bdd0c4

File tree

5 files changed

+178
-1
lines changed

5 files changed

+178
-1
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88

99
from .about import __version__
1010
from .activity_handler import ActivityHandler
11+
from .auto_save_state_middleware import AutoSaveStateMiddleware
1112
from .bot_assert import BotAssert
1213
from .bot_adapter import BotAdapter
1314
from .bot_framework_adapter import BotFrameworkAdapter, BotFrameworkAdapterSettings
1415
from .bot_state import BotState
16+
from .bot_state_set import BotStateSet
1517
from .bot_telemetry_client import BotTelemetryClient
1618
from .card_factory import CardFactory
1719
from .conversation_state import ConversationState
@@ -33,11 +35,13 @@
3335
__all__ = [
3436
"ActivityHandler",
3537
"AnonymousReceiveMiddleware",
38+
"AutoSaveStateMiddleware",
3639
"BotAdapter",
3740
"BotAssert",
3841
"BotFrameworkAdapter",
3942
"BotFrameworkAdapterSettings",
4043
"BotState",
44+
"BotStateSet",
4145
"BotTelemetryClient",
4246
"calculate_change_hash",
4347
"CardFactory",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from typing import Awaitable, Callable, List, Union
2+
3+
from .bot_state import BotState
4+
from .bot_state_set import BotStateSet
5+
from .middleware_set import Middleware
6+
from .turn_context import TurnContext
7+
8+
9+
class AutoSaveStateMiddleware(Middleware):
10+
def __init__(self, bot_states: Union[List[BotState], BotStateSet] = None):
11+
if bot_states is None:
12+
bot_states = []
13+
if isinstance(bot_states, BotStateSet):
14+
self.bot_state_set: BotStateSet = bot_states
15+
else:
16+
self.bot_state_set: BotStateSet = BotStateSet(bot_states)
17+
18+
def add(self, bot_state: BotState) -> "AutoSaveStateMiddleware":
19+
if bot_state is None:
20+
raise TypeError("Expected BotState")
21+
22+
self.bot_state_set.add(bot_state)
23+
return self
24+
25+
async def on_process_request(
26+
self, context: TurnContext, logic: Callable[[TurnContext], Awaitable]
27+
):
28+
await logic()
29+
await self.bot_state_set.save_all_changes(context, False)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from asyncio import wait
2+
from typing import List
3+
from .bot_state import BotState
4+
from .turn_context import TurnContext
5+
6+
7+
class BotStateSet:
8+
def __init__(self, bot_states: List[BotState]):
9+
self.bot_states = list(bot_states)
10+
11+
def add(self, bot_state: BotState) -> "BotStateSet":
12+
if bot_state is None:
13+
raise TypeError("Expected BotState")
14+
15+
self.bot_states.append(bot_state)
16+
return self
17+
18+
async def load_all(self, turn_context: TurnContext, force: bool = False):
19+
await wait(
20+
[bot_state.load(turn_context, force) for bot_state in self.bot_states]
21+
)
22+
23+
async def save_all_changes(self, turn_context: TurnContext, force: bool = False):
24+
await wait(
25+
[
26+
bot_state.save_changes(turn_context, force)
27+
for bot_state in self.bot_states
28+
]
29+
)
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import aiounittest
2+
from botbuilder.core import AutoSaveStateMiddleware, BotState, TurnContext
3+
from botbuilder.core.adapters import TestAdapter
4+
from botbuilder.schema import Activity
5+
6+
7+
async def aux_func():
8+
return
9+
10+
11+
class BotStateMock(BotState):
12+
def __init__(self, state): # pylint: disable=super-init-not-called
13+
self.state = state
14+
self.assert_force = False
15+
self.read_called = False
16+
self.write_called = False
17+
18+
async def load(self, turn_context: TurnContext, force: bool = False) -> None:
19+
assert turn_context is not None, "BotStateMock.load() not passed context."
20+
if self.assert_force:
21+
assert force, "BotStateMock.load(): force not set."
22+
self.read_called = True
23+
24+
async def save_changes(
25+
self, turn_context: TurnContext, force: bool = False
26+
) -> None:
27+
assert (
28+
turn_context is not None
29+
), "BotStateMock.save_changes() not passed context."
30+
if self.assert_force:
31+
assert force, "BotStateMock.save_changes(): force not set."
32+
self.write_called = True
33+
34+
def get_storage_key(
35+
self, turn_context: TurnContext # pylint: disable=unused-argument
36+
) -> str:
37+
return ""
38+
39+
40+
class TestAutoSaveMiddleware(aiounittest.AsyncTestCase):
41+
async def test_should_add_and_call_load_all_on_single_plugin(self):
42+
adapter = TestAdapter()
43+
context = TurnContext(adapter, Activity())
44+
foo_state = BotStateMock({"foo": "bar"})
45+
bot_state_set = AutoSaveStateMiddleware().add(foo_state)
46+
await bot_state_set.bot_state_set.load_all(context)
47+
48+
async def test_should_add_and_call_load_all_on_multiple_plugins(self):
49+
adapter = TestAdapter()
50+
context = TurnContext(adapter, Activity())
51+
foo_state = BotStateMock({"foo": "bar"})
52+
bar_state = BotStateMock({"bar": "foo"})
53+
bot_state_set = AutoSaveStateMiddleware([foo_state, bar_state])
54+
await bot_state_set.bot_state_set.load_all(context)
55+
56+
async def test_should_add_and_call_save_all_changes_on_a_single_plugin(self):
57+
adapter = TestAdapter()
58+
context = TurnContext(adapter, Activity())
59+
foo_state = BotStateMock({"foo": "bar"})
60+
bot_state_set = AutoSaveStateMiddleware().add(foo_state)
61+
await bot_state_set.bot_state_set.save_all_changes(context)
62+
assert foo_state.write_called, "write not called for plugin."
63+
64+
async def test_should_add_and_call_save_all_changes_on_multiple_plugins(self):
65+
adapter = TestAdapter()
66+
context = TurnContext(adapter, Activity())
67+
foo_state = BotStateMock({"foo": "bar"})
68+
bar_state = BotStateMock({"bar": "foo"})
69+
autosave_middleware = AutoSaveStateMiddleware([foo_state, bar_state])
70+
await autosave_middleware.bot_state_set.save_all_changes(context)
71+
assert (
72+
foo_state.write_called or bar_state.write_called
73+
), "write not called for either plugin."
74+
assert foo_state.write_called, "write not called for 'foo_state' plugin."
75+
assert bar_state.write_called, "write not called for 'bar_state' plugin."
76+
77+
async def test_should_pass_force_flag_through_in_load_all_call(self):
78+
adapter = TestAdapter()
79+
context = TurnContext(adapter, Activity())
80+
foo_state = BotStateMock({"foo": "bar"})
81+
foo_state.assert_force = True
82+
autosave_middleware = AutoSaveStateMiddleware().add(foo_state)
83+
await autosave_middleware.bot_state_set.load_all(context, True)
84+
85+
async def test_should_pass_force_flag_through_in_save_all_changes_call(self):
86+
adapter = TestAdapter()
87+
context = TurnContext(adapter, Activity())
88+
foo_state = BotStateMock({"foo": "bar"})
89+
foo_state.assert_force = True
90+
autosave_middleware = AutoSaveStateMiddleware().add(foo_state)
91+
await autosave_middleware.bot_state_set.save_all_changes(context, True)
92+
93+
async def test_should_work_as_a_middleware_plugin(self):
94+
adapter = TestAdapter()
95+
context = TurnContext(adapter, Activity())
96+
foo_state = BotStateMock({"foo": "bar"})
97+
autosave_middleware = AutoSaveStateMiddleware().add(foo_state)
98+
await autosave_middleware.on_process_request(context, aux_func)
99+
assert foo_state.write_called, "save_all_changes() not called."
100+
101+
async def test_should_support_plugins_passed_to_constructor(self):
102+
adapter = TestAdapter()
103+
context = TurnContext(adapter, Activity())
104+
foo_state = BotStateMock({"foo": "bar"})
105+
autosave_middleware = AutoSaveStateMiddleware().add(foo_state)
106+
await autosave_middleware.on_process_request(context, aux_func)
107+
assert foo_state.write_called, "save_all_changes() not called."
108+
109+
async def test_should_not_add_any_bot_state_on_construction_if_none_are_passed_in(
110+
self
111+
):
112+
middleware = AutoSaveStateMiddleware()
113+
assert (
114+
not middleware.bot_state_set.bot_states
115+
), "should not have added any BotState."

libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/number_prompt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
from typing import Callable, Dict
55

6-
from babel.numbers import parse_decimal
76
from recognizers_number import recognize_number
87
from recognizers_text import Culture, ModelResult
8+
from babel.numbers import parse_decimal
99

1010
from botbuilder.core.turn_context import TurnContext
1111
from botbuilder.schema import ActivityTypes

0 commit comments

Comments
 (0)