Repository: microsoft/botbuilder-python

Issue:
PrivateConversationState
Feature parity with https://github.com/Microsoft/botbuilder-dotnet


Discussion:

Resolving patch:
diff --git a/libraries/botbuilder-core/botbuilder/core/__init__.py b/libraries/botbuilder-core/botbuilder/core/__init__.py
--- a/libraries/botbuilder-core/botbuilder/core/__init__.py
+++ b/libraries/botbuilder-core/botbuilder/core/__init__.py
@@ -23,6 +23,7 @@
 from .message_factory import MessageFactory
 from .middleware_set import AnonymousReceiveMiddleware, Middleware, MiddlewareSet
 from .null_telemetry_client import NullTelemetryClient
+from .private_conversation_state import PrivateConversationState
 from .recognizer import Recognizer
 from .recognizer_result import RecognizerResult, TopIntent
 from .show_typing_middleware import ShowTypingMiddleware
@@ -55,6 +56,7 @@
     "Middleware",
     "MiddlewareSet",
     "NullTelemetryClient",
+    "PrivateConversationState",
     "Recognizer",
     "RecognizerResult",
     "Severity",
diff --git a/libraries/botbuilder-core/botbuilder/core/private_conversation_state.py b/libraries/botbuilder-core/botbuilder/core/private_conversation_state.py
new file mode 100644
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/private_conversation_state.py
@@ -0,0 +1,36 @@
+from .bot_state import BotState
+from .turn_context import TurnContext
+from .storage import Storage
+
+
+class PrivateConversationState(BotState):
+    def __init__(self, storage: Storage, namespace: str = ""):
+        async def aux_func(context: TurnContext) -> str:
+            nonlocal self
+            return await self.get_storage_key(context)
+
+        self.namespace = namespace
+        super().__init__(storage, aux_func)
+
+    def get_storage_key(self, turn_context: TurnContext) -> str:
+        activity = turn_context.activity
+        channel_id = activity.channel_id if activity is not None else None
+
+        if not channel_id:
+            raise Exception("missing activity.channel_id")
+
+        if activity and activity.conversation and activity.conversation.id is not None:
+            conversation_id = activity.conversation.id
+        else:
+            raise Exception("missing activity.conversation.id")
+
+        if (
+            activity
+            and activity.from_property
+            and activity.from_property.id is not None
+        ):
+            user_id = activity.from_property.id
+        else:
+            raise Exception("missing activity.from_property.id")
+
+        return f"{channel_id}/conversations/{ conversation_id }/users/{ user_id }/{ self.namespace }"


Test patch:
diff --git a/libraries/botbuilder-core/tests/test_private_conversation_state.py b/libraries/botbuilder-core/tests/test_private_conversation_state.py
new file mode 100644
--- /dev/null
+++ b/libraries/botbuilder-core/tests/test_private_conversation_state.py
@@ -0,0 +1,36 @@
+import aiounittest
+
+from botbuilder.core import MemoryStorage, TurnContext, PrivateConversationState
+from botbuilder.core.adapters import TestAdapter
+from botbuilder.schema import Activity, ChannelAccount, ConversationAccount
+
+RECEIVED_MESSAGE = Activity(
+    text="received",
+    type="message",
+    channel_id="test",
+    conversation=ConversationAccount(id="convo"),
+    from_property=ChannelAccount(id="user"),
+)
+
+
+class TestPrivateConversationState(aiounittest.AsyncTestCase):
+    async def test_should_load_and_save_state_from_storage(self):
+        storage = MemoryStorage()
+        adapter = TestAdapter()
+        context = TurnContext(adapter, RECEIVED_MESSAGE)
+        private_conversation_state = PrivateConversationState(storage)
+
+        # Simulate a "Turn" in a conversation by loading the state,
+        # changing it and then saving the changes to state.
+        await private_conversation_state.load(context)
+        key = private_conversation_state.get_storage_key(context)
+        state = private_conversation_state.get(context)
+        assert state == {}, "State not loaded"
+        assert key, "Key not found"
+        state["test"] = "foo"
+        await private_conversation_state.save_changes(context)
+
+        # Check the storage to see if the changes to state were saved.
+        items = await storage.read([key])
+        assert key in items, "Saved state not found in storage."
+        assert items[key]["test"] == "foo", "Missing test value in stored state."