From 51e3cdabbcdceffd40fdfb2512b315d85d810b7c Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Wed, 7 Aug 2024 19:27:20 +0530 Subject: [PATCH 01/14] Bump PyYAML 6.0 to 6.0.1 (#2151) --- libraries/botframework-connector/tests/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/botframework-connector/tests/requirements.txt b/libraries/botframework-connector/tests/requirements.txt index dfbb418bd..d6c057b7e 100644 --- a/libraries/botframework-connector/tests/requirements.txt +++ b/libraries/botframework-connector/tests/requirements.txt @@ -1,5 +1,5 @@ pytest-cov>=2.6.0 pytest~=7.3.1 -pyyaml==6.0 +pyyaml==6.0.1 pytest-asyncio==0.15.1 ddt==1.2.1 \ No newline at end of file From 734b6d1e98b085e3ccbcecc95efc235df5680bc6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Aug 2024 13:52:55 -0500 Subject: [PATCH 02/14] Bump the pip group across 2 directories with 1 update (#2152) Bumps the pip group with 1 update in the /libraries/botbuilder-adapters-slack directory: [aiohttp](https://github.com/aio-libs/aiohttp). Bumps the pip group with 1 update in the /libraries/botbuilder-integration-aiohttp directory: [aiohttp](https://github.com/aio-libs/aiohttp). Updates `aiohttp` from 3.9.5 to 3.10.2 - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.5...v3.10.2) Updates `aiohttp` from 3.9.5 to 3.10.2 - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.5...v3.10.2) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production dependency-group: pip - dependency-name: aiohttp dependency-type: direct:production dependency-group: pip ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libraries/botbuilder-adapters-slack/requirements.txt | 2 +- libraries/botbuilder-integration-aiohttp/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/botbuilder-adapters-slack/requirements.txt b/libraries/botbuilder-adapters-slack/requirements.txt index 8d2c7b043..88ce85a22 100644 --- a/libraries/botbuilder-adapters-slack/requirements.txt +++ b/libraries/botbuilder-adapters-slack/requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.9.5 +aiohttp==3.10.2 pyslack botbuilder-core==4.17.0 slackclient diff --git a/libraries/botbuilder-integration-aiohttp/requirements.txt b/libraries/botbuilder-integration-aiohttp/requirements.txt index 9ce978580..6228ea580 100644 --- a/libraries/botbuilder-integration-aiohttp/requirements.txt +++ b/libraries/botbuilder-integration-aiohttp/requirements.txt @@ -1,4 +1,4 @@ msrest== 0.7.* botframework-connector==4.17.0 botbuilder-schema==4.17.0 -aiohttp==3.9.5 +aiohttp==3.10.2 From 86673e2054ac80b7ed1f0939a45739aa3afe17a5 Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Fri, 30 Aug 2024 12:34:54 -0500 Subject: [PATCH 03/14] Update botbuilder-python-ci.yml for Azure Pipelines --- pipelines/botbuilder-python-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipelines/botbuilder-python-ci.yml b/pipelines/botbuilder-python-ci.yml index 6460bb907..b622bab3f 100644 --- a/pipelines/botbuilder-python-ci.yml +++ b/pipelines/botbuilder-python-ci.yml @@ -59,7 +59,7 @@ jobs: pip install -r ./libraries/botbuilder-core/tests/requirements.txt pip install -r ./libraries/botbuilder-ai/tests/requirements.txt pip install coveralls - pip install pylint==2.17 + pip install pylint==3.2.6 pip install black==24.4.2 displayName: 'Install dependencies' From b80253582cfa57faef55d8579cc953fbb6f4beee Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Tue, 3 Sep 2024 22:39:58 +0530 Subject: [PATCH 04/14] aiohttp 3.10.2 (#2158) * aiohttp 3.10.2 * Fixing pylint errors --------- Co-authored-by: tracyboehrer --- libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py | 11 ++++++++++- libraries/botbuilder-ai/setup.py | 2 +- .../flask/flask_telemetry_middleware.py | 2 ++ .../botbuilder/dialogs/choices/choice_factory.py | 3 +-- .../botbuilder-dialogs/botbuilder/dialogs/dialog.py | 3 +-- .../aiohttp/streaming/aiohttp_web_socket.py | 2 ++ libraries/botbuilder-integration-aiohttp/setup.py | 2 +- .../setup.py | 2 +- .../botbuilder/schema/_models_py3.py | 6 +++--- 9 files changed, 22 insertions(+), 11 deletions(-) diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py b/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py index 825e08e8e..773c487e6 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py +++ b/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import asyncio import json from typing import Dict, List, NamedTuple, Union from aiohttp import ClientSession, ClientTimeout @@ -52,8 +53,16 @@ def __init__( opt = options or QnAMakerOptions() self._validate_options(opt) + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + instance_timeout = ClientTimeout(total=opt.timeout / 1000) - self._http_client = http_client or ClientSession(timeout=instance_timeout) + self._http_client = http_client or ClientSession( + timeout=instance_timeout, loop=loop + ) self.telemetry_client: Union[BotTelemetryClient, NullTelemetryClient] = ( telemetry_client or NullTelemetryClient() diff --git a/libraries/botbuilder-ai/setup.py b/libraries/botbuilder-ai/setup.py index 105f1a4c9..2242efed9 100644 --- a/libraries/botbuilder-ai/setup.py +++ b/libraries/botbuilder-ai/setup.py @@ -8,7 +8,7 @@ "azure-cognitiveservices-language-luis==0.2.0", "botbuilder-schema==4.17.0", "botbuilder-core==4.17.0", - "aiohttp==3.9.5", + "aiohttp==3.10.2", ] TESTS_REQUIRES = ["aiounittest>=1.1.0"] diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_middleware.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_middleware.py index 8003074c9..5cc2676f2 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_middleware.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/flask/flask_telemetry_middleware.py @@ -38,6 +38,8 @@ def __call__(self, environ, start_response): def process_request(self, environ) -> bool: """Process the incoming Flask request.""" + body_unicode = None + # Bot Service doesn't handle anything over 256k length = int(environ.get("CONTENT_LENGTH", "0")) if length > 256 * 1024: diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_factory.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_factory.py index 0e5edd8e1..ef1dfc117 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_factory.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_factory.py @@ -46,8 +46,7 @@ def for_channel( else: size = len(choice.value) - if size > max_title_length: - max_title_length = size + max_title_length = max(max_title_length, size) # Determine list style supports_suggested_actions = Channel.supports_suggested_actions( diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py index 22dfe342b..43cfe3052 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py @@ -176,7 +176,6 @@ def _register_source_location( Registers a SourceRange in the provided location. :param path: The path to the source file. :param line_number: The line number where the source will be located on the file. - :return: """ if path: # This will be added when debbuging support is ported. @@ -185,4 +184,4 @@ def _register_source_location( # start_point = SourcePoint(line_index = line_number, char_index = 0 ), # end_point = SourcePoint(line_index = line_number + 1, char_index = 0 ), # ) - return + pass diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/streaming/aiohttp_web_socket.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/streaming/aiohttp_web_socket.py index 2cd4ec13b..aa4a94a8e 100644 --- a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/streaming/aiohttp_web_socket.py +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/streaming/aiohttp_web_socket.py @@ -40,6 +40,8 @@ async def receive(self) -> WebSocketMessage: try: message = await self._aiohttp_ws.receive() + message_data = None + if message.type == WSMsgType.TEXT: message_data = list(str(message.data).encode("ascii")) elif message.type == WSMsgType.BINARY: diff --git a/libraries/botbuilder-integration-aiohttp/setup.py b/libraries/botbuilder-integration-aiohttp/setup.py index 891647bb7..a777d50c9 100644 --- a/libraries/botbuilder-integration-aiohttp/setup.py +++ b/libraries/botbuilder-integration-aiohttp/setup.py @@ -10,7 +10,7 @@ "botframework-connector==4.17.0", "botbuilder-core==4.17.0", "yarl>=1.8.1", - "aiohttp==3.9.5", + "aiohttp==3.10.2", ] root = os.path.abspath(os.path.dirname(__file__)) diff --git a/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py index d40487403..d8b0f09cf 100644 --- a/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py +++ b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py @@ -6,7 +6,7 @@ REQUIRES = [ "applicationinsights>=0.11.9", - "aiohttp==3.9.5", + "aiohttp==3.10.2", "botbuilder-schema==4.17.0", "botframework-connector==4.17.0", "botbuilder-core==4.17.0", diff --git a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py index b75cc9f82..9976c9809 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py @@ -641,7 +641,7 @@ def create_reply(self, text: str = None, locale: str = None): ), reply_to_id=( self.id - if not type == ActivityTypes.conversation_update + if type != ActivityTypes.conversation_update or self.channel_id not in ["directline", "webchat"] else None ), @@ -688,7 +688,7 @@ def create_trace( ), reply_to_id=( self.id - if not type == ActivityTypes.conversation_update + if type != ActivityTypes.conversation_update or self.channel_id not in ["directline", "webchat"] else None ), @@ -749,7 +749,7 @@ def get_conversation_reference(self): return ConversationReference( activity_id=( self.id - if not type == ActivityTypes.conversation_update + if type != ActivityTypes.conversation_update or self.channel_id not in ["directline", "webchat"] else None ), From f3d50f8c6c13b9015fdc126f922cdcdccb9998ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 09:20:49 -0500 Subject: [PATCH 05/14] Bump aiohttp (#2157) Bumps the pip group with 1 update in the /libraries/botbuilder-integration-aiohttp directory: [aiohttp](https://github.com/aio-libs/aiohttp). Updates `aiohttp` from 3.10.2 to 3.10.5 - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.10.2...v3.10.5) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production dependency-group: pip ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libraries/botbuilder-integration-aiohttp/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/botbuilder-integration-aiohttp/requirements.txt b/libraries/botbuilder-integration-aiohttp/requirements.txt index 6228ea580..de0f13750 100644 --- a/libraries/botbuilder-integration-aiohttp/requirements.txt +++ b/libraries/botbuilder-integration-aiohttp/requirements.txt @@ -1,4 +1,4 @@ msrest== 0.7.* botframework-connector==4.17.0 botbuilder-schema==4.17.0 -aiohttp==3.10.2 +aiohttp==3.10.5 From bb1561e8a1648e8d1d50c055c6fbaf7181ae7195 Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Wed, 4 Sep 2024 21:03:50 +0530 Subject: [PATCH 06/14] Fixing python313 deprecation warnings (#2159) * Fixing DeprecationWarnings in Python 3.13.0rc1 * Fix black issues * Fixing pylint issue --- .../botbuilder-ai/botbuilder/ai/luis/activity_util.py | 4 ++-- libraries/botbuilder-ai/tests/qna/test_qna.py | 4 +--- .../processor/telemetry_processor.py | 2 +- libraries/botbuilder-applicationinsights/setup.py | 2 +- .../botbuilder/core/adapters/test_adapter.py | 4 ++-- .../botbuilder/core/inspection/trace_activity.py | 8 ++++---- .../botbuilder/core/transcript_logger.py | 6 +++--- .../botbuilder-core/botbuilder/core/turn_context.py | 4 ++-- .../botbuilder/dialogs/prompts/oauth_prompt.py | 6 +++++- .../botbuilder-schema/botbuilder/schema/_models_py3.py | 6 +++--- libraries/botframework-connector/azure_bdist_wheel.py | 4 +--- .../botframework-connector/tests/requirements.txt | 5 +++-- .../botframework-connector/tests/test_attachments.py | 10 +++++++++- 13 files changed, 37 insertions(+), 28 deletions(-) diff --git a/libraries/botbuilder-ai/botbuilder/ai/luis/activity_util.py b/libraries/botbuilder-ai/botbuilder/ai/luis/activity_util.py index d2656a3ba..303917fbb 100644 --- a/libraries/botbuilder-ai/botbuilder/ai/luis/activity_util.py +++ b/libraries/botbuilder-ai/botbuilder/ai/luis/activity_util.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from datetime import datetime +from datetime import datetime, timezone from botbuilder.schema import ( Activity, @@ -51,7 +51,7 @@ def create_trace( reply = Activity( type=ActivityTypes.trace, - timestamp=datetime.utcnow(), + timestamp=datetime.now(timezone.utc), from_property=from_property, recipient=ChannelAccount( id=turn_activity.from_property.id, name=turn_activity.from_property.name diff --git a/libraries/botbuilder-ai/tests/qna/test_qna.py b/libraries/botbuilder-ai/tests/qna/test_qna.py index 236594ac0..8a3f595ed 100644 --- a/libraries/botbuilder-ai/tests/qna/test_qna.py +++ b/libraries/botbuilder-ai/tests/qna/test_qna.py @@ -347,8 +347,6 @@ async def test_trace_test(self): self._knowledge_base_id, trace_activity.value.knowledge_base_id ) - return result - async def test_returns_answer_with_timeout(self): question: str = "how do I clean the stove?" options = QnAMakerOptions(timeout=999999) @@ -823,7 +821,7 @@ async def test_call_train(self): QnAMaker, "call_train", return_value=None ) as mocked_call_train: qna = QnAMaker(QnaApplicationTest.tests_endpoint) - qna.call_train(feedback_records) + await qna.call_train(feedback_records) mocked_call_train.assert_called_once_with(feedback_records) diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/telemetry_processor.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/telemetry_processor.py index dfe451e3f..0802f3cdf 100644 --- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/telemetry_processor.py +++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/telemetry_processor.py @@ -3,7 +3,7 @@ import base64 import json from abc import ABC, abstractmethod -from _sha256 import sha256 +from hashlib import sha256 class TelemetryProcessor(ABC): diff --git a/libraries/botbuilder-applicationinsights/setup.py b/libraries/botbuilder-applicationinsights/setup.py index 0932ff98f..9573e27f2 100644 --- a/libraries/botbuilder-applicationinsights/setup.py +++ b/libraries/botbuilder-applicationinsights/setup.py @@ -12,7 +12,7 @@ ] TESTS_REQUIRES = [ "aiounittest==1.3.0", - "django==3.2.24", # For samples + "django==4.2.15", # For samples "djangorestframework==3.14.0", # For samples "flask==2.2.5", # For samples ] diff --git a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py index 77f566625..ebfeb303a 100644 --- a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py +++ b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py @@ -8,7 +8,7 @@ import asyncio import inspect import uuid -from datetime import datetime +from datetime import datetime, timezone from uuid import uuid4 from typing import Awaitable, Coroutine, Dict, List, Callable, Union from copy import copy @@ -155,7 +155,7 @@ async def process_activity( finally: self._conversation_lock.release() - activity.timestamp = activity.timestamp or datetime.utcnow() + activity.timestamp = activity.timestamp or datetime.now(timezone.utc) await self.run_pipeline(self.create_turn_context(activity), logic) async def send_activities( diff --git a/libraries/botbuilder-core/botbuilder/core/inspection/trace_activity.py b/libraries/botbuilder-core/botbuilder/core/inspection/trace_activity.py index 307ef64cd..37cb33151 100644 --- a/libraries/botbuilder-core/botbuilder/core/inspection/trace_activity.py +++ b/libraries/botbuilder-core/botbuilder/core/inspection/trace_activity.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from datetime import datetime +from datetime import datetime, timezone from typing import Dict, Union from botbuilder.core import BotState @@ -11,7 +11,7 @@ def make_command_activity(command: str) -> Activity: return Activity( type=ActivityTypes.trace, - timestamp=datetime.utcnow(), + timestamp=datetime.now(timezone.utc), name="Command", label="Command", value=command, @@ -22,7 +22,7 @@ def make_command_activity(command: str) -> Activity: def from_activity(activity: Activity, name: str, label: str) -> Activity: return Activity( type=ActivityTypes.trace, - timestamp=datetime.utcnow(), + timestamp=datetime.now(timezone.utc), name=name, label=label, value=activity, @@ -33,7 +33,7 @@ def from_activity(activity: Activity, name: str, label: str) -> Activity: def from_state(bot_state: Union[BotState, Dict]) -> Activity: return Activity( type=ActivityTypes.trace, - timestamp=datetime.utcnow(), + timestamp=datetime.now(timezone.utc), name="Bot State", label="BotState", value=bot_state, diff --git a/libraries/botbuilder-core/botbuilder/core/transcript_logger.py b/libraries/botbuilder-core/botbuilder/core/transcript_logger.py index e9536c1b6..5aa1ea726 100644 --- a/libraries/botbuilder-core/botbuilder/core/transcript_logger.py +++ b/libraries/botbuilder-core/botbuilder/core/transcript_logger.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. """Logs incoming and outgoing activities to a TranscriptStore..""" -import datetime +from datetime import datetime, timezone import copy import random import string @@ -86,11 +86,11 @@ async def send_activities_handler( prefix = "g_" + "".join( random.choice(alphanumeric) for i in range(5) ) - epoch = datetime.datetime.utcfromtimestamp(0) + epoch = datetime.fromtimestamp(0, timezone.utc) if cloned_activity.timestamp: reference = cloned_activity.timestamp else: - reference = datetime.datetime.today() + reference = datetime.now(timezone.utc) delta = (reference - epoch).total_seconds() * 1000 cloned_activity.id = f"{prefix}{delta}" await self.log_activity(transcript, cloned_activity) diff --git a/libraries/botbuilder-core/botbuilder/core/turn_context.py b/libraries/botbuilder-core/botbuilder/core/turn_context.py index 90ab99bd0..6c4a4eef3 100644 --- a/libraries/botbuilder-core/botbuilder/core/turn_context.py +++ b/libraries/botbuilder-core/botbuilder/core/turn_context.py @@ -3,7 +3,7 @@ import re from copy import copy, deepcopy -from datetime import datetime +from datetime import datetime, timezone from typing import List, Callable, Union, Dict from botframework.connector import Channels from botbuilder.schema import ( @@ -308,7 +308,7 @@ async def send_trace_activity( ) -> ResourceResponse: trace_activity = Activity( type=ActivityTypes.trace, - timestamp=datetime.utcnow(), + timestamp=datetime.now(timezone.utc), name=name, value=value, value_type=value_type, diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py index c5b066913..d31a0b56a 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py @@ -420,7 +420,11 @@ async def _recognize_token( ) elif OAuthPrompt._is_teams_verification_invoke(context): - code = context.activity.value["state"] + code = ( + context.activity.value.get("state", None) + if context.activity.value + else None + ) try: token = await _UserTokenAccess.get_user_token( context, self._settings, code diff --git a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py index 9976c9809..1b6a631c6 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py @@ -4,7 +4,7 @@ from typing import List from botbuilder.schema._connector_client_enums import ActivityTypes -from datetime import datetime +from datetime import datetime, timezone from enum import Enum from msrest.serialization import Model from msrest.exceptions import HttpOperationError @@ -630,7 +630,7 @@ def create_reply(self, text: str = None, locale: str = None): """ return Activity( type=ActivityTypes.message, - timestamp=datetime.utcnow(), + timestamp=datetime.now(timezone.utc), from_property=ChannelAccount( id=self.recipient.id if self.recipient else None, name=self.recipient.name if self.recipient else None, @@ -677,7 +677,7 @@ def create_trace( return Activity( type=ActivityTypes.trace, - timestamp=datetime.utcnow(), + timestamp=datetime.now(timezone.utc), from_property=ChannelAccount( id=self.recipient.id if self.recipient else None, name=self.recipient.name if self.recipient else None, diff --git a/libraries/botframework-connector/azure_bdist_wheel.py b/libraries/botframework-connector/azure_bdist_wheel.py index d33af36bd..56a1b0b20 100644 --- a/libraries/botframework-connector/azure_bdist_wheel.py +++ b/libraries/botframework-connector/azure_bdist_wheel.py @@ -555,9 +555,7 @@ def write_record(self, bdist_dir, distinfo_dir): for azure_sub_package in folder_with_init: init_file = os.path.join(bdist_dir, azure_sub_package, "__init__.py") if os.path.isfile(init_file): - logger.info( - "manually remove {} while building the wheel".format(init_file) - ) + logger.info("manually remove %s while building the wheel", init_file) os.remove(init_file) else: raise ValueError( diff --git a/libraries/botframework-connector/tests/requirements.txt b/libraries/botframework-connector/tests/requirements.txt index d6c057b7e..5f0d9558d 100644 --- a/libraries/botframework-connector/tests/requirements.txt +++ b/libraries/botframework-connector/tests/requirements.txt @@ -1,5 +1,6 @@ pytest-cov>=2.6.0 pytest~=7.3.1 pyyaml==6.0.1 -pytest-asyncio==0.15.1 -ddt==1.2.1 \ No newline at end of file +pytest-asyncio==0.23.8 +ddt==1.2.1 +setuptools==72.1.0 \ No newline at end of file diff --git a/libraries/botframework-connector/tests/test_attachments.py b/libraries/botframework-connector/tests/test_attachments.py index b6d171250..a4b8b36b8 100644 --- a/libraries/botframework-connector/tests/test_attachments.py +++ b/libraries/botframework-connector/tests/test_attachments.py @@ -46,7 +46,15 @@ def read_base64(path_to_file): return encoded_string -LOOP = asyncio.get_event_loop() +# Ensure there's an event loop and get the auth token +# LOOP = asyncio.get_event_loop() +try: + LOOP = asyncio.get_running_loop() +except RuntimeError: + LOOP = asyncio.new_event_loop() + asyncio.set_event_loop(LOOP) + +# Run the async function to get the auth token AUTH_TOKEN = LOOP.run_until_complete(get_auth_token()) From 693acf0f18330a236d9293e63731df1943f249dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 11:37:51 -0500 Subject: [PATCH 07/14] Bump the pip group across 3 directories with 1 update (#2163) Bumps the pip group with 1 update in the /libraries/botbuilder-core directory: [cryptography](https://github.com/pyca/cryptography). Bumps the pip group with 1 update in the /libraries/botbuilder-dialogs directory: [cryptography](https://github.com/pyca/cryptography). Bumps the pip group with 1 update in the /libraries/botframework-connector directory: [cryptography](https://github.com/pyca/cryptography). Updates `cryptography` from 42.0.4 to 43.0.1 - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/42.0.4...43.0.1) Updates `cryptography` from 42.0.4 to 43.0.1 - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/42.0.4...43.0.1) Updates `cryptography` from 42.0.4 to 43.0.1 - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/42.0.4...43.0.1) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production dependency-group: pip - dependency-name: cryptography dependency-type: direct:production dependency-group: pip - dependency-name: cryptography dependency-type: direct:production dependency-group: pip ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libraries/botbuilder-core/requirements.txt | 2 +- libraries/botbuilder-dialogs/requirements.txt | 2 +- libraries/botframework-connector/requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/botbuilder-core/requirements.txt b/libraries/botbuilder-core/requirements.txt index 6ce30f68f..4b9aabc5a 100644 --- a/libraries/botbuilder-core/requirements.txt +++ b/libraries/botbuilder-core/requirements.txt @@ -4,5 +4,5 @@ botbuilder-schema==4.17.0 botframework-streaming==4.17.0 requests==2.32.0 PyJWT==2.4.0 -cryptography==42.0.4 +cryptography==43.0.1 aiounittest==1.3.0 \ No newline at end of file diff --git a/libraries/botbuilder-dialogs/requirements.txt b/libraries/botbuilder-dialogs/requirements.txt index 920200124..d8f2cb4f2 100644 --- a/libraries/botbuilder-dialogs/requirements.txt +++ b/libraries/botbuilder-dialogs/requirements.txt @@ -4,5 +4,5 @@ botbuilder-schema==4.17.0 botbuilder-core==4.17.0 requests==2.32.0 PyJWT==2.4.0 -cryptography==42.0.4 +cryptography==43.0.1 aiounittest==1.3.0 diff --git a/libraries/botframework-connector/requirements.txt b/libraries/botframework-connector/requirements.txt index 0632606a7..5a6d8d4e9 100644 --- a/libraries/botframework-connector/requirements.txt +++ b/libraries/botframework-connector/requirements.txt @@ -2,5 +2,5 @@ msrest==0.7.* botbuilder-schema==4.17.0 requests==2.32.0 PyJWT==2.4.0 -cryptography==42.0.4 +cryptography==43.0.1 msal>=1.29.0 From 31dd5693bc10e79800d352c21869ae533ebcb74a Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Mon, 9 Sep 2024 21:00:44 +0530 Subject: [PATCH 08/14] Bump aiohttp (#2164) --- libraries/botbuilder-adapters-slack/requirements.txt | 2 +- libraries/botbuilder-ai/setup.py | 2 +- libraries/botbuilder-integration-aiohttp/setup.py | 2 +- .../botbuilder-integration-applicationinsights-aiohttp/setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/botbuilder-adapters-slack/requirements.txt b/libraries/botbuilder-adapters-slack/requirements.txt index 88ce85a22..98af56627 100644 --- a/libraries/botbuilder-adapters-slack/requirements.txt +++ b/libraries/botbuilder-adapters-slack/requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.10.2 +aiohttp==3.10.5 pyslack botbuilder-core==4.17.0 slackclient diff --git a/libraries/botbuilder-ai/setup.py b/libraries/botbuilder-ai/setup.py index 2242efed9..e842fdc34 100644 --- a/libraries/botbuilder-ai/setup.py +++ b/libraries/botbuilder-ai/setup.py @@ -8,7 +8,7 @@ "azure-cognitiveservices-language-luis==0.2.0", "botbuilder-schema==4.17.0", "botbuilder-core==4.17.0", - "aiohttp==3.10.2", + "aiohttp==3.10.5", ] TESTS_REQUIRES = ["aiounittest>=1.1.0"] diff --git a/libraries/botbuilder-integration-aiohttp/setup.py b/libraries/botbuilder-integration-aiohttp/setup.py index a777d50c9..1fe5d5ccc 100644 --- a/libraries/botbuilder-integration-aiohttp/setup.py +++ b/libraries/botbuilder-integration-aiohttp/setup.py @@ -10,7 +10,7 @@ "botframework-connector==4.17.0", "botbuilder-core==4.17.0", "yarl>=1.8.1", - "aiohttp==3.10.2", + "aiohttp==3.10.5", ] root = os.path.abspath(os.path.dirname(__file__)) diff --git a/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py index d8b0f09cf..30c45b2f7 100644 --- a/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py +++ b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py @@ -6,7 +6,7 @@ REQUIRES = [ "applicationinsights>=0.11.9", - "aiohttp==3.10.2", + "aiohttp==3.10.5", "botbuilder-schema==4.17.0", "botframework-connector==4.17.0", "botbuilder-core==4.17.0", From 1e5102b2e4b7dd6b8fd75d1553b4f282557e7e6e Mon Sep 17 00:00:00 2001 From: Nikita-Gz <61112041+Nikita-Gz@users.noreply.github.com> Date: Tue, 10 Sep 2024 22:38:03 +0300 Subject: [PATCH 09/14] Fixed type hint in turn_context.py (#2147) Previous type hint implied that send_activity will always return ResourceResponse, meanwhile it can also return None Co-authored-by: tracyboehrer --- libraries/botbuilder-core/botbuilder/core/turn_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/botbuilder-core/botbuilder/core/turn_context.py b/libraries/botbuilder-core/botbuilder/core/turn_context.py index 6c4a4eef3..852fd1f31 100644 --- a/libraries/botbuilder-core/botbuilder/core/turn_context.py +++ b/libraries/botbuilder-core/botbuilder/core/turn_context.py @@ -158,7 +158,7 @@ async def send_activity( activity_or_text: Union[Activity, str], speak: str = None, input_hint: str = None, - ) -> ResourceResponse: + ) -> Union[ResourceResponse, None]: """ Sends a single activity or message to the user. :param activity_or_text: From e493609b831f15c6f3dd80d939d9ad88b4ca2922 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 10 Sep 2024 20:38:23 +0100 Subject: [PATCH 10/14] Remove urllib3 version pin (#2144) Co-authored-by: tracyboehrer --- libraries/botbuilder-schema/setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/botbuilder-schema/setup.py b/libraries/botbuilder-schema/setup.py index 2075a5f20..43855c655 100644 --- a/libraries/botbuilder-schema/setup.py +++ b/libraries/botbuilder-schema/setup.py @@ -6,7 +6,10 @@ NAME = "botbuilder-schema" VERSION = os.environ["packageVersion"] if "packageVersion" in os.environ else "4.17.0" -REQUIRES = ["msrest== 0.7.*", "urllib3<2.0.0"] +REQUIRES = [ + "msrest== 0.7.*", + "urllib3", +] root = os.path.abspath(os.path.dirname(__file__)) From b3e74361f3802a003e9fb1bf59a5ad8054a7f67f Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Tue, 10 Sep 2024 15:11:09 -0500 Subject: [PATCH 11/14] Fixed ExpectReplies response (#2166) * Fixed ExpectReplies response * Fix formatting --------- Co-authored-by: Tracy Boehrer --- .../botbuilder-core/botbuilder/core/cloud_adapter_base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/botbuilder-core/botbuilder/core/cloud_adapter_base.py b/libraries/botbuilder-core/botbuilder/core/cloud_adapter_base.py index 454861257..0f695a2a7 100644 --- a/libraries/botbuilder-core/botbuilder/core/cloud_adapter_base.py +++ b/libraries/botbuilder-core/botbuilder/core/cloud_adapter_base.py @@ -436,7 +436,9 @@ def _process_turn_results(self, context: TurnContext) -> InvokeResponse: if context.activity.delivery_mode == DeliveryModes.expect_replies: return InvokeResponse( status=HTTPStatus.OK, - body=ExpectedReplies(activities=context.buffered_reply_activities), + body=ExpectedReplies( + activities=context.buffered_reply_activities + ).serialize(), ) # Handle Invoke scenarios where the bot will return a specific body and return code. From 50e72c0fa8a1991bb474e871987d8fff2b1bfc6e Mon Sep 17 00:00:00 2001 From: tracyboehrer Date: Tue, 10 Sep 2024 15:18:57 -0500 Subject: [PATCH 12/14] Updated azure-cosmos to 4.7.0, requiring dropped support for obsolete CosmosDBStorage class. (#2165) Co-authored-by: Tracy Boehrer --- .../botbuilder/azure/__init__.py | 4 +- .../azure/cosmosdb_partitioned_storage.py | 179 +++++---- .../botbuilder/azure/cosmosdb_storage.py | 378 ------------------ libraries/botbuilder-azure/setup.py | 2 +- .../tests/test_cosmos_partitioned_storage.py | 13 +- .../tests/test_cosmos_storage.py | 300 -------------- .../botbuilder/testing/storage_base_tests.py | 4 +- 7 files changed, 105 insertions(+), 775 deletions(-) delete mode 100644 libraries/botbuilder-azure/botbuilder/azure/cosmosdb_storage.py delete mode 100644 libraries/botbuilder-azure/tests/test_cosmos_storage.py diff --git a/libraries/botbuilder-azure/botbuilder/azure/__init__.py b/libraries/botbuilder-azure/botbuilder/azure/__init__.py index e625500a3..e6c70e7fc 100644 --- a/libraries/botbuilder-azure/botbuilder/azure/__init__.py +++ b/libraries/botbuilder-azure/botbuilder/azure/__init__.py @@ -7,10 +7,10 @@ from .about import __version__ from .azure_queue_storage import AzureQueueStorage -from .cosmosdb_storage import CosmosDbStorage, CosmosDbConfig, CosmosDbKeyEscape from .cosmosdb_partitioned_storage import ( CosmosDbPartitionedStorage, CosmosDbPartitionedConfig, + CosmosDbKeyEscape, ) from .blob_storage import BlobStorage, BlobStorageSettings @@ -18,8 +18,6 @@ "AzureQueueStorage", "BlobStorage", "BlobStorageSettings", - "CosmosDbStorage", - "CosmosDbConfig", "CosmosDbKeyEscape", "CosmosDbPartitionedStorage", "CosmosDbPartitionedConfig", diff --git a/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_partitioned_storage.py b/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_partitioned_storage.py index 982ac5974..cfe66f8d8 100644 --- a/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_partitioned_storage.py +++ b/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_partitioned_storage.py @@ -6,14 +6,14 @@ from typing import Dict, List from threading import Lock import json - +from hashlib import sha256 +from azure.core import MatchConditions from azure.cosmos import documents, http_constants from jsonpickle.pickler import Pickler from jsonpickle.unpickler import Unpickler import azure.cosmos.cosmos_client as cosmos_client # pylint: disable=no-name-in-module,import-error -import azure.cosmos.errors as cosmos_errors # pylint: disable=no-name-in-module,import-error +import azure.cosmos.exceptions as cosmos_exceptions from botbuilder.core.storage import Storage -from botbuilder.azure import CosmosDbKeyEscape class CosmosDbPartitionedConfig: @@ -63,6 +63,49 @@ def __init__( self.compatibility_mode = compatibility_mode or kwargs.get("compatibility_mode") +class CosmosDbKeyEscape: + @staticmethod + def sanitize_key( + key: str, key_suffix: str = "", compatibility_mode: bool = True + ) -> str: + """Return the sanitized key. + + Replace characters that are not allowed in keys in Cosmos. + + :param key: The provided key to be escaped. + :param key_suffix: The string to add a the end of all RowKeys. + :param compatibility_mode: True if keys should be truncated in order to support previous CosmosDb + max key length of 255. This behavior can be overridden by setting + cosmosdb_partitioned_config.compatibility_mode to False. + :return str: + """ + # forbidden characters + bad_chars = ["\\", "?", "/", "#", "\t", "\n", "\r", "*"] + # replace those with with '*' and the + # Unicode code point of the character and return the new string + key = "".join(map(lambda x: "*" + str(ord(x)) if x in bad_chars else x, key)) + + if key_suffix is None: + key_suffix = "" + + return CosmosDbKeyEscape.truncate_key(f"{key}{key_suffix}", compatibility_mode) + + @staticmethod + def truncate_key(key: str, compatibility_mode: bool = True) -> str: + max_key_len = 255 + + if not compatibility_mode: + return key + + if len(key) > max_key_len: + aux_hash = sha256(key.encode("utf-8")) + aux_hex = aux_hash.hexdigest() + + key = key[0 : max_key_len - len(aux_hex)] + aux_hex + + return key + + class CosmosDbPartitionedStorage(Storage): """A CosmosDB based storage provider using partitioning for a bot.""" @@ -99,7 +142,8 @@ async def read(self, keys: List[str]) -> Dict[str, object]: :return dict: """ if not keys: - raise Exception("Keys are required when reading") + # No keys passed in, no result to return. Back-compat with original CosmosDBStorage. + return {} await self.initialize() @@ -111,8 +155,8 @@ async def read(self, keys: List[str]) -> Dict[str, object]: key, self.config.key_suffix, self.config.compatibility_mode ) - read_item_response = self.client.ReadItem( - self.__item_link(escaped_key), self.__get_partition_key(escaped_key) + read_item_response = self.container.read_item( + escaped_key, self.__get_partition_key(escaped_key) ) document_store_item = read_item_response if document_store_item: @@ -122,13 +166,8 @@ async def read(self, keys: List[str]) -> Dict[str, object]: # When an item is not found a CosmosException is thrown, but we want to # return an empty collection so in this instance we catch and do not rethrow. # Throw for any other exception. - except cosmos_errors.HTTPFailure as err: - if ( - err.status_code - == cosmos_errors.http_constants.StatusCodes.NOT_FOUND - ): - continue - raise err + except cosmos_exceptions.CosmosResourceNotFoundError: + continue except Exception as err: raise err return store_items @@ -162,20 +201,16 @@ async def write(self, changes: Dict[str, object]): if e_tag == "": raise Exception("cosmosdb_storage.write(): etag missing") - access_condition = { - "accessCondition": {"type": "IfMatch", "condition": e_tag} - } - options = ( - access_condition if e_tag != "*" and e_tag and e_tag != "" else None - ) + access_condition = e_tag != "*" and e_tag and e_tag != "" + try: - self.client.UpsertItem( - database_or_Container_link=self.__container_link, - document=doc, - options=options, + self.container.upsert_item( + body=doc, + etag=e_tag if access_condition else None, + match_condition=( + MatchConditions.IfNotModified if access_condition else None + ), ) - except cosmos_errors.HTTPFailure as err: - raise err except Exception as err: raise err @@ -192,69 +227,66 @@ async def delete(self, keys: List[str]): key, self.config.key_suffix, self.config.compatibility_mode ) try: - self.client.DeleteItem( - document_link=self.__item_link(escaped_key), - options=self.__get_partition_key(escaped_key), + self.container.delete_item( + escaped_key, + self.__get_partition_key(escaped_key), ) - except cosmos_errors.HTTPFailure as err: - if ( - err.status_code - == cosmos_errors.http_constants.StatusCodes.NOT_FOUND - ): - continue - raise err + except cosmos_exceptions.CosmosResourceNotFoundError: + continue except Exception as err: raise err async def initialize(self): if not self.container: if not self.client: + connection_policy = self.config.cosmos_client_options.get( + "connection_policy", documents.ConnectionPolicy() + ) + + # kwargs 'connection_verify' is to handle CosmosClient overwriting the + # ConnectionPolicy.DisableSSLVerification value. self.client = cosmos_client.CosmosClient( self.config.cosmos_db_endpoint, - {"masterKey": self.config.auth_key}, - self.config.cosmos_client_options.get("connection_policy", None), + self.config.auth_key, self.config.cosmos_client_options.get("consistency_level", None), + **{ + "connection_policy": connection_policy, + "connection_verify": not connection_policy.DisableSSLVerification, + }, ) if not self.database: with self.__lock: - try: - if not self.database: - self.database = self.client.CreateDatabase( - {"id": self.config.database_id} - ) - except cosmos_errors.HTTPFailure: - self.database = self.client.ReadDatabase( - "dbs/" + self.config.database_id + if not self.database: + self.database = self.client.create_database_if_not_exists( + self.config.database_id ) self.__get_or_create_container() def __get_or_create_container(self): with self.__lock: - container_def = { - "id": self.config.container_id, - "partitionKey": { - "paths": ["/id"], - "kind": documents.PartitionKind.Hash, - }, + partition_key = { + "paths": ["/id"], + "kind": documents.PartitionKind.Hash, } try: if not self.container: - self.container = self.client.CreateContainer( - "dbs/" + self.database["id"], - container_def, - {"offerThroughput": self.config.container_throughput}, + self.container = self.database.create_container( + self.config.container_id, + partition_key, + offer_throughput=self.config.container_throughput, ) - except cosmos_errors.HTTPFailure as err: + except cosmos_exceptions.CosmosHttpResponseError as err: if err.status_code == http_constants.StatusCodes.CONFLICT: - self.container = self.client.ReadContainer( - "dbs/" + self.database["id"] + "/colls/" + container_def["id"] + self.container = self.database.get_container_client( + self.config.container_id ) - if "partitionKey" not in self.container: + properties = self.container.read() + if "partitionKey" not in properties: self.compatability_mode_partition_key = True else: - paths = self.container["partitionKey"]["paths"] + paths = properties["partitionKey"]["paths"] if "/partitionKey" in paths: self.compatability_mode_partition_key = True elif "/id" not in paths: @@ -267,7 +299,7 @@ def __get_or_create_container(self): raise err def __get_partition_key(self, key: str) -> str: - return None if self.compatability_mode_partition_key else {"partitionKey": key} + return None if self.compatability_mode_partition_key else key @staticmethod def __create_si(result) -> object: @@ -303,28 +335,3 @@ def __create_dict(store_item: object) -> Dict: # loop through attributes and write and return a dict return json_dict - - def __item_link(self, identifier) -> str: - """Return the item link of a item in the container. - - :param identifier: - :return str: - """ - return self.__container_link + "/docs/" + identifier - - @property - def __container_link(self) -> str: - """Return the container link in the database. - - :param: - :return str: - """ - return self.__database_link + "/colls/" + self.config.container_id - - @property - def __database_link(self) -> str: - """Return the database link. - - :return str: - """ - return "dbs/" + self.config.database_id diff --git a/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_storage.py b/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_storage.py deleted file mode 100644 index 2e383666f..000000000 --- a/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_storage.py +++ /dev/null @@ -1,378 +0,0 @@ -"""Implements a CosmosDB based storage provider. -""" - -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from hashlib import sha256 -from typing import Dict, List -from threading import Semaphore -import json -import warnings -from jsonpickle.pickler import Pickler -from jsonpickle.unpickler import Unpickler -import azure.cosmos.cosmos_client as cosmos_client # pylint: disable=no-name-in-module,import-error -import azure.cosmos.errors as cosmos_errors # pylint: disable=no-name-in-module,import-error -from botbuilder.core.storage import Storage - - -class CosmosDbConfig: - """The class for CosmosDB configuration for the Azure Bot Framework.""" - - def __init__( - self, - endpoint: str = None, - masterkey: str = None, - database: str = None, - container: str = None, - partition_key: str = None, - database_creation_options: dict = None, - container_creation_options: dict = None, - **kwargs, - ): - """Create the Config object. - - :param endpoint: - :param masterkey: - :param database: - :param container: - :param filename: - :return CosmosDbConfig: - """ - self.__config_file = kwargs.get("filename") - if self.__config_file: - kwargs = json.load(open(self.__config_file)) - self.endpoint = endpoint or kwargs.get("endpoint") - self.masterkey = masterkey or kwargs.get("masterkey") - self.database = database or kwargs.get("database", "bot_db") - self.container = container or kwargs.get("container", "bot_container") - self.partition_key = partition_key or kwargs.get("partition_key") - self.database_creation_options = database_creation_options or kwargs.get( - "database_creation_options" - ) - self.container_creation_options = container_creation_options or kwargs.get( - "container_creation_options" - ) - - -class CosmosDbKeyEscape: - @staticmethod - def sanitize_key( - key: str, key_suffix: str = "", compatibility_mode: bool = True - ) -> str: - """Return the sanitized key. - - Replace characters that are not allowed in keys in Cosmos. - - :param key: The provided key to be escaped. - :param key_suffix: The string to add a the end of all RowKeys. - :param compatibility_mode: True if keys should be truncated in order to support previous CosmosDb - max key length of 255. This behavior can be overridden by setting - cosmosdb_partitioned_config.compatibility_mode to False. - :return str: - """ - # forbidden characters - bad_chars = ["\\", "?", "/", "#", "\t", "\n", "\r", "*"] - # replace those with with '*' and the - # Unicode code point of the character and return the new string - key = "".join(map(lambda x: "*" + str(ord(x)) if x in bad_chars else x, key)) - - if key_suffix is None: - key_suffix = "" - - return CosmosDbKeyEscape.truncate_key(f"{key}{key_suffix}", compatibility_mode) - - @staticmethod - def truncate_key(key: str, compatibility_mode: bool = True) -> str: - max_key_len = 255 - - if not compatibility_mode: - return key - - if len(key) > max_key_len: - aux_hash = sha256(key.encode("utf-8")) - aux_hex = aux_hash.hexdigest() - - key = key[0 : max_key_len - len(aux_hex)] + aux_hex - - return key - - -class CosmosDbStorage(Storage): - """A CosmosDB based storage provider for a bot.""" - - def __init__( - self, config: CosmosDbConfig, client: cosmos_client.CosmosClient = None - ): - """Create the storage object. - - :param config: - """ - super(CosmosDbStorage, self).__init__() - warnings.warn( - "CosmosDbStorage is obsolete. Use CosmosDbPartitionedStorage instead." - ) - self.config = config - self.client = client or cosmos_client.CosmosClient( - self.config.endpoint, {"masterKey": self.config.masterkey} - ) - # these are set by the functions that check - # the presence of the database and container or creates them - self.database = None - self.container = None - self._database_creation_options = config.database_creation_options - self._container_creation_options = config.container_creation_options - self.__semaphore = Semaphore() - - async def read(self, keys: List[str]) -> Dict[str, object]: - """Read storeitems from storage. - - :param keys: - :return dict: - """ - try: - # check if the database and container exists and if not create - if not self.__container_exists: - self.__create_db_and_container() - if keys: - # create the parameters object - parameters = [ - { - "name": f"@id{i}", - "value": f"{CosmosDbKeyEscape.sanitize_key(key)}", - } - for i, key in enumerate(keys) - ] - # get the names of the params - parameter_sequence = ",".join(param.get("name") for param in parameters) - # create the query - query = { - "query": f"SELECT c.id, c.realId, c.document, c._etag FROM c WHERE c.id in ({parameter_sequence})", - "parameters": parameters, - } - - if self.config.partition_key: - options = {"partitionKey": self.config.partition_key} - else: - options = {"enableCrossPartitionQuery": True} - - # run the query and store the results as a list - results = list( - self.client.QueryItems(self.__container_link, query, options) - ) - # return a dict with a key and an object - return {r.get("realId"): self.__create_si(r) for r in results} - - # No keys passed in, no result to return. - return {} - except TypeError as error: - raise error - - async def write(self, changes: Dict[str, object]): - """Save storeitems to storage. - - :param changes: - :return: - """ - if changes is None: - raise Exception("Changes are required when writing") - if not changes: - return - try: - # check if the database and container exists and if not create - if not self.__container_exists: - self.__create_db_and_container() - # iterate over the changes - for key, change in changes.items(): - # store the e_tag - e_tag = None - if isinstance(change, dict): - e_tag = change.get("e_tag", None) - elif hasattr(change, "e_tag"): - e_tag = change.e_tag - # create the new document - doc = { - "id": CosmosDbKeyEscape.sanitize_key(key), - "realId": key, - "document": self.__create_dict(change), - } - if e_tag == "": - raise Exception("cosmosdb_storage.write(): etag missing") - # the e_tag will be * for new docs so do an insert - if e_tag == "*" or not e_tag: - self.client.UpsertItem( - database_or_Container_link=self.__container_link, - document=doc, - options={"disableAutomaticIdGeneration": True}, - ) - # if we have an etag, do opt. concurrency replace - elif e_tag: - access_condition = {"type": "IfMatch", "condition": e_tag} - self.client.ReplaceItem( - document_link=self.__item_link( - CosmosDbKeyEscape.sanitize_key(key) - ), - new_document=doc, - options={"accessCondition": access_condition}, - ) - except Exception as error: - raise error - - async def delete(self, keys: List[str]): - """Remove storeitems from storage. - - :param keys: - :return: - """ - try: - # check if the database and container exists and if not create - if not self.__container_exists: - self.__create_db_and_container() - - options = {} - if self.config.partition_key: - options["partitionKey"] = self.config.partition_key - - # call the function for each key - for key in keys: - self.client.DeleteItem( - document_link=self.__item_link(CosmosDbKeyEscape.sanitize_key(key)), - options=options, - ) - # print(res) - except cosmos_errors.HTTPFailure as http_failure: - # print(h.status_code) - if http_failure.status_code != 404: - raise http_failure - except TypeError as error: - raise error - - def __create_si(self, result) -> object: - """Create an object from a result out of CosmosDB. - - :param result: - :return object: - """ - # get the document item from the result and turn into a dict - doc = result.get("document") - # read the e_tag from Cosmos - if result.get("_etag"): - doc["e_tag"] = result["_etag"] - - result_obj = Unpickler().restore(doc) - - # create and return the object - return result_obj - - def __create_dict(self, store_item: object) -> Dict: - """Return the dict of an object. - - This eliminates non_magic attributes and the e_tag. - - :param store_item: - :return dict: - """ - # read the content - json_dict = Pickler().flatten(store_item) - if "e_tag" in json_dict: - del json_dict["e_tag"] - - # loop through attributes and write and return a dict - return json_dict - - def __item_link(self, identifier) -> str: - """Return the item link of a item in the container. - - :param identifier: - :return str: - """ - return self.__container_link + "/docs/" + identifier - - @property - def __container_link(self) -> str: - """Return the container link in the database. - - :param: - :return str: - """ - return self.__database_link + "/colls/" + self.container - - @property - def __database_link(self) -> str: - """Return the database link. - - :return str: - """ - return "dbs/" + self.database - - @property - def __container_exists(self) -> bool: - """Return whether the database and container have been created. - - :return bool: - """ - return self.database and self.container - - def __create_db_and_container(self): - """Call the get or create methods.""" - with self.__semaphore: - db_id = self.config.database - container_name = self.config.container - self.database = self._get_or_create_database(self.client, db_id) - self.container = self._get_or_create_container(self.client, container_name) - - def _get_or_create_database( # pylint: disable=invalid-name - self, doc_client, id - ) -> str: - """Return the database link. - - Check if the database exists or create the database. - - :param doc_client: - :param id: - :return str: - """ - # query CosmosDB for a database with that name/id - dbs = list( - doc_client.QueryDatabases( - { - "query": "SELECT * FROM r WHERE r.id=@id", - "parameters": [{"name": "@id", "value": id}], - } - ) - ) - # if there are results, return the first (database names are unique) - if dbs: - return dbs[0]["id"] - - # create the database if it didn't exist - res = doc_client.CreateDatabase({"id": id}, self._database_creation_options) - return res["id"] - - def _get_or_create_container(self, doc_client, container) -> str: - """Return the container link. - - Check if the container exists or create the container. - - :param doc_client: - :param container: - :return str: - """ - # query CosmosDB for a container in the database with that name - containers = list( - doc_client.QueryContainers( - self.__database_link, - { - "query": "SELECT * FROM r WHERE r.id=@id", - "parameters": [{"name": "@id", "value": container}], - }, - ) - ) - # if there are results, return the first (container names are unique) - if containers: - return containers[0]["id"] - - # Create a container if it didn't exist - res = doc_client.CreateContainer( - self.__database_link, {"id": container}, self._container_creation_options - ) - return res["id"] diff --git a/libraries/botbuilder-azure/setup.py b/libraries/botbuilder-azure/setup.py index 04fd479cb..9c40b3ab5 100644 --- a/libraries/botbuilder-azure/setup.py +++ b/libraries/botbuilder-azure/setup.py @@ -5,7 +5,7 @@ from setuptools import setup REQUIRES = [ - "azure-cosmos==3.2.0", + "azure-cosmos==4.7.0", "azure-storage-blob==12.7.0", "azure-storage-queue==12.4.0", "botbuilder-schema==4.17.0", diff --git a/libraries/botbuilder-azure/tests/test_cosmos_partitioned_storage.py b/libraries/botbuilder-azure/tests/test_cosmos_partitioned_storage.py index cb6dd0822..d52733fd9 100644 --- a/libraries/botbuilder-azure/tests/test_cosmos_partitioned_storage.py +++ b/libraries/botbuilder-azure/tests/test_cosmos_partitioned_storage.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -import azure.cosmos.errors as cosmos_errors +import azure.cosmos.exceptions as cosmos_exceptions from azure.cosmos import documents import pytest from botbuilder.azure import CosmosDbPartitionedStorage, CosmosDbPartitionedConfig @@ -27,8 +27,8 @@ async def reset(): storage = CosmosDbPartitionedStorage(get_settings()) await storage.initialize() try: - storage.client.DeleteDatabase(database_link="dbs/" + get_settings().database_id) - except cosmos_errors.HTTPFailure: + storage.client.delete_database(get_settings().database_id) + except cosmos_exceptions.HttpResponseError: pass @@ -99,9 +99,12 @@ async def test_passes_cosmos_client_options(self): client = CosmosDbPartitionedStorage(settings_with_options) await client.initialize() - assert client.client.connection_policy.DisableSSLVerification is True assert ( - client.client.default_headers["x-ms-consistency-level"] + client.client.client_connection.connection_policy.DisableSSLVerification + is True + ) + assert ( + client.client.client_connection.default_headers["x-ms-consistency-level"] == documents.ConsistencyLevel.Eventual ) diff --git a/libraries/botbuilder-azure/tests/test_cosmos_storage.py b/libraries/botbuilder-azure/tests/test_cosmos_storage.py deleted file mode 100644 index c66660857..000000000 --- a/libraries/botbuilder-azure/tests/test_cosmos_storage.py +++ /dev/null @@ -1,300 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from unittest.mock import Mock -import azure.cosmos.errors as cosmos_errors -from azure.cosmos.cosmos_client import CosmosClient -import pytest -from botbuilder.core import StoreItem -from botbuilder.azure import CosmosDbStorage, CosmosDbConfig -from botbuilder.testing import StorageBaseTests - -# local cosmosdb emulator instance cosmos_db_config -COSMOS_DB_CONFIG = CosmosDbConfig( - endpoint="https://localhost:8081", - masterkey="C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", - database="test-db", - container="bot-storage", -) -EMULATOR_RUNNING = False - - -def get_storage(): - return CosmosDbStorage(COSMOS_DB_CONFIG) - - -async def reset(): - storage = CosmosDbStorage(COSMOS_DB_CONFIG) - try: - storage.client.DeleteDatabase(database_link="dbs/" + COSMOS_DB_CONFIG.database) - except cosmos_errors.HTTPFailure: - pass - - -def get_mock_client(identifier: str = "1"): - # pylint: disable=attribute-defined-outside-init, invalid-name - mock = MockClient() - - mock.QueryDatabases = Mock(return_value=[]) - mock.QueryContainers = Mock(return_value=[]) - mock.CreateDatabase = Mock(return_value={"id": identifier}) - mock.CreateContainer = Mock(return_value={"id": identifier}) - - return mock - - -class MockClient(CosmosClient): - def __init__(self): # pylint: disable=super-init-not-called - pass - - -class SimpleStoreItem(StoreItem): - def __init__(self, counter=1, e_tag="*"): - super(SimpleStoreItem, self).__init__() - self.counter = counter - self.e_tag = e_tag - - -class TestCosmosDbStorageConstructor: - @pytest.mark.asyncio - async def test_cosmos_storage_init_should_error_without_cosmos_db_config(self): - try: - CosmosDbStorage(CosmosDbConfig()) - except Exception as error: - assert error - - @pytest.mark.asyncio - async def test_creation_request_options_are_being_called(self): - # pylint: disable=protected-access - test_config = CosmosDbConfig( - endpoint="https://localhost:8081", - masterkey="C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", - database="test-db", - container="bot-storage", - database_creation_options={"OfferThroughput": 1000}, - container_creation_options={"OfferThroughput": 500}, - ) - - test_id = "1" - client = get_mock_client(identifier=test_id) - storage = CosmosDbStorage(test_config, client) - storage.database = test_id - - assert storage._get_or_create_database(doc_client=client, id=test_id), test_id - client.CreateDatabase.assert_called_with( - {"id": test_id}, test_config.database_creation_options - ) - assert storage._get_or_create_container( - doc_client=client, container=test_id - ), test_id - client.CreateContainer.assert_called_with( - "dbs/" + test_id, {"id": test_id}, test_config.container_creation_options - ) - - -class TestCosmosDbStorageBaseStorageTests: - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_return_empty_object_when_reading_unknown_key(self): - await reset() - - test_ran = await StorageBaseTests.return_empty_object_when_reading_unknown_key( - get_storage() - ) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_handle_null_keys_when_reading(self): - await reset() - - test_ran = await StorageBaseTests.handle_null_keys_when_reading(get_storage()) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_handle_null_keys_when_writing(self): - await reset() - - test_ran = await StorageBaseTests.handle_null_keys_when_writing(get_storage()) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_does_not_raise_when_writing_no_items(self): - await reset() - - test_ran = await StorageBaseTests.does_not_raise_when_writing_no_items( - get_storage() - ) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_create_object(self): - await reset() - - test_ran = await StorageBaseTests.create_object(get_storage()) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_handle_crazy_keys(self): - await reset() - - test_ran = await StorageBaseTests.handle_crazy_keys(get_storage()) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_update_object(self): - await reset() - - test_ran = await StorageBaseTests.update_object(get_storage()) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_delete_object(self): - await reset() - - test_ran = await StorageBaseTests.delete_object(get_storage()) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_perform_batch_operations(self): - await reset() - - test_ran = await StorageBaseTests.perform_batch_operations(get_storage()) - - assert test_ran - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_proceeds_through_waterfall(self): - await reset() - - test_ran = await StorageBaseTests.proceeds_through_waterfall(get_storage()) - - assert test_ran - - -class TestCosmosDbStorage: - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_cosmos_storage_init_should_work_with_just_endpoint_and_key(self): - storage = CosmosDbStorage( - CosmosDbConfig( - endpoint=COSMOS_DB_CONFIG.endpoint, masterkey=COSMOS_DB_CONFIG.masterkey - ) - ) - await storage.write({"user": SimpleStoreItem()}) - data = await storage.read(["user"]) - assert "user" in data - assert data["user"].counter == 1 - assert len(data.keys()) == 1 - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_cosmos_storage_read_update_should_return_new_etag(self): - await reset() - storage = CosmosDbStorage(COSMOS_DB_CONFIG) - await storage.write({"test": SimpleStoreItem(counter=1)}) - data_result = await storage.read(["test"]) - data_result["test"].counter = 2 - await storage.write(data_result) - data_updated = await storage.read(["test"]) - assert data_updated["test"].counter == 2 - assert data_updated["test"].e_tag != data_result["test"].e_tag - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_cosmos_storage_read_with_invalid_key_should_return_empty_dict(self): - await reset() - storage = CosmosDbStorage(COSMOS_DB_CONFIG) - data = await storage.read(["test"]) - - assert isinstance(data, dict) - assert not data.keys() - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_cosmos_storage_write_should_overwrite_when_new_e_tag_is_an_asterisk( - self, - ): - await reset() - storage = CosmosDbStorage(COSMOS_DB_CONFIG) - await storage.write({"user": SimpleStoreItem()}) - - await storage.write({"user": SimpleStoreItem(counter=10, e_tag="*")}) - data = await storage.read(["user"]) - assert data["user"].counter == 10 - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_cosmos_storage_delete_should_delete_multiple_values_when_given_multiple_valid_keys( - self, - ): - await reset() - storage = CosmosDbStorage(COSMOS_DB_CONFIG) - await storage.write({"test": SimpleStoreItem(), "test2": SimpleStoreItem(2)}) - - await storage.delete(["test", "test2"]) - data = await storage.read(["test", "test2"]) - assert not data.keys() - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_cosmos_storage_delete_should_delete_values_when_given_multiple_valid_keys_and_ignore_other_data( - self, - ): - await reset() - storage = CosmosDbStorage(COSMOS_DB_CONFIG) - await storage.write( - { - "test": SimpleStoreItem(), - "test2": SimpleStoreItem(counter=2), - "test3": SimpleStoreItem(counter=3), - } - ) - - await storage.delete(["test", "test2"]) - data = await storage.read(["test", "test2", "test3"]) - assert len(data.keys()) == 1 - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_cosmos_storage_delete_invalid_key_should_do_nothing_and_not_affect_cached_data( - self, - ): - await reset() - storage = CosmosDbStorage(COSMOS_DB_CONFIG) - await storage.write({"test": SimpleStoreItem()}) - - await storage.delete(["foo"]) - data = await storage.read(["test"]) - assert len(data.keys()) == 1 - data = await storage.read(["foo"]) - assert not data.keys() - - @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.") - @pytest.mark.asyncio - async def test_cosmos_storage_delete_invalid_keys_should_do_nothing_and_not_affect_cached_data( - self, - ): - await reset() - storage = CosmosDbStorage(COSMOS_DB_CONFIG) - await storage.write({"test": SimpleStoreItem()}) - - await storage.delete(["foo", "bar"]) - data = await storage.read(["test"]) - assert len(data.keys()) == 1 diff --git a/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py b/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py index e196099a0..e374a3401 100644 --- a/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py +++ b/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py @@ -24,7 +24,7 @@ async def test_handle_null_keys_when_reading(self): assert test_ran """ import pytest -from botbuilder.azure import CosmosDbStorage +from botbuilder.azure import CosmosDbPartitionedStorage from botbuilder.core import ( ConversationState, TurnContext, @@ -57,7 +57,7 @@ async def return_empty_object_when_reading_unknown_key(storage) -> bool: @staticmethod async def handle_null_keys_when_reading(storage) -> bool: - if isinstance(storage, (CosmosDbStorage, MemoryStorage)): + if isinstance(storage, (CosmosDbPartitionedStorage, MemoryStorage)): result = await storage.read(None) assert len(result.keys()) == 0 # Catch-all From 0fba27be15cd72bffb7f34bf1ed20c05c3df0957 Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:37:15 +0530 Subject: [PATCH 13/14] port: Add Teams read receipt event (#6356) (#2167) * port: Add Teams read receipt event (#6356) * remove unused variables --- .../core/teams/teams_activity_handler.py | 18 +++++++ .../teams/test_teams_activity_handler.py | 27 ++++++++++ .../botbuilder/schema/teams/__init__.py | 2 + .../botbuilder/schema/teams/_models_py3.py | 51 +++++++++++++++++++ .../tests/teams/test_read_receipt_info.py | 28 ++++++++++ 5 files changed, 126 insertions(+) create mode 100644 libraries/botbuilder-schema/tests/teams/test_read_receipt_info.py diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py index 33b4e419c..4fd6d4ee1 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py @@ -28,6 +28,7 @@ TabRequest, TabSubmit, MeetingParticipantsEventDetails, + ReadReceiptInfo, ) from botframework.connector import Channels from ..serializer_helper import deserializer_helper @@ -906,6 +907,10 @@ async def on_event_activity(self, turn_context: TurnContext): the scope of a channel. """ if turn_context.activity.channel_id == Channels.ms_teams: + if turn_context.activity.name == "application/vnd.microsoft.readReceipt": + return await self.on_teams_read_receipt_event( + turn_context.activity.value, turn_context + ) if turn_context.activity.name == "application/vnd.microsoft.meetingStart": return await self.on_teams_meeting_start_event( turn_context.activity.value, turn_context @@ -931,6 +936,19 @@ async def on_event_activity(self, turn_context: TurnContext): return await super().on_event_activity(turn_context) + async def on_teams_read_receipt_event( + self, read_receipt_info: ReadReceiptInfo, turn_context: TurnContext + ): # pylint: disable=unused-argument + """ + Override this in a derived class to provide logic for when the bot receives a read receipt event. + + :param read_receipt_info: Information regarding the read receipt. i.e. Id of the message last read by the user. + :param turn_context: A context object for this turn. + + :returns: A task that represents the work queued to execute. + """ + return + async def on_teams_meeting_start_event( self, meeting: MeetingStartEventDetails, turn_context: TurnContext ): # pylint: disable=unused-argument diff --git a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py index 390df6191..af9bcd4ab 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py @@ -33,6 +33,7 @@ TabSubmit, TabContext, MeetingParticipantsEventDetails, + ReadReceiptInfo, ) from botframework.connector import Channels from simple_adapter import SimpleAdapter @@ -318,6 +319,14 @@ async def on_event_activity(self, turn_context: TurnContext): self.record.append("on_event_activity") return await super().on_event_activity(turn_context) + async def on_teams_read_receipt_event( + self, read_receipt_info: ReadReceiptInfo, turn_context: TurnContext + ): + self.record.append("on_teams_read_receipt_event") + return await super().on_teams_read_receipt_event( + turn_context.activity.value, turn_context + ) + async def on_teams_meeting_start_event( self, meeting: MeetingStartEventDetails, turn_context: TurnContext ): @@ -1141,6 +1150,24 @@ async def test_typing_activity(self): assert len(bot.record) == 1 assert bot.record[0] == "on_typing_activity" + async def test_on_teams_read_receipt_event(self): + activity = Activity( + type=ActivityTypes.event, + name="application/vnd.microsoft.readReceipt", + channel_id=Channels.ms_teams, + value={"lastReadMessageId": "10101010"}, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + assert len(bot.record) == 2 + assert bot.record[0] == "on_event_activity" + assert bot.record[1] == "on_teams_read_receipt_event" + async def test_on_teams_meeting_start_event(self): activity = Activity( type=ActivityTypes.event, diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py index 55901f7a4..0bac60e96 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py @@ -80,6 +80,7 @@ from ._models_py3 import UserMeetingDetails from ._models_py3 import TeamsMeetingMember from ._models_py3 import MeetingParticipantsEventDetails +from ._models_py3 import ReadReceiptInfo __all__ = [ "AppBasedLinkQuery", @@ -161,4 +162,5 @@ "UserMeetingDetails", "TeamsMeetingMember", "MeetingParticipantsEventDetails", + "ReadReceiptInfo", ] diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py index 260442cf8..a507467cc 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py @@ -2568,3 +2568,54 @@ class MeetingParticipantsEventDetails(Model): def __init__(self, *, members: List[TeamsMeetingMember] = None, **kwargs) -> None: super(MeetingParticipantsEventDetails, self).__init__(**kwargs) self.members = members + + +class ReadReceiptInfo(Model): + """General information about a read receipt. + + :param last_read_message_id: The id of the last read message. + :type last_read_message_id: str + """ + + _attribute_map = { + "last_read_message_id": {"key": "lastReadMessageId", "type": "str"}, + } + + def __init__(self, *, last_read_message_id: str = None, **kwargs) -> None: + super(ReadReceiptInfo, self).__init__(**kwargs) + self.last_read_message_id = last_read_message_id + + @staticmethod + def is_message_read(compare_message_id, last_read_message_id): + """ + Helper method useful for determining if a message has been read. + This method converts the strings to integers. If the compare_message_id is + less than or equal to the last_read_message_id, then the message has been read. + + :param compare_message_id: The id of the message to compare. + :param last_read_message_id: The id of the last message read by the user. + :return: True if the compare_message_id is less than or equal to the last_read_message_id. + """ + if not compare_message_id or not last_read_message_id: + return False + + try: + compare_message_id_long = int(compare_message_id) + last_read_message_id_long = int(last_read_message_id) + except ValueError: + return False + + return compare_message_id_long <= last_read_message_id_long + + def is_message_read_instance(self, compare_message_id): + """ + Helper method useful for determining if a message has been read. + If the compare_message_id is less than or equal to the last_read_message_id, + then the message has been read. + + :param compare_message_id: The id of the message to compare. + :return: True if the compare_message_id is less than or equal to the last_read_message_id. + """ + return ReadReceiptInfo.is_message_read( + compare_message_id, self.last_read_message_id + ) diff --git a/libraries/botbuilder-schema/tests/teams/test_read_receipt_info.py b/libraries/botbuilder-schema/tests/teams/test_read_receipt_info.py new file mode 100644 index 000000000..e6aad9bf3 --- /dev/null +++ b/libraries/botbuilder-schema/tests/teams/test_read_receipt_info.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botbuilder.schema.teams import ReadReceiptInfo + + +class TestReadReceiptInfo(aiounittest.AsyncTestCase): + def test_read_receipt_info(self): + # Arrange + test_cases = [ + ("1000", "1000", True), + ("1001", "1000", True), + ("1000", "1001", False), + ("1000", None, False), + (None, "1000", False), + ] + + for last_read, compare, is_read in test_cases: + # Act + info = ReadReceiptInfo(last_read_message_id=last_read) + + # Assert + self.assertEqual(info.last_read_message_id, last_read) + self.assertEqual(info.is_message_read_instance(compare), is_read) + self.assertEqual( + ReadReceiptInfo.is_message_read(compare, last_read), is_read + ) From fddd368dca2c0889c01cf4ac101666236aa9298b Mon Sep 17 00:00:00 2001 From: gandiddi <164224646+gandiddi@users.noreply.github.com> Date: Tue, 17 Sep 2024 18:23:37 +0530 Subject: [PATCH 14/14] Add new Teams invoke types 'config/fetch' and 'config/submit' (#2170) --- .../core/teams/teams_activity_handler.py | 42 ++++++++++ .../teams/test_teams_activity_handler.py | 52 ++++++++++++ .../botbuilder/schema/teams/__init__.py | 8 ++ .../botbuilder/schema/teams/_models_py3.py | 80 +++++++++++++++++++ .../tests/teams/test_bot_config_auth.py | 14 ++++ .../tests/teams/test_config_auth_response.py | 14 ++++ .../tests/teams/test_config_response.py | 13 +++ .../tests/teams/test_config_task_response.py | 14 ++++ 8 files changed, 237 insertions(+) create mode 100644 libraries/botbuilder-schema/tests/teams/test_bot_config_auth.py create mode 100644 libraries/botbuilder-schema/tests/teams/test_config_auth_response.py create mode 100644 libraries/botbuilder-schema/tests/teams/test_config_response.py create mode 100644 libraries/botbuilder-schema/tests/teams/test_config_task_response.py diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py index 4fd6d4ee1..7832887dc 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py @@ -186,6 +186,22 @@ async def on_invoke_activity(self, turn_context: TurnContext) -> InvokeResponse: ) ) + if turn_context.activity.name == "config/fetch": + return self._create_invoke_response( + await self.on_teams_config_fetch( + turn_context, + turn_context.activity.value, + ) + ) + + if turn_context.activity.name == "config/submit": + return self._create_invoke_response( + await self.on_teams_config_submit( + turn_context, + turn_context.activity.value, + ) + ) + return await super().on_invoke_activity(turn_context) except _InvokeResponseException as invoke_exception: @@ -515,6 +531,32 @@ async def on_teams_tab_submit( # pylint: disable=unused-argument """ raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + async def on_teams_config_fetch( # pylint: disable=unused-argument + self, turn_context: TurnContext, config_data: any + ): + """ + Override this in a derived class to provide logic for when a config is fetched. + + :param turn_context: A context object for this turn. + :param config_data: The config fetch invoke request value payload. + + :returns: A Config Response for the request. + """ + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_config_submit( # pylint: disable=unused-argument + self, turn_context: TurnContext, config_data: any + ): + """ + Override this in a derived class to provide logic for when a config is submitted. + + :param turn_context: A context object for this turn. + :param config_data: The config fetch invoke request value payload. + + :returns: A Config Response for the request. + """ + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + async def on_conversation_update_activity(self, turn_context: TurnContext): """ Invoked when a conversation update activity is received from the channel. diff --git a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py index af9bcd4ab..2ea62717b 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py @@ -315,6 +315,14 @@ async def on_teams_tab_submit( self.record.append("on_teams_tab_submit") return await super().on_teams_tab_submit(turn_context, tab_submit) + async def on_teams_config_fetch(self, turn_context: TurnContext, config_data: any): + self.record.append("on_teams_config_fetch") + return await super().on_teams_config_fetch(turn_context, config_data) + + async def on_teams_config_submit(self, turn_context: TurnContext, config_data: any): + self.record.append("on_teams_config_submit") + return await super().on_teams_config_submit(turn_context, config_data) + async def on_event_activity(self, turn_context: TurnContext): self.record.append("on_event_activity") return await super().on_event_activity(turn_context) @@ -1126,6 +1134,50 @@ async def test_on_teams_tab_submit(self): assert bot.record[0] == "on_invoke_activity" assert bot.record[1] == "on_teams_tab_submit" + async def test_on_teams_config_fetch(self): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="config/fetch", + value={ + "data": {"key": "value", "type": "config/fetch"}, + "context": {"theme": "default"}, + }, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 2 + assert bot.record[0] == "on_invoke_activity" + assert bot.record[1] == "on_teams_config_fetch" + + async def test_on_teams_config_submit(self): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="config/submit", + value={ + "data": {"key": "value", "type": "config/submit"}, + "context": {"theme": "default"}, + }, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 2 + assert bot.record[0] == "on_invoke_activity" + assert bot.record[1] == "on_teams_config_submit" + async def test_on_end_of_conversation_activity(self): activity = Activity(type=ActivityTypes.end_of_conversation) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py index 0bac60e96..8fb944b16 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py @@ -81,6 +81,10 @@ from ._models_py3 import TeamsMeetingMember from ._models_py3 import MeetingParticipantsEventDetails from ._models_py3 import ReadReceiptInfo +from ._models_py3 import BotConfigAuth +from ._models_py3 import ConfigAuthResponse +from ._models_py3 import ConfigResponse +from ._models_py3 import ConfigTaskResponse __all__ = [ "AppBasedLinkQuery", @@ -163,4 +167,8 @@ "TeamsMeetingMember", "MeetingParticipantsEventDetails", "ReadReceiptInfo", + "BotConfigAuth", + "ConfigAuthResponse", + "ConfigResponse", + "ConfigTaskResponse", ] diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py index a507467cc..a1ab30d6c 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py @@ -2619,3 +2619,83 @@ def is_message_read_instance(self, compare_message_id): return ReadReceiptInfo.is_message_read( compare_message_id, self.last_read_message_id ) + + +class BotConfigAuth(Model): + """Specifies bot config auth, including type and suggestedActions. + + :param type: The type of bot config auth. + :type type: str + :param suggested_actions: The suggested actions of bot config auth. + :type suggested_actions: ~botframework.connector.models.SuggestedActions + """ + + _attribute_map = { + "type": {"key": "type", "type": "str"}, + "suggested_actions": {"key": "suggestedActions", "type": "SuggestedActions"}, + } + + def __init__(self, *, type: str = "auth", suggested_actions=None, **kwargs) -> None: + super(BotConfigAuth, self).__init__(**kwargs) + self.type = type + self.suggested_actions = suggested_actions + + +class ConfigResponseBase(Model): + """Specifies Invoke response base, including response type. + + :param response_type: Response type for invoke request + :type response_type: str + """ + + _attribute_map = { + "response_type": {"key": "responseType", "type": "str"}, + } + + def __init__(self, *, response_type: str = None, **kwargs) -> None: + super(ConfigResponseBase, self).__init__(**kwargs) + self.response_type = response_type + + +class ConfigResponse(ConfigResponseBase): + """Envelope for Config Response Payload. + + :param config: The response to the config message. Possible values: 'auth', 'task' + :type config: T + :param cache_info: Response cache info + :type cache_info: ~botframework.connector.teams.models.CacheInfo + """ + + _attribute_map = { + "config": {"key": "config", "type": "object"}, + "cache_info": {"key": "cacheInfo", "type": "CacheInfo"}, + } + + def __init__(self, *, config=None, cache_info=None, **kwargs) -> None: + super(ConfigResponse, self).__init__(response_type="config", **kwargs) + self.config = config + self.cache_info = cache_info + + +class ConfigTaskResponse(ConfigResponse): + """Envelope for Config Task Response. + + This class uses TaskModuleResponseBase as the type for the config parameter. + """ + + def __init__(self, *, config=None, **kwargs) -> None: + super(ConfigTaskResponse, self).__init__( + config=config or TaskModuleResponseBase(), **kwargs + ) + + +class ConfigAuthResponse(ConfigResponse): + """Envelope for Config Auth Response. + + This class uses BotConfigAuth as the type for the config parameter. + """ + + def __init__(self, *, config=None, **kwargs) -> None: + super(ConfigAuthResponse, self).__init__( + config=config or BotConfigAuth(), **kwargs + ) diff --git a/libraries/botbuilder-schema/tests/teams/test_bot_config_auth.py b/libraries/botbuilder-schema/tests/teams/test_bot_config_auth.py new file mode 100644 index 000000000..f6d771c4e --- /dev/null +++ b/libraries/botbuilder-schema/tests/teams/test_bot_config_auth.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botbuilder.schema.teams import BotConfigAuth + + +class TestBotConfigAuth(aiounittest.AsyncTestCase): + def test_bot_config_auth_inits_with_no_args(self): + bot_config_auth_response = BotConfigAuth() + + self.assertIsNotNone(bot_config_auth_response) + self.assertIsInstance(bot_config_auth_response, BotConfigAuth) + self.assertEqual("auth", bot_config_auth_response.type) diff --git a/libraries/botbuilder-schema/tests/teams/test_config_auth_response.py b/libraries/botbuilder-schema/tests/teams/test_config_auth_response.py new file mode 100644 index 000000000..54221399d --- /dev/null +++ b/libraries/botbuilder-schema/tests/teams/test_config_auth_response.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botbuilder.schema.teams import ConfigAuthResponse + + +class TestConfigAuthResponse(aiounittest.AsyncTestCase): + def test_config_auth_response_init_with_no_args(self): + config_auth_response = ConfigAuthResponse() + + self.assertIsNotNone(config_auth_response) + self.assertIsInstance(config_auth_response, ConfigAuthResponse) + self.assertEqual("config", config_auth_response.response_type) diff --git a/libraries/botbuilder-schema/tests/teams/test_config_response.py b/libraries/botbuilder-schema/tests/teams/test_config_response.py new file mode 100644 index 000000000..39d2ce0d5 --- /dev/null +++ b/libraries/botbuilder-schema/tests/teams/test_config_response.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botbuilder.schema.teams import ConfigResponse + + +class TestConfigResponse(aiounittest.AsyncTestCase): + def test_config_response_inits_with_no_args(self): + config_response = ConfigResponse() + + self.assertIsNotNone(config_response) + self.assertIsInstance(config_response, ConfigResponse) diff --git a/libraries/botbuilder-schema/tests/teams/test_config_task_response.py b/libraries/botbuilder-schema/tests/teams/test_config_task_response.py new file mode 100644 index 000000000..53126388d --- /dev/null +++ b/libraries/botbuilder-schema/tests/teams/test_config_task_response.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botbuilder.schema.teams import ConfigTaskResponse + + +class TestConfigTaskResponse(aiounittest.AsyncTestCase): + def test_config_task_response_init_with_no_args(self): + config_task_response = ConfigTaskResponse() + + self.assertIsNotNone(config_task_response) + self.assertIsInstance(config_task_response, ConfigTaskResponse) + self.assertEqual("config", config_task_response.response_type)