diff --git a/dev/hosting_dialogs/choices/test_channel.py b/dev/hosting_dialogs/choices/test_channel.py deleted file mode 100644 index 74fc849c1..000000000 --- a/dev/hosting_dialogs/choices/test_channel.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import List, Tuple - -import pytest - -from microsoft_agents.activity import ( - Activity, - ActivityTypes, - Channels, - ConversationAccount, - ChannelAccount, -) -from microsoft_agents.hosting.core import TurnContext -from microsoft_agents.hosting.dialogs.choices import Channel -from tests._common.testing_objects import MockTestingAdapter - - -class TestChannel: - def test_supports_suggested_actions(self): - actual = Channel.supports_suggested_actions(Channels.facebook, 5) - assert actual - - def test_supports_suggested_actions_many(self): - supports_suggested_actions_data: List[Tuple[str, int, bool]] = [ - (Channels.line, 13, True), - (Channels.line, 14, False), - (Channels.skype, 10, True), - (Channels.skype, 11, False), - (Channels.kik, 20, True), - (Channels.kik, 21, False), - (Channels.emulator, 100, True), - (Channels.emulator, 101, False), - (Channels.direct_line_speech, 100, True), - ] - - for channel, button_cnt, expected in supports_suggested_actions_data: - actual = Channel.supports_suggested_actions(channel, button_cnt) - assert ( - expected == actual - ), f"channel={channel}, button_cnt={button_cnt}: expected {expected}, got {actual}" - - def test_supports_card_actions_many(self): - supports_card_action_data: List[Tuple[str, int, bool]] = [ - (Channels.line, 99, True), - (Channels.line, 100, False), - (Channels.slack, 100, True), - (Channels.skype, 3, True), - (Channels.skype, 5, False), - (Channels.direct_line_speech, 99, True), - ] - - for channel, button_cnt, expected in supports_card_action_data: - actual = Channel.supports_card_actions(channel, button_cnt) - assert ( - expected == actual - ), f"channel={channel}, button_cnt={button_cnt}: expected {expected}, got {actual}" - - def test_supports_suggested_actions_accepts_string_channel_id(self): - assert Channel.supports_suggested_actions("facebook", 5) - assert not Channel.supports_suggested_actions("facebook", 11) - - def test_supports_card_actions_accepts_string_channel_id(self): - assert Channel.supports_card_actions("msteams", 3) - assert not Channel.supports_card_actions("msteams", 4) - - def test_should_return_channel_id_from_context_activity(self): - adapter = MockTestingAdapter(channel_id=Channels.facebook) - test_activity = Activity( - type=ActivityTypes.message, - channel_id=Channels.facebook, - conversation=ConversationAccount(id="test"), - from_property=ChannelAccount(id="user"), - ) - test_context = TurnContext(adapter, test_activity) - channel_id = Channel.get_channel_id(test_context) - assert Channels.facebook == channel_id - - def test_should_return_empty_from_context_activity_missing_channel(self): - adapter = MockTestingAdapter() - test_activity = Activity( - type=ActivityTypes.message, - conversation=ConversationAccount(id="test"), - from_property=ChannelAccount(id="user"), - ) - test_context = TurnContext(adapter, test_activity) - channel_id = Channel.get_channel_id(test_context) - assert "" == channel_id diff --git a/dev/hosting_dialogs/choices/test_choice.py b/dev/hosting_dialogs/choices/test_choice.py deleted file mode 100644 index 4b37a3e62..000000000 --- a/dev/hosting_dialogs/choices/test_choice.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import List - -import pytest - -from microsoft_agents.hosting.dialogs.choices import Choice -from microsoft_agents.activity import CardAction - - -class TestChoice: - def test_value_round_trips(self) -> None: - choice = Choice() - expected = "any" - choice.value = expected - assert expected is choice.value - - def test_action_round_trips(self) -> None: - choice = Choice() - expected = CardAction(type="imBack", title="Test Action") - choice.action = expected - assert expected is choice.action - - def test_synonyms_round_trips(self) -> None: - choice = Choice() - expected: List[str] = [] - choice.synonyms = expected - assert expected is choice.synonyms diff --git a/dev/hosting_dialogs/choices/test_choice_factory.py b/dev/hosting_dialogs/choices/test_choice_factory.py deleted file mode 100644 index 47e143ff7..000000000 --- a/dev/hosting_dialogs/choices/test_choice_factory.py +++ /dev/null @@ -1,237 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import List - -import pytest - -from microsoft_agents.hosting.dialogs.choices import ( - Choice, - ChoiceFactory, - ChoiceFactoryOptions, -) -from microsoft_agents.activity import ( - ActionTypes, - Activity, - ActivityTypes, - Attachment, - AttachmentLayoutTypes, - CardAction, - HeroCard, - InputHints, - SuggestedActions, - Channels, -) - - -class TestChoiceFactory: - color_choices: List[Choice] = [Choice("red"), Choice("green"), Choice("blue")] - choices_with_actions: List[Choice] = [ - Choice( - "ImBack", - action=CardAction( - type=ActionTypes.im_back, title="ImBack Action", value="ImBack Value" - ), - ), - Choice( - "MessageBack", - action=CardAction( - type=ActionTypes.message_back, - title="MessageBack Action", - value="MessageBack Value", - ), - ), - Choice( - "PostBack", - action=CardAction( - type=ActionTypes.post_back, - title="PostBack Action", - value="PostBack Value", - ), - ), - ] - - def test_inline_should_render_choices_inline(self): - activity = ChoiceFactory.inline(TestChoiceFactory.color_choices, "select from:") - assert "select from: (1) red, (2) green, or (3) blue" == activity.text - - def test_should_render_choices_as_a_list(self): - activity = ChoiceFactory.list_style( - TestChoiceFactory.color_choices, "select from:" - ) - assert "select from:\n\n 1. red\n 2. green\n 3. blue" == activity.text - - def test_should_render_unincluded_numbers_choices_as_a_list(self): - activity = ChoiceFactory.list_style( - TestChoiceFactory.color_choices, - "select from:", - options=ChoiceFactoryOptions(include_numbers=False), - ) - assert "select from:\n\n - red\n - green\n - blue" == activity.text - - def test_should_render_choices_as_suggested_actions(self): - expected = Activity( - type=ActivityTypes.message, - text="select from:", - input_hint=InputHints.expecting_input, - suggested_actions=SuggestedActions( - actions=[ - CardAction(type=ActionTypes.im_back, value="red", title="red"), - CardAction(type=ActionTypes.im_back, value="green", title="green"), - CardAction(type=ActionTypes.im_back, value="blue", title="blue"), - ] - ), - ) - - activity = ChoiceFactory.suggested_action( - TestChoiceFactory.color_choices, "select from:" - ) - - assert expected == activity - - def test_should_render_choices_as_hero_card(self): - expected = Activity( - type=ActivityTypes.message, - input_hint=InputHints.expecting_input, - attachment_layout=AttachmentLayoutTypes.list, - attachments=[ - Attachment( - content=HeroCard( - text="select from:", - buttons=[ - CardAction( - type=ActionTypes.im_back, value="red", title="red" - ), - CardAction( - type=ActionTypes.im_back, value="green", title="green" - ), - CardAction( - type=ActionTypes.im_back, value="blue", title="blue" - ), - ], - ), - content_type="application/vnd.microsoft.card.hero", - ) - ], - ) - - activity = ChoiceFactory.hero_card( - TestChoiceFactory.color_choices, "select from:" - ) - - assert expected == activity - - def test_should_automatically_choose_render_style_based_on_channel_type(self): - expected = Activity( - type=ActivityTypes.message, - text="select from:", - input_hint=InputHints.expecting_input, - suggested_actions=SuggestedActions( - actions=[ - CardAction(type=ActionTypes.im_back, value="red", title="red"), - CardAction(type=ActionTypes.im_back, value="green", title="green"), - CardAction(type=ActionTypes.im_back, value="blue", title="blue"), - ] - ), - ) - activity = ChoiceFactory.for_channel( - Channels.emulator, TestChoiceFactory.color_choices, "select from:" - ) - - assert expected == activity - - def test_should_choose_correct_styles_for_teams(self): - expected = Activity( - type=ActivityTypes.message, - input_hint=InputHints.expecting_input, - attachment_layout=AttachmentLayoutTypes.list, - attachments=[ - Attachment( - content=HeroCard( - text="select from:", - buttons=[ - CardAction( - type=ActionTypes.im_back, value="red", title="red" - ), - CardAction( - type=ActionTypes.im_back, value="green", title="green" - ), - CardAction( - type=ActionTypes.im_back, value="blue", title="blue" - ), - ], - ), - content_type="application/vnd.microsoft.card.hero", - ) - ], - ) - activity = ChoiceFactory.for_channel( - Channels.ms_teams, TestChoiceFactory.color_choices, "select from:" - ) - assert expected == activity - - def test_should_include_choice_actions_in_suggested_actions(self): - expected = Activity( - type=ActivityTypes.message, - text="select from:", - input_hint=InputHints.expecting_input, - suggested_actions=SuggestedActions( - actions=[ - CardAction( - type=ActionTypes.im_back, - value="ImBack Value", - title="ImBack Action", - ), - CardAction( - type=ActionTypes.message_back, - value="MessageBack Value", - title="MessageBack Action", - ), - CardAction( - type=ActionTypes.post_back, - value="PostBack Value", - title="PostBack Action", - ), - ] - ), - ) - activity = ChoiceFactory.suggested_action( - TestChoiceFactory.choices_with_actions, "select from:" - ) - assert expected == activity - - def test_should_include_choice_actions_in_hero_cards(self): - expected = Activity( - type=ActivityTypes.message, - input_hint=InputHints.expecting_input, - attachment_layout=AttachmentLayoutTypes.list, - attachments=[ - Attachment( - content=HeroCard( - text="select from:", - buttons=[ - CardAction( - type=ActionTypes.im_back, - value="ImBack Value", - title="ImBack Action", - ), - CardAction( - type=ActionTypes.message_back, - value="MessageBack Value", - title="MessageBack Action", - ), - CardAction( - type=ActionTypes.post_back, - value="PostBack Value", - title="PostBack Action", - ), - ], - ), - content_type="application/vnd.microsoft.card.hero", - ) - ], - ) - activity = ChoiceFactory.hero_card( - TestChoiceFactory.choices_with_actions, "select from:" - ) - assert expected == activity diff --git a/dev/hosting_dialogs/choices/test_choice_factory_options.py b/dev/hosting_dialogs/choices/test_choice_factory_options.py deleted file mode 100644 index 2e3563a7c..000000000 --- a/dev/hosting_dialogs/choices/test_choice_factory_options.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest - -from microsoft_agents.hosting.dialogs.choices import ChoiceFactoryOptions - - -class TestChoiceFactoryOptions: - def test_inline_separator_round_trips(self) -> None: - choice_factor_options = ChoiceFactoryOptions() - expected = ", " - choice_factor_options.inline_separator = expected - assert expected == choice_factor_options.inline_separator - - def test_inline_or_round_trips(self) -> None: - choice_factor_options = ChoiceFactoryOptions() - expected = " or " - choice_factor_options.inline_or = expected - assert expected == choice_factor_options.inline_or - - def test_inline_or_more_round_trips(self) -> None: - choice_factor_options = ChoiceFactoryOptions() - expected = ", or " - choice_factor_options.inline_or_more = expected - assert expected == choice_factor_options.inline_or_more - - def test_include_numbers_round_trips(self) -> None: - choice_factor_options = ChoiceFactoryOptions() - expected = True - choice_factor_options.include_numbers = expected - assert expected == choice_factor_options.include_numbers diff --git a/dev/hosting_dialogs/choices/test_choice_recognizers.py b/dev/hosting_dialogs/choices/test_choice_recognizers.py deleted file mode 100644 index edc63cd24..000000000 --- a/dev/hosting_dialogs/choices/test_choice_recognizers.py +++ /dev/null @@ -1,198 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import List - -import pytest - -from microsoft_agents.hosting.dialogs.choices import ( - ChoiceRecognizers, - Find, - FindValuesOptions, - SortedValue, -) - - -def assert_result(result, start, end, text): - assert ( - result.start == start - ), f"Invalid ModelResult.start of '{result.start}' for '{text}' result." - assert ( - result.end == end - ), f"Invalid ModelResult.end of '{result.end}' for '{text}' result." - assert ( - result.text == text - ), f"Invalid ModelResult.text of '{result.text}' for '{text}' result." - - -def assert_value(result, value, index, score): - assert ( - result.type_name == "value" - ), f"Invalid ModelResult.type_name of '{result.type_name}' for '{value}' value." - assert result.resolution, f"Missing ModelResult.resolution for '{value}' value." - resolution = result.resolution - assert ( - resolution.value == value - ), f"Invalid resolution.value of '{resolution.value}' for '{value}' value." - assert ( - resolution.index == index - ), f"Invalid resolution.index of '{resolution.index}' for '{value}' value." - assert ( - resolution.score == score - ), f"Invalid resolution.score of '{resolution.score}' for '{value}' value." - - -def assert_choice(result, value, index, score, synonym=None): - assert ( - result.type_name == "choice" - ), f"Invalid ModelResult.type_name of '{result.type_name}' for '{value}' choice." - assert result.resolution, f"Missing ModelResult.resolution for '{value}' choice." - resolution = result.resolution - assert ( - resolution.value == value - ), f"Invalid resolution.value of '{resolution.value}' for '{value}' choice." - assert ( - resolution.index == index - ), f"Invalid resolution.index of '{resolution.index}' for '{value}' choice." - assert ( - resolution.score == score - ), f"Invalid resolution.score of '{resolution.score}' for '{value}' choice." - if synonym: - assert ( - resolution.synonym == synonym - ), f"Invalid resolution.synonym of '{resolution.synonym}' for '{value}' choice." - - -_color_choices: List[str] = ["red", "green", "blue"] -_overlapping_choices: List[str] = ["bread", "bread pudding", "pudding"] - -_color_values: List[SortedValue] = [ - SortedValue(value="red", index=0), - SortedValue(value="green", index=1), - SortedValue(value="blue", index=2), -] - -_overlapping_values: List[SortedValue] = [ - SortedValue(value="bread", index=0), - SortedValue(value="bread pudding", index=1), - SortedValue(value="pudding", index=2), -] - -_similar_values: List[SortedValue] = [ - SortedValue(value="option A", index=0), - SortedValue(value="option B", index=1), - SortedValue(value="option C", index=2), -] - - -class TestChoiceRecognizers: - # Find.find_values - - def test_should_find_a_simple_value_in_a_single_word_utterance(self): - found = Find.find_values("red", _color_values) - assert len(found) == 1, f"Invalid token count of '{len(found)}' returned." - assert_result(found[0], 0, 2, "red") - assert_value(found[0], "red", 0, 1.0) - - def test_should_find_a_simple_value_in_an_utterance(self): - found = Find.find_values("the red one please.", _color_values) - assert len(found) == 1, f"Invalid token count of '{len(found)}' returned." - assert_result(found[0], 4, 6, "red") - assert_value(found[0], "red", 0, 1.0) - - def test_should_find_multiple_values_within_an_utterance(self): - found = Find.find_values("the red and blue ones please.", _color_values) - assert len(found) == 2, f"Invalid token count of '{len(found)}' returned." - assert_result(found[0], 4, 6, "red") - assert_value(found[0], "red", 0, 1.0) - assert_value(found[1], "blue", 2, 1.0) - - def test_should_find_multiple_values_that_overlap(self): - found = Find.find_values( - "the bread pudding and bread please.", _overlapping_values - ) - assert len(found) == 2, f"Invalid token count of '{len(found)}' returned." - assert_result(found[0], 4, 16, "bread pudding") - assert_value(found[0], "bread pudding", 1, 1.0) - assert_value(found[1], "bread", 0, 1.0) - - def test_should_correctly_disambiguate_between_similar_values(self): - found = Find.find_values( - "option B", _similar_values, FindValuesOptions(allow_partial_matches=True) - ) - assert len(found) == 1, f"Invalid token count of '{len(found)}' returned." - assert_value(found[0], "option B", 1, 1.0) - - def test_should_find_a_single_choice_in_an_utterance(self): - found = Find.find_choices("the red one please.", _color_choices) - assert len(found) == 1, f"Invalid token count of '{len(found)}' returned." - assert_result(found[0], 4, 6, "red") - assert_choice(found[0], "red", 0, 1.0, "red") - - def test_should_find_multiple_choices_within_an_utterance(self): - found = Find.find_choices("the red and blue ones please.", _color_choices) - assert len(found) == 2, f"Invalid token count of '{len(found)}' returned." - assert_result(found[0], 4, 6, "red") - assert_choice(found[0], "red", 0, 1.0) - assert_choice(found[1], "blue", 2, 1.0) - - def test_should_find_multiple_choices_that_overlap(self): - found = Find.find_choices( - "the bread pudding and bread please.", _overlapping_choices - ) - assert len(found) == 2, f"Invalid token count of '{len(found)}' returned." - assert_result(found[0], 4, 16, "bread pudding") - assert_choice(found[0], "bread pudding", 1, 1.0) - assert_choice(found[1], "bread", 0, 1.0) - - def test_should_accept_null_utterance_in_find_choices(self): - found = Find.find_choices(None, _color_choices) - assert not found - - # ChoiceRecognizers.recognize_choices - - def test_should_find_a_choice_in_an_utterance_by_name(self): - found = ChoiceRecognizers.recognize_choices( - "the red one please.", _color_choices - ) - assert len(found) == 1 - assert_result(found[0], 4, 6, "red") - assert_choice(found[0], "red", 0, 1.0, "red") - - def test_should_find_a_choice_in_an_utterance_by_ordinal_position(self): - found = ChoiceRecognizers.recognize_choices( - "the first one please.", _color_choices - ) - assert len(found) == 1 - assert_result(found[0], 4, 8, "first") - assert_choice(found[0], "red", 0, 1.0) - - def test_should_find_multiple_choices_in_an_utterance_by_ordinal_position(self): - found = ChoiceRecognizers.recognize_choices( - "the first and third one please", _color_choices - ) - assert len(found) == 2 - assert_choice(found[0], "red", 0, 1.0) - assert_choice(found[1], "blue", 2, 1.0) - - def test_should_find_a_choice_in_an_utterance_by_numerical_index_digit(self): - found = ChoiceRecognizers.recognize_choices("1", _color_choices) - assert len(found) == 1 - assert_result(found[0], 0, 0, "1") - assert_choice(found[0], "red", 0, 1.0) - - def test_should_find_a_choice_in_an_utterance_by_numerical_index_text(self): - found = ChoiceRecognizers.recognize_choices("one", _color_choices) - assert len(found) == 1 - assert_result(found[0], 0, 2, "one") - assert_choice(found[0], "red", 0, 1.0) - - def test_should_find_multiple_choices_in_an_utterance_by_numerical_index(self): - found = ChoiceRecognizers.recognize_choices("option one and 3.", _color_choices) - assert len(found) == 2 - assert_choice(found[0], "red", 0, 1.0) - assert_choice(found[1], "blue", 2, 1.0) - - def test_should_accept_null_utterance_in_recognize_choices(self): - found = ChoiceRecognizers.recognize_choices(None, _color_choices) - assert not found diff --git a/dev/hosting_dialogs/choices/test_choice_tokenizer.py b/dev/hosting_dialogs/choices/test_choice_tokenizer.py deleted file mode 100644 index 16d9d38db..000000000 --- a/dev/hosting_dialogs/choices/test_choice_tokenizer.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest - -from microsoft_agents.hosting.dialogs.choices import Tokenizer - - -def _assert_token(token, start, end, text, normalized=None): - assert ( - token.start == start - ), f"Invalid token.start of '{token.start}' for '{text}' token." - assert token.end == end, f"Invalid token.end of '{token.end}' for '{text}' token." - assert ( - token.text == text - ), f"Invalid token.text of '{token.text}' for '{text}' token." - assert token.normalized == ( - normalized or text.lower() - ), f"Invalid token.normalized of '{token.normalized}' for '{text}' token." - - -class TestChoiceTokenizer: - def test_should_break_on_spaces(self): - tokens = Tokenizer.default_tokenizer("how now brown cow") - assert len(tokens) == 4 - _assert_token(tokens[0], 0, 2, "how") - _assert_token(tokens[1], 4, 6, "now") - _assert_token(tokens[2], 8, 12, "brown") - _assert_token(tokens[3], 14, 16, "cow") - - def test_should_break_on_punctuation(self): - tokens = Tokenizer.default_tokenizer("how-now.brown:cow?") - assert len(tokens) == 4 - _assert_token(tokens[0], 0, 2, "how") - _assert_token(tokens[1], 4, 6, "now") - _assert_token(tokens[2], 8, 12, "brown") - _assert_token(tokens[3], 14, 16, "cow") - - def test_should_tokenize_single_character_tokens(self): - tokens = Tokenizer.default_tokenizer("a b c d") - assert len(tokens) == 4 - _assert_token(tokens[0], 0, 0, "a") - _assert_token(tokens[1], 2, 2, "b") - _assert_token(tokens[2], 4, 4, "c") - _assert_token(tokens[3], 6, 6, "d") - - def test_should_return_a_single_token(self): - tokens = Tokenizer.default_tokenizer("food") - assert len(tokens) == 1 - _assert_token(tokens[0], 0, 3, "food") - - def test_should_return_no_tokens(self): - tokens = Tokenizer.default_tokenizer(".?-()") - assert not tokens - - def test_should_return_a_the_normalized_and_original_text_for_a_token(self): - tokens = Tokenizer.default_tokenizer("fOoD") - assert len(tokens) == 1 - _assert_token(tokens[0], 0, 3, "fOoD", "food") - - def test_should_break_on_emojis(self): - tokens = Tokenizer.default_tokenizer("food 💥👍😀") - assert len(tokens) == 4 - _assert_token(tokens[0], 0, 3, "food") - _assert_token(tokens[1], 5, 5, "💥") - _assert_token(tokens[2], 6, 6, "👍") - _assert_token(tokens[3], 7, 7, "😀") diff --git a/dev/hosting_dialogs/helpers.py b/dev/hosting_dialogs/helpers.py deleted file mode 100644 index b8e93bfcb..000000000 --- a/dev/hosting_dialogs/helpers.py +++ /dev/null @@ -1,312 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" -Test helpers for dialog tests. Provides a TestAdapter/TestFlow compatibility -layer that wraps MockTestingAdapter with the old botbuilder-style chained -send/assert_reply API. -""" - -from typing import Callable, Union, Awaitable - -from microsoft_agents.activity import ( - Activity, - ActivityTypes, - TokenResponse, - SignInResource, - TokenOrSignInResourceResponse, -) -from microsoft_agents.hosting.core import ChannelAdapter, TurnContext -from microsoft_agents.hosting.core.authorization import ClaimsIdentity -from tests._common.testing_objects import MockTestingAdapter - -AgentCallbackHandler = Callable[["TurnContext"], Awaitable[None]] - - -class _MockUserToken: - """Mock user_token API for dialog tests.""" - - def __init__(self, store: dict, exchange_store: dict, throw_on_exchange: dict): - self._store = store - self._exchange_store = exchange_store - self._throw_on_exchange = throw_on_exchange - - @staticmethod - def _key(connection_name, channel_id, user_id): - return f"{connection_name}:{channel_id}:{user_id}" - - @staticmethod - def _exchange_key(connection_name, channel_id, user_id, item): - return f"{connection_name}:{channel_id}:{user_id}:{item}" - - async def get_token(self, user_id, connection_name, channel_id, code=None): - key = self._key(connection_name, channel_id, user_id) - entry = self._store.get(key) - if entry: - token, stored_code = entry - if stored_code is None or (code is not None and code == stored_code): - return TokenResponse( - connection_name=connection_name, - token=token, - channel_id=channel_id, - ) - return None - - async def sign_out(self, user_id, connection_name, channel_id): - key = self._key(connection_name, channel_id, user_id) - self._store.pop(key, None) - - async def exchange_token(self, user_id, connection_name, channel_id, body=None): - token = (body or {}).get("token") or (body or {}).get("uri") - key = self._exchange_key(connection_name, channel_id, user_id, token or "") - if key in self._throw_on_exchange: - raise Exception("Token exchange not allowed for this item.") - result = self._exchange_store.get(key) - if result: - return TokenResponse( - connection_name=connection_name, - token=result, - channel_id=channel_id, - ) - return None - - async def _get_token_or_sign_in_resource( - self, user_id, connection_name, channel_id, state, *_ - ): - key = self._key(connection_name, channel_id, user_id) - entry = self._store.get(key) - if entry: - token, stored_code = entry - if stored_code is None: - return TokenOrSignInResourceResponse( - token_response=TokenResponse( - connection_name=connection_name, - token=token, - channel_id=channel_id, - ) - ) - return TokenOrSignInResourceResponse( - sign_in_resource=SignInResource( - sign_in_link=f"https://token.botframework.com/oauthcards?state={state or ''}" - ) - ) - - -class _MockAgentSignIn: - """Mock agent_sign_in API for dialog tests.""" - - async def get_sign_in_resource(self, state=None): - return SignInResource( - sign_in_link=f"https://token.botframework.com/oauthcards?state={state or ''}", - ) - - -class DialogUserTokenClient: - """ - A lightweight UserTokenClient mock for dialog tests. - Implements the user_token and agent_sign_in APIs used by _UserTokenAccess. - """ - - def __init__(self): - self._store = {} - self._exchange_store = {} - self._throw_on_exchange = {} - self.user_token = _MockUserToken( - self._store, self._exchange_store, self._throw_on_exchange - ) - self.agent_sign_in = _MockAgentSignIn() - - def add_user_token( - self, - connection_name: str, - channel_id: str, - user_id: str, - token: str, - magic_code: str = None, - ): - key = f"{connection_name}:{channel_id}:{user_id}" - self._store[key] = (token, magic_code) - - def add_exchangeable_token( - self, - connection_name: str, - channel_id: str, - user_id: str, - exchangeable_item: str, - token: str, - ): - key = f"{connection_name}:{channel_id}:{user_id}:{exchangeable_item}" - self._exchange_store[key] = token - - def throw_on_exchange_request( - self, - connection_name: str, - channel_id: str, - user_id: str, - exchangeable_item: str, - ): - key = f"{connection_name}:{channel_id}:{user_id}:{exchangeable_item}" - self._throw_on_exchange[key] = True - - -class TestFlow: - """ - Provides a fluent interface for sending messages and asserting replies - in dialog tests. - """ - - def __init__(self, adapter: "DialogTestAdapter", callback: AgentCallbackHandler): - self._adapter = adapter - self._callback = callback - - async def send(self, msg: Union[str, Activity]) -> "TestFlow": - """Send a message or activity to the agent.""" - import asyncio as _asyncio - - # Small delay to ensure time-based timeouts (e.g. OAuthPrompt timeout=1ms) can fire. - await _asyncio.sleep(0.002) - self._adapter.active_queue.clear() - if isinstance(msg, str): - await self._adapter.send_text_to_agent_async(msg, self._callback) - else: - await self._adapter.process_activity_async(msg, self._callback) - return TestFlow(self._adapter, self._callback) - - async def assert_reply( - self, expected: Union[str, Activity, Callable, None] = None - ) -> "TestFlow": - """Assert the next reply matches the expected text, activity, or callable inspector.""" - import inspect - - reply = self._adapter.get_next_reply() - if expected is not None: - if callable(expected) and not isinstance(expected, (str, Activity)): - # Inspector callable: (activity, description) -> bool (sync or async) - result = expected(reply, None) - if inspect.isawaitable(result): - result = await result - assert ( - result is not False - ), f"Inspector returned False for reply: {reply}" - elif isinstance(expected, str): - assert reply is not None, f"Expected reply '{expected}' but got None" - assert ( - reply.text == expected - ), f"Expected reply text '{expected}' but got '{reply.text}'" - elif isinstance(expected, Activity): - assert reply is not None, "Expected a reply but got None" - if expected.text: - assert ( - reply.text == expected.text - ), f"Expected reply text '{expected.text}' but got '{reply.text}'" - if expected.type: - assert ( - reply.type == expected.type - ), f"Expected activity type '{expected.type}' but got '{reply.type}'" - return TestFlow(self._adapter, self._callback) - - -class DialogTestAdapter(MockTestingAdapter): - """ - A test adapter compatible with the botbuilder TestAdapter API. - Provides send() and assert_reply() methods for fluent test flows. - Also provides a proper UserTokenClient in turn_state for OAuthPrompt tests. - """ - - def __init__(self, callback: AgentCallbackHandler = None, **kwargs): - super().__init__(**kwargs) - self._callback = callback - # Dialog-specific token client that implements the user_token API - self._dialog_token_client = DialogUserTokenClient() - # OAuthPrompt reads claims["aud"] from the identity in turn_state - self.claims_identity = ClaimsIdentity({"aud": "test-app-id"}, True) - - def add_user_token( - self, - connection_name: str, - channel_id: str, - user_id: str, - token: str, - magic_code: str = None, - ): - """Store a user token for retrieval by OAuthPrompt.""" - self._dialog_token_client.add_user_token( - connection_name, channel_id, user_id, token, magic_code - ) - # Also update the base class token client for compatibility - super().add_user_token(connection_name, channel_id, user_id, token, magic_code) - - def add_exchangeable_token( - self, - connection_name: str, - channel_id: str, - user_id: str, - exchangeable_item: str, - token: str, - ): - self._dialog_token_client.add_exchangeable_token( - connection_name, channel_id, user_id, exchangeable_item, token - ) - super().add_exchangeable_token( - connection_name, channel_id, user_id, exchangeable_item, token - ) - - def throw_on_exchange_request( - self, - connection_name: str, - channel_id: str, - user_id: str, - exchangeable_item: str, - ): - self._dialog_token_client.throw_on_exchange_request( - connection_name, channel_id, user_id, exchangeable_item - ) - super().throw_on_exchange_request( - connection_name, channel_id, user_id, exchangeable_item - ) - - def create_turn_context( - self, activity: Activity, identity: ClaimsIdentity = None - ) -> TurnContext: - """ - Creates a turn context with the dialog token client in turn_state - so OAuthPrompt can find it via _UserTokenAccess. - """ - turn_context = super().create_turn_context(activity, identity) - turn_context.turn_state[ChannelAdapter.USER_TOKEN_CLIENT_KEY] = ( - self._dialog_token_client - ) - # OAuthPrompt reads claims["aud"] from this identity - turn_context.turn_state[ChannelAdapter.AGENT_IDENTITY_KEY] = ( - identity or self.claims_identity - ) - return turn_context - - def make_activity(self, text: str = None) -> Activity: - """ - Creates a message activity without setting locale, so that prompts' - default_locale is used when no locale is present in the activity. - This matches botbuilder TestAdapter behavior. - """ - from microsoft_agents.activity import ActivityTypes - - activity = Activity( - type=ActivityTypes.message, - from_property=self.conversation.user, - recipient=self.conversation.agent, - conversation=self.conversation.conversation, - service_url=self.conversation.service_url, - id=str(self._next_id), - text=text, - ) - self._next_id += 1 - return activity - - async def send(self, msg: Union[str, Activity]) -> TestFlow: - """Send a message or activity and return a TestFlow for assertions.""" - self.active_queue.clear() - if isinstance(msg, str): - await self.send_text_to_agent_async(msg, self._callback) - else: - await self.process_activity_async(msg, self._callback) - return TestFlow(self, self._callback) diff --git a/dev/hosting_dialogs/memory/scopes/test_memory_scopes.py b/dev/hosting_dialogs/memory/scopes/test_memory_scopes.py deleted file mode 100644 index 5eca2f3f4..000000000 --- a/dev/hosting_dialogs/memory/scopes/test_memory_scopes.py +++ /dev/null @@ -1,684 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -# pylint: disable=pointless-string-statement - -from collections import namedtuple - -import pytest - -from microsoft_agents.hosting.core import ( - ConversationState, - MemoryStorage, - TurnContext, - UserState, -) -from microsoft_agents.hosting.dialogs import ( - Dialog, - DialogContext, - DialogContainer, - DialogInstance, - DialogSet, - DialogState, - ObjectPath, -) -from microsoft_agents.hosting.dialogs.memory.scopes import ( - ClassMemoryScope, - ConversationMemoryScope, - DialogContextMemoryScope, - DialogMemoryScope, - UserMemoryScope, - SettingsMemoryScope, - ThisMemoryScope, - TurnMemoryScope, -) -from microsoft_agents.activity import ( - Activity, - ActivityTypes, - ChannelAccount, - ConversationAccount, - Channels, -) -from tests.hosting_dialogs.helpers import DialogTestAdapter - - -class _TestDialog(Dialog): - def __init__(self, id: str, message: str): - super().__init__(id) - - def aux_try_get_value(state): # pylint: disable=unused-argument - return "resolved value" - - ExpressionObject = namedtuple("ExpressionObject", "try_get_value") - self.message = message - self.expression = ExpressionObject(aux_try_get_value) - - async def begin_dialog(self, dialog_context: DialogContext, options: object = None): - dialog_context.active_dialog.state["is_dialog"] = True - await dialog_context.context.send_activity(self.message) - return Dialog.end_of_turn - - -class _TestContainer(DialogContainer): - def __init__(self, id: str, child: Dialog = None): - super().__init__(id) - self.child_id = None - if child: - self.dialogs.add(child) - self.child_id = child.id - - async def begin_dialog(self, dialog_context: DialogContext, options: object = None): - state = dialog_context.active_dialog.state - state["is_container"] = True - if self.child_id: - state["dialog"] = DialogState() - child_dc = self.create_child_context(dialog_context) - return await child_dc.begin_dialog(self.child_id, options) - - return Dialog.end_of_turn - - async def continue_dialog(self, dialog_context: DialogContext): - child_dc = self.create_child_context(dialog_context) - if child_dc: - return await child_dc.continue_dialog() - - return Dialog.end_of_turn - - def create_child_context(self, dialog_context: DialogContext): - state = dialog_context.active_dialog.state - if state["dialog"] is not None: - child_dc = DialogContext( - self.dialogs, dialog_context.context, state["dialog"] - ) - child_dc.parent = dialog_context - return child_dc - - return None - - -_begin_message = Activity( - text="begin", - type=ActivityTypes.message, - channel_id=Channels.test, - service_url="https://test.com", - from_property=ChannelAccount(id="user"), - recipient=ChannelAccount(id="bot"), - conversation=ConversationAccount(id="convo1"), -) - - -class TestMemoryScopes: - @pytest.mark.asyncio - async def test_class_memory_scope_should_find_registered_dialog(self): - # Create ConversationState with MemoryStorage and register the state as middleware. - conversation_state = ConversationState(MemoryStorage()) - - # Create a DialogState property, DialogSet and register the dialogs. - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - dialog = _TestDialog("test", "test message") - dialogs.add(dialog) - - # Create test context - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - await dialog_state.set( - context, DialogState(dialog_stack=[DialogInstance(id="test", state={})]) - ) - - dialog_context = await dialogs.create_context(context) - - # Run test - scope = ClassMemoryScope() - memory = scope.get_memory(dialog_context) - assert memory, "memory not returned" - assert memory.message == "test message" - assert memory.expression == "resolved value" - - @pytest.mark.asyncio - async def test_class_memory_scope_should_not_allow_set_memory_call(self): - # Create ConversationState with MemoryStorage and register the state as middleware. - conversation_state = ConversationState(MemoryStorage()) - - # Create a DialogState property, DialogSet and register the dialogs. - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - dialog = _TestDialog("test", "test message") - dialogs.add(dialog) - - # Create test context - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - await dialog_state.set( - context, DialogState(dialog_stack=[DialogInstance(id="test", state={})]) - ) - - dialog_context = await dialogs.create_context(context) - - # Run test - scope = ClassMemoryScope() - with pytest.raises(Exception) as exc_info: - scope.set_memory(dialog_context, {}) - - assert "not supported" in str(exc_info.value) - - @pytest.mark.asyncio - async def test_class_memory_scope_should_not_allow_load_and_save_changes_calls( - self, - ): - # Create ConversationState with MemoryStorage and register the state as middleware. - conversation_state = ConversationState(MemoryStorage()) - - # Create a DialogState property, DialogSet and register the dialogs. - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - dialog = _TestDialog("test", "test message") - dialogs.add(dialog) - - # Create test context - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - await dialog_state.set( - context, DialogState(dialog_stack=[DialogInstance(id="test", state={})]) - ) - - dialog_context = await dialogs.create_context(context) - - # Run test - scope = ClassMemoryScope() - await scope.load(dialog_context) - memory = scope.get_memory(dialog_context) - with pytest.raises(AttributeError) as exc_info: - memory.message = "foo" - - assert "can't set attribute" in str(exc_info.value) - await scope.save_changes(dialog_context) - assert dialog.message == "test message" - - def test_class_memory_scope_has_unique_name(self): - """ClassMemoryScope must use 'class', not 'settings', to avoid colliding with SettingsMemoryScope.""" - assert ClassMemoryScope().name == "class" - assert ClassMemoryScope().name != SettingsMemoryScope().name - - @pytest.mark.asyncio - async def test_conversation_memory_scope_should_return_conversation_state(self): - # Create ConversationState with MemoryStorage and register the state as middleware. - conversation_state = ConversationState(MemoryStorage()) - - # Create a DialogState property, DialogSet and register the dialogs. - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - dialog = _TestDialog("test", "test message") - dialogs.add(dialog) - - # Create test context - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - context.turn_state["ConversationState"] = conversation_state - - dialog_context = await dialogs.create_context(context) - - # Initialize conversation state - foo_cls = namedtuple("TestObject", "foo") - conversation_prop = conversation_state.create_property("conversation") - await conversation_prop.set(context, foo_cls(foo="bar")) - await conversation_state.save(context) - - # Run test - scope = ConversationMemoryScope() - memory = scope.get_memory(dialog_context) - assert memory, "memory not returned" - - # TODO: Make get_path_value take conversation.foo - test_obj = ObjectPath.get_path_value(memory, "conversation") - assert test_obj.foo == "bar" - - @pytest.mark.asyncio - async def test_user_memory_scope_should_not_return_state_if_not_loaded(self): - # Initialize user state - storage = MemoryStorage() - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - user_state = UserState(storage) - context.turn_state["UserState"] = user_state - foo_cls = namedtuple("TestObject", "foo") - user_prop = user_state.create_property("conversation") - await user_prop.set(context, foo_cls(foo="bar")) - await user_state.save(context) - - # Replace context and user_state with new instances - context = TurnContext(adapter, _begin_message) - user_state = UserState(storage) - context.turn_state["UserState"] = user_state - - # Create a DialogState property, DialogSet and register the dialogs. - conversation_state = ConversationState(storage) - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - dialog = _TestDialog("test", "test message") - dialogs.add(dialog) - - # Create test context - dialog_context = await dialogs.create_context(context) - - # Run test - scope = UserMemoryScope() - memory = scope.get_memory(dialog_context) - assert memory is None, "state returned" - - @pytest.mark.asyncio - async def test_user_memory_scope_should_return_state_once_loaded(self): - # Initialize user state - storage = MemoryStorage() - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - user_state = UserState(storage) - context.turn_state["UserState"] = user_state - foo_cls = namedtuple("TestObject", "foo") - user_prop = user_state.create_property("conversation") - await user_prop.set(context, foo_cls(foo="bar")) - await user_state.save(context) - - # Replace context and conversation_state with instances - context = TurnContext(adapter, _begin_message) - user_state = UserState(storage) - context.turn_state["UserState"] = user_state - - # Create a DialogState property, DialogSet and register the dialogs. - conversation_state = ConversationState(storage) - context.turn_state["ConversationState"] = conversation_state - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - dialog = _TestDialog("test", "test message") - dialogs.add(dialog) - - # Create test context - dialog_context = await dialogs.create_context(context) - - # Run test - scope = UserMemoryScope() - memory = scope.get_memory(dialog_context) - assert memory is None, "state returned" - - await scope.load(dialog_context) - memory = scope.get_memory(dialog_context) - assert memory is not None, "state not returned" - - # TODO: Make get_path_value take conversation.foo - test_obj = ObjectPath.get_path_value(memory, "conversation") - assert test_obj.foo == "bar" - - @pytest.mark.asyncio - async def test_dialog_memory_scope_should_return_containers_state(self): - # Create a DialogState property, DialogSet and register the dialogs. - storage = MemoryStorage() - conversation_state = ConversationState(storage) - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - container = _TestContainer("container") - dialogs.add(container) - - # Create test context - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - dialog_context = await dialogs.create_context(context) - - # Run test - scope = DialogMemoryScope() - await dialog_context.begin_dialog("container") - memory = scope.get_memory(dialog_context) - assert memory is not None, "state not returned" - assert memory["is_container"] - - @pytest.mark.asyncio - async def test_dialog_memory_scope_should_return_parent_containers_state_for_children( - self, - ): - # Create a DialogState property, DialogSet and register the dialogs. - storage = MemoryStorage() - conversation_state = ConversationState(storage) - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - container = _TestContainer("container", _TestDialog("child", "test message")) - dialogs.add(container) - - # Create test context - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - dialog_context = await dialogs.create_context(context) - - # Run test - scope = DialogMemoryScope() - await dialog_context.begin_dialog("container") - child_dc = dialog_context.child - assert child_dc is not None, "No child DC" - memory = scope.get_memory(child_dc) - assert memory is not None, "state not returned" - assert memory["is_container"] - - @pytest.mark.asyncio - async def test_dialog_memory_scope_should_return_childs_state_when_no_parent(self): - # Create a DialogState property, DialogSet and register the dialogs. - storage = MemoryStorage() - conversation_state = ConversationState(storage) - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - dialog = _TestDialog("test", "test message") - dialogs.add(dialog) - - # Create test context - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - dialog_context = await dialogs.create_context(context) - - # Run test - scope = DialogMemoryScope() - await dialog_context.begin_dialog("test") - memory = scope.get_memory(dialog_context) - assert memory is not None, "state not returned" - assert memory["is_dialog"] - - @pytest.mark.asyncio - async def test_dialog_memory_scope_should_overwrite_parents_memory(self): - # Create a DialogState property, DialogSet and register the dialogs. - storage = MemoryStorage() - conversation_state = ConversationState(storage) - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - container = _TestContainer("container", _TestDialog("child", "test message")) - dialogs.add(container) - - # Create test context - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - dialog_context = await dialogs.create_context(context) - - # Run test - scope = DialogMemoryScope() - await dialog_context.begin_dialog("container") - child_dc = dialog_context.child - assert child_dc is not None, "No child DC" - - foo_cls = namedtuple("TestObject", "foo") - scope.set_memory(child_dc, foo_cls("bar")) - memory = scope.get_memory(child_dc) - assert memory is not None, "state not returned" - assert memory.foo == "bar" - - @pytest.mark.asyncio - async def test_dialog_memory_scope_should_overwrite_active_dialogs_memory(self): - # Create a DialogState property, DialogSet and register the dialogs. - storage = MemoryStorage() - conversation_state = ConversationState(storage) - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - container = _TestContainer("container") - dialogs.add(container) - - # Create test context - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - dialog_context = await dialogs.create_context(context) - - # Run test - scope = DialogMemoryScope() - await dialog_context.begin_dialog("container") - foo_cls = namedtuple("TestObject", "foo") - scope.set_memory(dialog_context, foo_cls("bar")) - memory = scope.get_memory(dialog_context) - assert memory is not None, "state not returned" - assert memory.foo == "bar" - - @pytest.mark.asyncio - async def test_dialog_memory_scope_should_raise_error_if_set_memory_called_without_memory( - self, - ): - # Create a DialogState property, DialogSet and register the dialogs. - storage = MemoryStorage() - conversation_state = ConversationState(storage) - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - container = _TestContainer("container") - dialogs.add(container) - - # Create test context - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - dialog_context = await dialogs.create_context(context) - - # Run test - with pytest.raises(Exception): - scope = DialogMemoryScope() - await dialog_context.begin_dialog("container") - scope.set_memory(dialog_context, None) - - @pytest.mark.asyncio - async def test_dialog_memory_scope_accepts_empty_dict(self): - """set_memory() must accept an empty dict — empty dialog state is valid.""" - storage = MemoryStorage() - conversation_state = ConversationState(storage) - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - dialogs.add(_TestContainer("container")) - - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - dialog_context = await dialogs.create_context(context) - await dialog_context.begin_dialog("container") - - scope = DialogMemoryScope() - scope.set_memory(dialog_context, {}) - assert scope.get_memory(dialog_context) == {} - - @pytest.mark.skip(reason="Requires test_settings module not available") - @pytest.mark.asyncio - async def test_settings_memory_scope_should_return_content_of_settings(self): - # pylint: disable=import-outside-toplevel - from tests.hosting_dialogs.memory.scopes.test_settings import DefaultConfig - - # Create a DialogState property, DialogSet and register the dialogs. - conversation_state = ConversationState(MemoryStorage()) - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state).add(_TestDialog("test", "test message")) - - # Create test context - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - dialog_context = await dialogs.create_context(context) - settings = DefaultConfig() - dialog_context.context.turn_state["settings"] = settings - - # Run test - scope = SettingsMemoryScope() - memory = scope.get_memory(dialog_context) - assert memory is not None - assert memory.STRING == "test" - assert memory.INT == 3 - assert memory.LIST[0] == "zero" - assert memory.LIST[1] == "one" - assert memory.LIST[2] == "two" - assert memory.LIST[3] == "three" - - @pytest.mark.asyncio - async def test_this_memory_scope_should_return_active_dialogs_state(self): - # Create a DialogState property, DialogSet and register the dialogs. - storage = MemoryStorage() - conversation_state = ConversationState(storage) - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - dialog = _TestDialog("test", "test message") - dialogs.add(dialog) - - # Create test context - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - dialog_context = await dialogs.create_context(context) - - # Run test - scope = ThisMemoryScope() - await dialog_context.begin_dialog("test") - memory = scope.get_memory(dialog_context) - assert memory is not None, "state not returned" - assert memory["is_dialog"] - - @pytest.mark.asyncio - async def test_this_memory_scope_should_overwrite_active_dialogs_memory(self): - # Create a DialogState property, DialogSet and register the dialogs. - storage = MemoryStorage() - conversation_state = ConversationState(storage) - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - container = _TestContainer("container") - dialogs.add(container) - - # Create test context - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - dialog_context = await dialogs.create_context(context) - - # Run test - scope = ThisMemoryScope() - await dialog_context.begin_dialog("container") - foo_cls = namedtuple("TestObject", "foo") - scope.set_memory(dialog_context, foo_cls("bar")) - memory = scope.get_memory(dialog_context) - assert memory is not None, "state not returned" - assert memory.foo == "bar" - - @pytest.mark.asyncio - async def test_this_memory_scope_should_raise_error_if_set_memory_called_without_memory( - self, - ): - # Create a DialogState property, DialogSet and register the dialogs. - storage = MemoryStorage() - conversation_state = ConversationState(storage) - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - container = _TestContainer("container") - dialogs.add(container) - - # Create test context - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - dialog_context = await dialogs.create_context(context) - - # Run test - with pytest.raises(Exception): - scope = ThisMemoryScope() - await dialog_context.begin_dialog("container") - scope.set_memory(dialog_context, None) - - @pytest.mark.asyncio - async def test_this_memory_scope_accepts_empty_dict(self): - """set_memory() must accept an empty dict — empty dialog state is valid.""" - storage = MemoryStorage() - conversation_state = ConversationState(storage) - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - dialogs.add(_TestContainer("container")) - - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - dialog_context = await dialogs.create_context(context) - await dialog_context.begin_dialog("container") - - scope = ThisMemoryScope() - scope.set_memory(dialog_context, {}) - assert scope.get_memory(dialog_context) == {} - - @pytest.mark.asyncio - async def test_this_memory_scope_should_raise_error_if_set_memory_called_without_active_dialog( - self, - ): - # Create a DialogState property, DialogSet and register the dialogs. - storage = MemoryStorage() - conversation_state = ConversationState(storage) - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - container = _TestContainer("container") - dialogs.add(container) - - # Create test context - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - dialog_context = await dialogs.create_context(context) - - # Run test - with pytest.raises(Exception): - scope = ThisMemoryScope() - foo_cls = namedtuple("TestObject", "foo") - scope.set_memory(dialog_context, foo_cls("bar")) - - @pytest.mark.asyncio - async def test_turn_memory_scope_should_persist_changes_to_turn_state(self): - # Create a DialogState property, DialogSet and register the dialogs. - storage = MemoryStorage() - conversation_state = ConversationState(storage) - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - dialog = _TestDialog("test", "test message") - dialogs.add(dialog) - - # Create test context - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - dialog_context = await dialogs.create_context(context) - - # Run test - scope = TurnMemoryScope() - memory = scope.get_memory(dialog_context) - assert memory is not None, "state not returned" - memory["foo"] = "bar" - memory = scope.get_memory(dialog_context) - assert memory["foo"] == "bar" - - @pytest.mark.asyncio - async def test_turn_memory_scope_should_overwrite_values_in_turn_state(self): - # Create a DialogState property, DialogSet and register the dialogs. - storage = MemoryStorage() - conversation_state = ConversationState(storage) - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - dialog = _TestDialog("test", "test message") - dialogs.add(dialog) - - # Create test context - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - dialog_context = await dialogs.create_context(context) - - # Run test - scope = TurnMemoryScope() - foo_cls = namedtuple("TestObject", "foo") - scope.set_memory(dialog_context, foo_cls("bar")) - memory = scope.get_memory(dialog_context) - assert memory is not None, "state not returned" - assert memory.foo == "bar" - - @pytest.mark.asyncio - async def test_turn_memory_scope_preserves_empty_dict(self): - """An empty dict stored in turn state must not be replaced with a new CaseInsensitiveDict.""" - storage = MemoryStorage() - conversation_state = ConversationState(storage) - dialog_state = conversation_state.create_property("dialogs") - dialogs = DialogSet(dialog_state) - dialogs.add(_TestDialog("test", "test message")) - - adapter = DialogTestAdapter() - context = TurnContext(adapter, _begin_message) - dialog_context = await dialogs.create_context(context) - - scope = TurnMemoryScope() - empty = {} - scope.set_memory(dialog_context, empty) - - retrieved = scope.get_memory(dialog_context) - assert ( - retrieved is empty - ), "get_memory() must return the stored empty dict, not a new one" - - def test_dialog_context_memory_scope_has_unique_name(self): - """DialogContextMemoryScope must not share its scope name with SettingsMemoryScope.""" - dc_scope = DialogContextMemoryScope() - settings_scope = SettingsMemoryScope() - assert dc_scope.name == "dialogContext" - assert dc_scope.name != settings_scope.name diff --git a/dev/hosting_dialogs/memory/scopes/test_settings.py b/dev/hosting_dialogs/memory/scopes/test_settings.py deleted file mode 100644 index 9e21a99a6..000000000 --- a/dev/hosting_dialogs/memory/scopes/test_settings.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - - -class DefaultConfig: - """Bot Configuration""" - - STRING = os.environ.get("STRING", "test") - INT = os.environ.get("INT", 3) - LIST = os.environ.get("LIST", ["zero", "one", "two", "three"]) - NOT_TO_BE_OVERRIDDEN = os.environ.get("NOT_TO_BE_OVERRIDDEN", "one") - TO_BE_OVERRIDDEN = os.environ.get("TO_BE_OVERRIDDEN", "one") diff --git a/dev/hosting_dialogs/memory/test_at_path_resolver.py b/dev/hosting_dialogs/memory/test_at_path_resolver.py deleted file mode 100644 index 61b307e26..000000000 --- a/dev/hosting_dialogs/memory/test_at_path_resolver.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest - -from microsoft_agents.hosting.dialogs.memory.path_resolvers import AtPathResolver - -_PREFIX = "turn.recognized.entities." - - -class TestAtPathResolver: - def setup_method(self): - self.resolver = AtPathResolver() - - def test_simple_entity_no_suffix(self): - """@foo → turn.recognized.entities.foo.first()""" - assert self.resolver.transform_path("@foo") == f"{_PREFIX}foo.first()" - - def test_entity_with_dot_suffix(self): - """@foo.bar → turn.recognized.entities.foo.first().bar""" - assert self.resolver.transform_path("@foo.bar") == f"{_PREFIX}foo.first().bar" - - def test_entity_with_bracket_suffix(self): - """@foo[0] → turn.recognized.entities.foo.first()[0]""" - assert self.resolver.transform_path("@foo[0]") == f"{_PREFIX}foo.first()[0]" - - def test_entity_with_dot_and_bracket_suffix(self): - """@foo.bar[0] — dot comes before bracket, entity name is still just 'foo'""" - assert ( - self.resolver.transform_path("@foo.bar[0]") - == f"{_PREFIX}foo.first().bar[0]" - ) - - def test_non_at_path_is_returned_unchanged(self): - assert self.resolver.transform_path("turn.foo") == "turn.foo" - assert self.resolver.transform_path("user.name") == "user.name" - - def test_empty_path_raises(self): - with pytest.raises(TypeError): - self.resolver.transform_path("") - - def test_at_sign_only_is_returned_unchanged(self): - """A bare '@' has no subsequent path char so the branch is skipped.""" - assert self.resolver.transform_path("@") == "@" diff --git a/dev/hosting_dialogs/test_activity_prompt.py b/dev/hosting_dialogs/test_activity_prompt.py deleted file mode 100644 index 705f87c00..000000000 --- a/dev/hosting_dialogs/test_activity_prompt.py +++ /dev/null @@ -1,286 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest - -from microsoft_agents.hosting.dialogs.prompts import ( - ActivityPrompt, - PromptOptions, - PromptValidatorContext, -) -from microsoft_agents.activity import Activity, ActivityTypes -from microsoft_agents.hosting.core import ( - ConversationState, - MemoryStorage, - TurnContext, - MessageFactory, -) -from tests.hosting_dialogs.helpers import DialogTestAdapter -from microsoft_agents.hosting.dialogs import DialogSet, DialogTurnStatus, DialogReason - - -async def validator(prompt_context: PromptValidatorContext): - assert prompt_context.attempt_count > 0 - - activity = prompt_context.recognized.value - - if activity.type == ActivityTypes.event: - if int(activity.value) == 2: - prompt_context.recognized.value = MessageFactory.text(str(activity.value)) - return True - else: - await prompt_context.context.send_activity( - "Please send an 'event'-type Activity with a value of 2." - ) - - return False - - -class SimpleActivityPrompt(ActivityPrompt): - pass - - -class TestActivityPrompt: - def test_activity_prompt_with_empty_id_should_fail(self): - empty_id = "" - with pytest.raises(TypeError): - SimpleActivityPrompt(empty_id, validator) - - def test_activity_prompt_with_none_id_should_fail(self): - none_id = None - with pytest.raises(TypeError): - SimpleActivityPrompt(none_id, validator) - - def test_activity_prompt_with_none_validator_should_fail(self): - none_validator = None - with pytest.raises(TypeError): - SimpleActivityPrompt("EventActivityPrompt", none_validator) - - @pytest.mark.asyncio - async def test_basic_activity_prompt(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialog_state") - dialogs = DialogSet(dialog_state) - dialogs.add(SimpleActivityPrompt("EventActivityPrompt", validator)) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="please send an event." - ) - ) - await dialog_context.prompt("EventActivityPrompt", options) - elif results.status == DialogTurnStatus.Complete: - await turn_context.send_activity(results.result) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - event_activity = Activity(type=ActivityTypes.event, value=2) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("please send an event.") - step3 = await step2.send(event_activity) - await step3.assert_reply("2") - - @pytest.mark.asyncio - async def test_retry_activity_prompt(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialog_state") - dialogs = DialogSet(dialog_state) - dialogs.add(SimpleActivityPrompt("EventActivityPrompt", validator)) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="please send an event." - ) - ) - await dialog_context.prompt("EventActivityPrompt", options) - elif results.status == DialogTurnStatus.Complete: - await turn_context.send_activity(results.result) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - event_activity = Activity(type=ActivityTypes.event, value=2) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("please send an event.") - step3 = await step2.send("hello again") - step4 = await step3.assert_reply( - "Please send an 'event'-type Activity with a value of 2." - ) - step5 = await step4.send(event_activity) - await step5.assert_reply("2") - - @pytest.mark.asyncio - async def test_activity_prompt_should_return_dialog_end_if_validation_failed(self): - async def aux_validator(prompt_context: PromptValidatorContext): - assert prompt_context, "Validator missing prompt_context" - return False - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialog_state") - dialogs = DialogSet(dialog_state) - dialogs.add(SimpleActivityPrompt("EventActivityPrompt", aux_validator)) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="please send an event." - ), - retry_prompt=Activity( - type=ActivityTypes.message, text="event not received." - ), - ) - await dialog_context.prompt("EventActivityPrompt", options) - elif results.status == DialogTurnStatus.Complete: - await turn_context.send_activity(results.result) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("please send an event.") - step3 = await step2.send("test") - await step3.assert_reply("event not received.") - - @pytest.mark.asyncio - async def test_activity_prompt_resume_dialog_should_return_dialog_end(self): - async def aux_validator(prompt_context: PromptValidatorContext): - assert prompt_context, "Validator missing prompt_context" - return False - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialog_state") - dialogs = DialogSet(dialog_state) - event_prompt = SimpleActivityPrompt("EventActivityPrompt", aux_validator) - dialogs.add(event_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="please send an event." - ) - ) - await dialog_context.prompt("EventActivityPrompt", options) - - second_results = await event_prompt.resume_dialog( - dialog_context, DialogReason.NextCalled - ) - - assert ( - second_results.status == DialogTurnStatus.Waiting - ), "resume_dialog did not returned Dialog.EndOfTurn" - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("please send an event.") - await step2.assert_reply("please send an event.") - - @pytest.mark.asyncio - async def test_activity_prompt_onerror_should_return_dialogcontext(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialog_state") - dialogs = DialogSet(dialog_state) - dialogs.add(SimpleActivityPrompt("EventActivityPrompt", validator)) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="please send an event." - ) - ) - - try: - await dialog_context.prompt("EventActivityPrompt", options) - await dialog_context.prompt("Non existent id", options) - except Exception as err: - assert ( - err.data["DialogContext"] - is not None # pylint: disable=no-member - ) - assert ( - err.data["DialogContext"][ # pylint: disable=no-member - "active_dialog" - ] - == "EventActivityPrompt" - ) - else: - raise Exception("Should have thrown an error.") - - elif results.status == DialogTurnStatus.Complete: - await turn_context.send_activity(results.result) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - await adapter.send("hello") - - @pytest.mark.asyncio - async def test_activity_replace_dialog_onerror_should_return_dialogcontext(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialog_state") - dialogs = DialogSet(dialog_state) - dialogs.add(SimpleActivityPrompt("EventActivityPrompt", validator)) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="please send an event." - ) - ) - - try: - await dialog_context.prompt("EventActivityPrompt", options) - await dialog_context.replace_dialog("Non existent id", options) - except Exception as err: - assert ( - err.data["DialogContext"] - is not None # pylint: disable=no-member - ) - else: - raise Exception("Should have thrown an error.") - - elif results.status == DialogTurnStatus.Complete: - await turn_context.send_activity(results.result) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - await adapter.send("hello") diff --git a/dev/hosting_dialogs/test_attachment_prompt.py b/dev/hosting_dialogs/test_attachment_prompt.py deleted file mode 100644 index 6d1e3501a..000000000 --- a/dev/hosting_dialogs/test_attachment_prompt.py +++ /dev/null @@ -1,287 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import copy -import pytest -from microsoft_agents.hosting.dialogs.prompts import ( - AttachmentPrompt, - PromptOptions, - PromptValidatorContext, -) -from microsoft_agents.activity import Activity, ActivityTypes, Attachment, InputHints -from microsoft_agents.hosting.core import ( - TurnContext, - ConversationState, - MemoryStorage, - MessageFactory, -) -from microsoft_agents.hosting.dialogs import DialogSet, DialogTurnStatus -from tests.hosting_dialogs.helpers import DialogTestAdapter - - -class TestAttachmentPrompt: - def test_attachment_prompt_with_empty_id_should_fail(self): - with pytest.raises(TypeError): - AttachmentPrompt("") - - def test_attachment_prompt_with_none_id_should_fail(self): - with pytest.raises(TypeError): - AttachmentPrompt(None) - - @pytest.mark.asyncio - async def test_basic_attachment_prompt(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialog_state") - dialogs = DialogSet(dialog_state) - dialogs.add(AttachmentPrompt("AttachmentPrompt")) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="please add an attachment." - ) - ) - await dialog_context.prompt("AttachmentPrompt", options) - elif results.status == DialogTurnStatus.Complete: - attachment = results.result[0] - content = MessageFactory.text(attachment.content) - await turn_context.send_activity(content) - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - attachment = Attachment(content="some content", content_type="text/plain") - attachment_activity = Activity( - type=ActivityTypes.message, attachments=[attachment] - ) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("please add an attachment.") - step3 = await step2.send(attachment_activity) - await step3.assert_reply("some content") - - @pytest.mark.asyncio - async def test_attachment_prompt_with_input_hint(self): - prompt_activity = Activity( - type=ActivityTypes.message, - text="please add an attachment.", - input_hint=InputHints.accepting_input, - ) - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialog_state") - dialogs = DialogSet(dialog_state) - dialogs.add(AttachmentPrompt("AttachmentPrompt")) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions(prompt=copy.copy(prompt_activity)) - await dialog_context.prompt("AttachmentPrompt", options) - elif results.status == DialogTurnStatus.Complete: - attachment = results.result[0] - content = MessageFactory.text(attachment.content) - await turn_context.send_activity(content) - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - step1 = await adapter.send("hello") - await step1.assert_reply(prompt_activity) - - @pytest.mark.asyncio - async def test_attachment_prompt_with_validator(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialog_state") - dialogs = DialogSet(dialog_state) - - async def aux_validator(prompt_context: PromptValidatorContext): - assert prompt_context, "Validator missing prompt_context" - return prompt_context.recognized.succeeded - - dialogs.add(AttachmentPrompt("AttachmentPrompt", aux_validator)) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="please add an attachment." - ) - ) - await dialog_context.prompt("AttachmentPrompt", options) - elif results.status == DialogTurnStatus.Complete: - attachment = results.result[0] - content = MessageFactory.text(attachment.content) - await turn_context.send_activity(content) - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - attachment = Attachment(content="some content", content_type="text/plain") - attachment_activity = Activity( - type=ActivityTypes.message, attachments=[attachment] - ) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("please add an attachment.") - step3 = await step2.send(attachment_activity) - await step3.assert_reply("some content") - - @pytest.mark.asyncio - async def test_retry_attachment_prompt(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialog_state") - dialogs = DialogSet(dialog_state) - dialogs.add(AttachmentPrompt("AttachmentPrompt")) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="please add an attachment." - ) - ) - await dialog_context.prompt("AttachmentPrompt", options) - elif results.status == DialogTurnStatus.Complete: - attachment = results.result[0] - content = MessageFactory.text(attachment.content) - await turn_context.send_activity(content) - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - attachment = Attachment(content="some content", content_type="text/plain") - attachment_activity = Activity( - type=ActivityTypes.message, attachments=[attachment] - ) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("please add an attachment.") - step3 = await step2.send("hello again") - step4 = await step3.assert_reply("please add an attachment.") - step5 = await step4.send(attachment_activity) - await step5.assert_reply("some content") - - @pytest.mark.asyncio - async def test_attachment_prompt_with_custom_retry(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialog_state") - dialogs = DialogSet(dialog_state) - - async def aux_validator(prompt_context: PromptValidatorContext): - assert prompt_context, "Validator missing prompt_context" - return prompt_context.recognized.succeeded - - dialogs.add(AttachmentPrompt("AttachmentPrompt", aux_validator)) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="please add an attachment." - ), - retry_prompt=Activity( - type=ActivityTypes.message, text="please try again." - ), - ) - await dialog_context.prompt("AttachmentPrompt", options) - elif results.status == DialogTurnStatus.Complete: - attachment = results.result[0] - content = MessageFactory.text(attachment.content) - await turn_context.send_activity(content) - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - attachment = Attachment(content="some content", content_type="text/plain") - attachment_activity = Activity( - type=ActivityTypes.message, attachments=[attachment] - ) - invalid_activity = Activity(type=ActivityTypes.message, text="invalid") - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("please add an attachment.") - step3 = await step2.send(invalid_activity) - step4 = await step3.assert_reply("please try again.") - step5 = await step4.send(attachment_activity) - await step5.assert_reply("some content") - - @pytest.mark.asyncio - async def test_should_send_ignore_retry_prompt_if_validator_replies(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialog_state") - dialogs = DialogSet(dialog_state) - - async def aux_validator(prompt_context: PromptValidatorContext): - assert prompt_context, "Validator missing prompt_context" - if not prompt_context.recognized.succeeded: - await prompt_context.context.send_activity("Bad input.") - return prompt_context.recognized.succeeded - - dialogs.add(AttachmentPrompt("AttachmentPrompt", aux_validator)) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="please add an attachment." - ), - retry_prompt=Activity( - type=ActivityTypes.message, text="please try again." - ), - ) - await dialog_context.prompt("AttachmentPrompt", options) - elif results.status == DialogTurnStatus.Complete: - attachment = results.result[0] - content = MessageFactory.text(attachment.content) - await turn_context.send_activity(content) - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - attachment = Attachment(content="some content", content_type="text/plain") - attachment_activity = Activity( - type=ActivityTypes.message, attachments=[attachment] - ) - invalid_activity = Activity(type=ActivityTypes.message, text="invalid") - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("please add an attachment.") - step3 = await step2.send(invalid_activity) - step4 = await step3.assert_reply("Bad input.") - step5 = await step4.send(attachment_activity) - await step5.assert_reply("some content") - - @pytest.mark.asyncio - async def test_should_not_send_retry_if_not_specified(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialog_state") - dialogs = DialogSet(dialog_state) - dialogs.add(AttachmentPrompt("AttachmentPrompt")) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dialog_context.begin_dialog("AttachmentPrompt", PromptOptions()) - elif results.status == DialogTurnStatus.Complete: - attachment = results.result[0] - content = MessageFactory.text(attachment.content) - await turn_context.send_activity(content) - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - attachment = Attachment(content="some content", content_type="text/plain") - attachment_activity = Activity( - type=ActivityTypes.message, attachments=[attachment] - ) - - step1 = await adapter.send("hello") - step2 = await step1.send("what?") - step3 = await step2.send(attachment_activity) - await step3.assert_reply("some content") diff --git a/dev/hosting_dialogs/test_choice_prompt.py b/dev/hosting_dialogs/test_choice_prompt.py deleted file mode 100644 index 990ab6860..000000000 --- a/dev/hosting_dialogs/test_choice_prompt.py +++ /dev/null @@ -1,991 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import List - -import pytest -from recognizers_text import Culture - -from microsoft_agents.hosting.core import ConversationState, MemoryStorage, TurnContext -from microsoft_agents.hosting.core import CardFactory -from microsoft_agents.hosting.dialogs import ( - DialogSet, - DialogTurnResult, - DialogTurnStatus, - ChoiceRecognizers, - FindChoicesOptions, -) -from microsoft_agents.hosting.dialogs.choices import ( - Choice, - ChoiceFactoryOptions, - ListStyle, -) -from microsoft_agents.hosting.dialogs.prompts import ( - ChoicePrompt, - PromptCultureModel, - PromptOptions, - PromptValidatorContext, -) -from microsoft_agents.activity import Activity, ActivityTypes -from tests.hosting_dialogs.helpers import DialogTestAdapter - -_color_choices: List[Choice] = [ - Choice(value="red"), - Choice(value="green"), - Choice(value="blue"), -] - -_answer_message: Activity = Activity(text="red", type=ActivityTypes.message) -_invalid_message: Activity = Activity(text="purple", type=ActivityTypes.message) - - -class TestChoicePrompt: - def test_choice_prompt_with_empty_id_should_fail(self): - empty_id = "" - - with pytest.raises(TypeError): - ChoicePrompt(empty_id) - - def test_choice_prompt_with_none_id_should_fail(self): - none_id = None - - with pytest.raises(TypeError): - ChoicePrompt(none_id) - - @pytest.mark.asyncio - async def test_should_call_choice_prompt_using_dc_prompt(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - choice_prompt = ChoicePrompt("ChoicePrompt") - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("ChoicePrompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step3 = await step2.send(_answer_message) - await step3.assert_reply("red") - - @pytest.mark.asyncio - async def test_should_call_choice_prompt_with_custom_validator(self): - async def validator(prompt: PromptValidatorContext) -> bool: - assert prompt - - return prompt.recognized.succeeded - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt", validator) - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step3 = await step2.send(_invalid_message) - step4 = await step3.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step5 = await step4.send(_answer_message) - await step5.assert_reply("red") - - @pytest.mark.asyncio - async def test_should_send_custom_retry_prompt(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - choice_prompt = ChoicePrompt("prompt") - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - retry_prompt=Activity( - type=ActivityTypes.message, - text="Please choose red, blue, or green.", - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step3 = await step2.send(_invalid_message) - step4 = await step3.assert_reply( - "Please choose red, blue, or green. (1) red, (2) green, or (3) blue" - ) - step5 = await step4.send(_answer_message) - await step5.assert_reply("red") - - @pytest.mark.asyncio - async def test_should_send_ignore_retry_prompt_if_validator_replies(self): - async def validator(prompt: PromptValidatorContext) -> bool: - assert prompt - - if not prompt.recognized.succeeded: - await prompt.context.send_activity("Bad input.") - - return prompt.recognized.succeeded - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt", validator) - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - retry_prompt=Activity( - type=ActivityTypes.message, - text="Please choose red, blue, or green.", - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step3 = await step2.send(_invalid_message) - step4 = await step3.assert_reply("Bad input.") - step5 = await step4.send(_answer_message) - await step5.assert_reply("red") - - @pytest.mark.asyncio - async def test_should_use_default_locale_when_rendering_choices(self): - async def validator(prompt: PromptValidatorContext) -> bool: - assert prompt - - if not prompt.recognized.succeeded: - await prompt.context.send_activity("Bad input.") - - return prompt.recognized.succeeded - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt( - "prompt", validator, default_locale=Culture.Spanish - ) - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send(Activity(type=ActivityTypes.message, text="Hello")) - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, o (3) blue" - ) - step3 = await step2.send(_invalid_message) - step4 = await step3.assert_reply("Bad input.") - step5 = await step4.send(Activity(type=ActivityTypes.message, text="red")) - await step5.assert_reply("red") - - @pytest.mark.asyncio - async def test_should_use_context_activity_locale_when_rendering_choices(self): - async def validator(prompt: PromptValidatorContext) -> bool: - assert prompt - - if not prompt.recognized.succeeded: - await prompt.context.send_activity("Bad input.") - - return prompt.recognized.succeeded - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt", validator) - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send( - Activity(type=ActivityTypes.message, text="Hello", locale=Culture.Spanish) - ) - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, o (3) blue" - ) - step3 = await step2.send(_answer_message) - await step3.assert_reply("red") - - @pytest.mark.asyncio - async def test_should_use_context_activity_locale_over_default_locale_when_rendering_choices( - self, - ): - async def validator(prompt: PromptValidatorContext) -> bool: - assert prompt - - if not prompt.recognized.succeeded: - await prompt.context.send_activity("Bad input.") - - return prompt.recognized.succeeded - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt( - "prompt", validator, default_locale=Culture.Spanish - ) - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send( - Activity(type=ActivityTypes.message, text="Hello", locale=Culture.English) - ) - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step3 = await step2.send(_answer_message) - await step3.assert_reply("red") - - @pytest.mark.asyncio - async def test_should_default_to_english_locale(self): - async def validator(prompt: PromptValidatorContext) -> bool: - assert prompt - - if not prompt.recognized.succeeded: - await prompt.context.send_activity("Bad input.") - - return prompt.recognized.succeeded - - locales = [None, "", "not-supported"] - - for locale in locales: - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt", validator) - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - # Activity.locale uses NonEmptyString which rejects None/""; use model_construct to bypass - send_activity = Activity.model_construct( - type=ActivityTypes.message, text="Hello", locale=locale or None - ) - step1 = await adapter.send(send_activity) - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step3 = await step2.send(_answer_message) - await step3.assert_reply("red") - - @pytest.mark.asyncio - async def test_should_recognize_locale_variations_of_correct_locales(self): - def cap_ending(locale: str) -> str: - return f"{locale.split('-')[0]}-{locale.split('-')[1].upper()}" - - def title_ending(locale: str) -> str: - return locale[:3] + locale[3].upper() + locale[4:] - - def cap_two_letter(locale: str) -> str: - return locale.split("-")[0].upper() - - def lower_two_letter(locale: str) -> str: - return locale.split("-")[0].upper() - - async def exec_test_for_locale(valid_locale: str, locale_variations: List): - # Hold the correct answer from when a valid locale is used - expected_answer = None - - def inspector(activity: Activity, description: str): - nonlocal expected_answer - - assert not description - - if valid_locale == test_locale: - expected_answer = activity.text - else: - # Ensure we're actually testing a variation. - assert activity.locale != valid_locale - - assert activity.text == expected_answer - return True - - async def validator(prompt: PromptValidatorContext) -> bool: - assert prompt - - if not prompt.recognized.succeeded: - await prompt.context.send_activity("Bad input.") - - return prompt.recognized.succeeded - - test_locale = None - for test_locale in locale_variations: - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt", validator) - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, - text="Please choose a color.", - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send( - Activity( - type=ActivityTypes.message, text="Hello", locale=test_locale - ) - ) - await step1.assert_reply(inspector) - - locales = [ - "zh-cn", - "nl-nl", - "en-us", - "fr-fr", - "de-de", - "it-it", - "ja-jp", - "ko-kr", - "pt-br", - "es-es", - "tr-tr", - "de-de", - ] - - locale_tests = [] - for locale in locales: - locale_tests.append( - [ - locale, - cap_ending(locale), - title_ending(locale), - cap_two_letter(locale), - lower_two_letter(locale), - ] - ) - - # Test each valid locale - for locale_test in locale_tests: - await exec_test_for_locale(locale_test[0], locale_test) - - @pytest.mark.asyncio - async def test_should_recognize_and_use_custom_locale_dict(self): - async def validator(prompt: PromptValidatorContext) -> bool: - assert prompt - - if not prompt.recognized.succeeded: - await prompt.context.send_activity("Bad input.") - - return prompt.recognized.succeeded - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - culture = PromptCultureModel( - locale="custom-locale", - no_in_language="customNo", - yes_in_language="customYes", - separator="customSeparator", - inline_or="customInlineOr", - inline_or_more="customInlineOrMore", - ) - - custom_dict = { - culture.locale: ChoiceFactoryOptions( - inline_or=culture.inline_or, - inline_or_more=culture.inline_or_more, - inline_separator=culture.separator, - include_numbers=True, - ) - } - - choice_prompt = ChoicePrompt("prompt", validator, choice_defaults=custom_dict) - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send( - Activity(type=ActivityTypes.message, text="Hello", locale=culture.locale) - ) - await step1.assert_reply( - "Please choose a color. (1) redcustomSeparator(2) greencustomInlineOrMore(3) blue" - ) - - @pytest.mark.asyncio - async def test_should_not_render_choices_if_list_style_none_is_specified(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt") - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - style=ListStyle.none, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply("Please choose a color.") - step3 = await step2.send(_answer_message) - await step3.assert_reply("red") - - @pytest.mark.asyncio - async def test_should_create_prompt_with_inline_choices_when_specified(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt") - choice_prompt.style = ListStyle.in_line - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step3 = await step2.send(_answer_message) - await step3.assert_reply("red") - - @pytest.mark.asyncio - async def test_should_create_prompt_with_list_choices_when_specified(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt") - choice_prompt.style = ListStyle.list_style - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply( - "Please choose a color.\n\n 1. red\n 2. green\n 3. blue" - ) - step3 = await step2.send(_answer_message) - await step3.assert_reply("red") - - @pytest.mark.asyncio - async def test_should_create_prompt_with_suggested_action_style_when_specified( - self, - ): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt") - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - style=ListStyle.suggested_action, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply("Please choose a color.") - step3 = await step2.send(_answer_message) - await step3.assert_reply("red") - - @pytest.mark.asyncio - async def test_should_create_prompt_with_auto_style_when_specified(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt") - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - style=ListStyle.auto, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step3 = await step2.send(_answer_message) - await step3.assert_reply("red") - - @pytest.mark.asyncio - async def test_should_recognize_valid_number_choice(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt") - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a color." - ), - choices=_color_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply( - "Please choose a color. (1) red, (2) green, or (3) blue" - ) - step3 = await step2.send("1") - await step3.assert_reply("red") - - @pytest.mark.asyncio - async def test_should_display_choices_on_hero_card(self): - size_choices = ["large", "medium", "small"] - - def assert_expected_activity( - activity: Activity, description - ): # pylint: disable=unused-argument - assert len(activity.attachments) == 1 - assert ( - activity.attachments[0].content_type - == CardFactory.content_types.hero_card - ) - assert activity.attachments[0].content.text == "Please choose a size." - return True - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt") - # Change the ListStyle of the prompt to ListStyle.hero_card. - choice_prompt.style = ListStyle.hero_card - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please choose a size." - ), - choices=size_choices, - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply(assert_expected_activity) - step3 = await step2.send("1") - await step3.assert_reply(size_choices[0]) - - @pytest.mark.asyncio - async def test_should_display_choices_on_hero_card_with_additional_attachment(self): - size_choices = ["large", "medium", "small"] - card = CardFactory.adaptive_card( - { - "type": "AdaptiveCard", - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "version": "1.2", - "body": [], - } - ) - card_activity = Activity(type=ActivityTypes.message, attachments=[card]) - - def assert_expected_activity( - activity: Activity, description - ): # pylint: disable=unused-argument - assert len(activity.attachments) == 2 - assert ( - activity.attachments[0].content_type - == CardFactory.content_types.adaptive_card - ) - assert ( - activity.attachments[1].content_type - == CardFactory.content_types.hero_card - ) - return True - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ChoicePrompt("prompt") - # Change the ListStyle of the prompt to ListStyle.hero_card. - choice_prompt.style = ListStyle.hero_card - dialogs.add(choice_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions(prompt=card_activity, choices=size_choices) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("Hello") - await step1.assert_reply(assert_expected_activity) - - def test_should_not_find_a_choice_in_an_utterance_by_ordinal(self): - found = ChoiceRecognizers.recognize_choices( - "the first one please", - _color_choices, - FindChoicesOptions(recognize_numbers=False, recognize_ordinals=False), - ) - assert not found - - def test_should_not_find_a_choice_in_an_utterance_by_numerical_index(self): - found = ChoiceRecognizers.recognize_choices( - "one", - _color_choices, - FindChoicesOptions(recognize_numbers=False, recognize_ordinals=False), - ) - assert not found - - @pytest.mark.asyncio - async def test_choice_prompt_with_empty_choices_renders_but_errors_on_response( - self, - ): - """ChoicePrompt with an empty choices list renders the prompt without - error, but raises TypeError when the user responds. - - This is because Find.find_choices() treats an empty list the same as - None and raises TypeError: "Find: choices cannot be None." Always - provide at least one Choice when using ChoicePrompt. - """ - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - dialogs.add(ChoicePrompt("ChoicePrompt")) - - turns = [] - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - try: - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Choose one:"), - choices=[], - ) - await dialog_context.prompt("ChoicePrompt", options) - turns.append("prompted") - else: - turns.append("continued") - except TypeError as exc: - turns.append(f"error:{exc}") - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - # First turn: prompt is sent without error - await adapter.send("hello") - assert turns[-1] == "prompted" - - # Second turn: recognition raises because choices is empty - await adapter.send("red") - assert turns[-1].startswith("error:") diff --git a/dev/hosting_dialogs/test_component_dialog.py b/dev/hosting_dialogs/test_component_dialog.py deleted file mode 100644 index 2edc46aff..000000000 --- a/dev/hosting_dialogs/test_component_dialog.py +++ /dev/null @@ -1,371 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest - -from microsoft_agents.activity import Activity, ActivityTypes -from microsoft_agents.hosting.core import ConversationState, MemoryStorage -from microsoft_agents.hosting.dialogs import ( - ComponentDialog, - Dialog, - DialogSet, - DialogTurnResult, - DialogTurnStatus, - WaterfallDialog, - WaterfallStepContext, -) -from microsoft_agents.hosting.dialogs.models.dialog_reason import DialogReason -from microsoft_agents.hosting.dialogs.prompts import NumberPrompt, PromptOptions -from tests.hosting_dialogs.helpers import DialogTestAdapter - - -def _number_prompt_options(text: str) -> PromptOptions: - return PromptOptions(prompt=Activity(type=ActivityTypes.message, text=text)) - - -class TestComponentDialog: - @pytest.mark.asyncio - async def test_begin_dialog_null_dc_raises(self): - dialog = ComponentDialog("dialogId") - with pytest.raises((TypeError, Exception)): - await dialog.begin_dialog(None) - - @pytest.mark.asyncio - async def test_basic_waterfall_with_number_prompt(self): - """A two-step waterfall collects two numbers via NumberPrompt.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - async def step1(step): - return await step.prompt( - "number", _number_prompt_options("Enter a number.") - ) - - async def step2(step): - await step.context.send_activity(f"Thanks for '{int(step.result)}'") - return await step.prompt( - "number", _number_prompt_options("Enter another number.") - ) - - ds.add(WaterfallDialog("test-waterfall", [step1, step2])) - ds.add(NumberPrompt("number", default_locale="en-us")) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dc.begin_dialog("test-waterfall") - elif results.status == DialogTurnStatus.Complete: - await tc.send_activity( - f"Bot received the number '{int(results.result)}'." - ) - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - flow = await adapter.send("hello") - flow = await flow.assert_reply("Enter a number.") - flow = await flow.send("42") - flow = await flow.assert_reply("Thanks for '42'") - flow = await flow.assert_reply("Enter another number.") - flow = await flow.send("64") - await flow.assert_reply("Bot received the number '64'.") - - @pytest.mark.asyncio - async def test_basic_component_dialog(self): - """ComponentDialog encapsulates its own waterfall and prompt.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - class TestComp(ComponentDialog): - def __init__(self): - super().__init__("TestComponentDialog") - - async def step1(step): - return await step.prompt( - "number", _number_prompt_options("Enter a number.") - ) - - async def step2(step): - await step.context.send_activity(f"Thanks for '{int(step.result)}'") - return await step.prompt( - "number", _number_prompt_options("Enter another number.") - ) - - self.add_dialog(WaterfallDialog("test-waterfall", [step1, step2])) - self.add_dialog(NumberPrompt("number", default_locale="en-us")) - - ds.add(TestComp()) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dc.begin_dialog("TestComponentDialog") - elif results.status == DialogTurnStatus.Complete: - await tc.send_activity( - f"Bot received the number '{int(results.result)}'." - ) - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - flow = await adapter.send("hello") - flow = await flow.assert_reply("Enter a number.") - flow = await flow.send("42") - flow = await flow.assert_reply("Thanks for '42'") - flow = await flow.assert_reply("Enter another number.") - flow = await flow.send("64") - await flow.assert_reply("Bot received the number '64'.") - - @pytest.mark.asyncio - async def test_call_dialog_in_parent_component(self): - """A child component can call a dialog registered in its parent.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - child_component = ComponentDialog("childComponent") - - async def child_step1(step): - await step.context.send_activity("Child started.") - return await step.begin_dialog("parentDialog", "test") - - async def child_step2(step): - await step.context.send_activity(f"Child finished. Value: {step.result}") - return await step.end_dialog() - - child_component.add_dialog( - WaterfallDialog("childDialog", [child_step1, child_step2]) - ) - - parent_component = ComponentDialog("parentComponent") - parent_component.add_dialog(child_component) - - async def parent_step(step): - await step.context.send_activity("Parent called.") - return await step.end_dialog(step.options) - - parent_component.add_dialog(WaterfallDialog("parentDialog", [parent_step])) - ds.add(parent_component) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dc.begin_dialog("parentComponent") - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - flow = await adapter.send("Hi") - flow = await flow.assert_reply("Child started.") - flow = await flow.assert_reply("Parent called.") - await flow.assert_reply("Child finished. Value: test") - - @pytest.mark.asyncio - async def test_call_dialog_defined_in_parent_component(self): - """Child can call parent-registered dialog and receive the return value.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - options = {"value": "test"} - - child_component = ComponentDialog("childComponent") - - async def child_step1(step): - await step.context.send_activity("Child started.") - return await step.begin_dialog("parentDialog", options) - - async def child_step2(step): - assert step.result == "test" - await step.context.send_activity("Child finished.") - return await step.end_dialog() - - child_component.add_dialog( - WaterfallDialog("childDialog", [child_step1, child_step2]) - ) - - parent_component = ComponentDialog("parentComponent") - parent_component.add_dialog(child_component) - - async def parent_step(step): - step_options = step.options - await step.context.send_activity( - f"Parent called with: {step_options['value']}" - ) - return await step.end_dialog(step_options["value"]) - - parent_component.add_dialog(WaterfallDialog("parentDialog", [parent_step])) - ds.add(parent_component) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dc.begin_dialog("parentComponent") - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - flow = await adapter.send("Hi") - flow = await flow.assert_reply("Child started.") - flow = await flow.assert_reply("Parent called with: test") - await flow.assert_reply("Child finished.") - - @pytest.mark.asyncio - async def test_nested_component_dialog(self): - """Nested ComponentDialogs properly pass control between each other.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - class InnerComp(ComponentDialog): - def __init__(self): - super().__init__("TestComponentDialog") - - async def step1(step): - return await step.prompt( - "number", _number_prompt_options("Enter a number.") - ) - - async def step2(step): - await step.context.send_activity(f"Thanks for '{int(step.result)}'") - return await step.prompt( - "number", _number_prompt_options("Enter another number.") - ) - - self.add_dialog(WaterfallDialog("test-waterfall", [step1, step2])) - self.add_dialog(NumberPrompt("number", default_locale="en-us")) - - class OuterComp(ComponentDialog): - def __init__(self): - super().__init__("TestNestedComponentDialog") - - async def step1(step): - return await step.prompt( - "number", _number_prompt_options("Enter a number.") - ) - - async def step2(step): - await step.context.send_activity(f"Thanks for '{int(step.result)}'") - return await step.prompt( - "number", _number_prompt_options("Enter another number.") - ) - - async def step3(step): - await step.context.send_activity(f"Got '{int(step.result)}'.") - return await step.begin_dialog("TestComponentDialog") - - self.add_dialog( - WaterfallDialog("test-waterfall", [step1, step2, step3]) - ) - self.add_dialog(NumberPrompt("number", default_locale="en-us")) - self.add_dialog(InnerComp()) - - ds.add(OuterComp()) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dc.begin_dialog("TestNestedComponentDialog") - elif results.status == DialogTurnStatus.Complete: - await tc.send_activity( - f"Bot received the number '{int(results.result)}'." - ) - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - flow = await adapter.send("hello") - flow = await flow.assert_reply("Enter a number.") - flow = await flow.send("42") - flow = await flow.assert_reply("Thanks for '42'") - flow = await flow.assert_reply("Enter another number.") - flow = await flow.send("64") - flow = await flow.assert_reply("Got '64'.") - flow = await flow.assert_reply("Enter a number.") - flow = await flow.send("101") - flow = await flow.assert_reply("Thanks for '101'") - flow = await flow.assert_reply("Enter another number.") - flow = await flow.send("5") - await flow.assert_reply("Bot received the number '5'.") - - -class TestComponentDialogOnEndDialogHook: - @pytest.mark.asyncio - async def test_on_end_dialog_called_with_cancel_reason(self): - """on_end_dialog hook receives CancelCalled reason when cancel_all_dialogs() is invoked.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - hook_calls = [] - - class TrackingComp(ComponentDialog): - def __init__(self): - super().__init__("TrackingComp") - - async def waiting_step(step): - return Dialog.end_of_turn - - self.add_dialog(WaterfallDialog("inner-wf", [waiting_step])) - - async def on_end_dialog(self, context, instance, reason): - hook_calls.append(reason) - - ds.add(TrackingComp()) - - turn = [0] - - async def exec(tc): - turn[0] += 1 - dc = await ds.create_context(tc) - if turn[0] == 1: - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dc.begin_dialog("TrackingComp") - else: - # Cancel without continuing so the waterfall doesn't advance - await dc.cancel_all_dialogs() - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - await adapter.send("hi") # starts the dialog, step 0 waits - await adapter.send("cancel") # triggers cancel_all_dialogs directly - - assert DialogReason.CancelCalled in hook_calls - - @pytest.mark.asyncio - async def test_on_end_dialog_called_with_end_reason_on_completion(self): - """on_end_dialog hook receives EndCalled reason when the component finishes normally.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - hook_calls = [] - - class TrackingComp(ComponentDialog): - def __init__(self): - super().__init__("TrackingComp") - - async def ending_step(step): - return await step.end_dialog("done") - - self.add_dialog(WaterfallDialog("inner-wf", [ending_step])) - - async def on_end_dialog(self, context, instance, reason): - hook_calls.append(reason) - - ds.add(TrackingComp()) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dc.begin_dialog("TrackingComp") - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - await adapter.send("hi") # starts and immediately completes the component - - assert DialogReason.EndCalled in hook_calls diff --git a/dev/hosting_dialogs/test_confirm_prompt.py b/dev/hosting_dialogs/test_confirm_prompt.py deleted file mode 100644 index b15a20301..000000000 --- a/dev/hosting_dialogs/test_confirm_prompt.py +++ /dev/null @@ -1,493 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import List - -import pytest - -from microsoft_agents.hosting.core import ( - ConversationState, - MemoryStorage, - TurnContext, - MessageFactory, -) -from microsoft_agents.hosting.dialogs import ( - DialogSet, - DialogTurnResult, - DialogTurnStatus, -) -from microsoft_agents.hosting.dialogs.choices import ( - Choice, - ChoiceFactoryOptions, - ListStyle, -) -from microsoft_agents.hosting.dialogs.prompts import ( - ConfirmPrompt, - PromptCultureModel, - PromptOptions, - PromptValidatorContext, -) -from microsoft_agents.activity import Activity, ActivityTypes -from tests.hosting_dialogs.helpers import DialogTestAdapter - - -class TestConfirmPrompt: - def test_confirm_prompt_with_empty_id_should_fail(self): - empty_id = "" - - with pytest.raises(TypeError): - ConfirmPrompt(empty_id) - - def test_confirm_prompt_with_none_id_should_fail(self): - none_id = None - - with pytest.raises(TypeError): - ConfirmPrompt(none_id) - - @pytest.mark.asyncio - async def test_confirm_prompt(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - confirm_prompt = ConfirmPrompt("ConfirmPrompt", default_locale="English") - dialogs.add(confirm_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Please confirm.") - ) - await dialog_context.prompt("ConfirmPrompt", options) - elif results.status == DialogTurnStatus.Complete: - message_text = "Confirmed" if results.result else "Not confirmed" - await turn_context.send_activity(MessageFactory.text(message_text)) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("Please confirm. (1) Yes or (2) No") - step3 = await step2.send("yes") - await step3.assert_reply("Confirmed") - - @pytest.mark.asyncio - async def test_confirm_prompt_retry(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - confirm_prompt = ConfirmPrompt("ConfirmPrompt", default_locale="English") - dialogs.add(confirm_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Please confirm."), - retry_prompt=Activity( - type=ActivityTypes.message, - text="Please confirm, say 'yes' or 'no' or something like that.", - ), - ) - await dialog_context.prompt("ConfirmPrompt", options) - elif results.status == DialogTurnStatus.Complete: - message_text = "Confirmed" if results.result else "Not confirmed" - await turn_context.send_activity(MessageFactory.text(message_text)) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("Please confirm. (1) Yes or (2) No") - step3 = await step2.send("lala") - step4 = await step3.assert_reply( - "Please confirm, say 'yes' or 'no' or something like that. (1) Yes or (2) No" - ) - step5 = await step4.send("no") - await step5.assert_reply("Not confirmed") - - @pytest.mark.asyncio - async def test_confirm_prompt_no_options(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - confirm_prompt = ConfirmPrompt("ConfirmPrompt", default_locale="English") - dialogs.add(confirm_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - await dialog_context.prompt("ConfirmPrompt", PromptOptions()) - elif results.status == DialogTurnStatus.Complete: - message_text = "Confirmed" if results.result else "Not confirmed" - await turn_context.send_activity(MessageFactory.text(message_text)) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply(" (1) Yes or (2) No") - step3 = await step2.send("lala") - step4 = await step3.assert_reply(" (1) Yes or (2) No") - step5 = await step4.send("no") - await step5.assert_reply("Not confirmed") - - @pytest.mark.asyncio - async def test_confirm_prompt_choice_options_numbers(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - confirm_prompt = ConfirmPrompt("ConfirmPrompt", default_locale="English") - confirm_prompt.choice_options = ChoiceFactoryOptions(include_numbers=True) - confirm_prompt.style = ListStyle.in_line - dialogs.add(confirm_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Please confirm."), - retry_prompt=Activity( - type=ActivityTypes.message, - text="Please confirm, say 'yes' or 'no' or something like that.", - ), - ) - await dialog_context.prompt("ConfirmPrompt", options) - elif results.status == DialogTurnStatus.Complete: - message_text = "Confirmed" if results.result else "Not confirmed" - await turn_context.send_activity(MessageFactory.text(message_text)) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("Please confirm. (1) Yes or (2) No") - step3 = await step2.send("lala") - step4 = await step3.assert_reply( - "Please confirm, say 'yes' or 'no' or something like that. (1) Yes or (2) No" - ) - step5 = await step4.send("2") - await step5.assert_reply("Not confirmed") - - @pytest.mark.asyncio - async def test_confirm_prompt_choice_options_multiple_attempts(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - confirm_prompt = ConfirmPrompt("ConfirmPrompt", default_locale="English") - confirm_prompt.choice_options = ChoiceFactoryOptions(include_numbers=True) - confirm_prompt.style = ListStyle.in_line - dialogs.add(confirm_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Please confirm."), - retry_prompt=Activity( - type=ActivityTypes.message, - text="Please confirm, say 'yes' or 'no' or something like that.", - ), - ) - await dialog_context.prompt("ConfirmPrompt", options) - elif results.status == DialogTurnStatus.Complete: - message_text = "Confirmed" if results.result else "Not confirmed" - await turn_context.send_activity(MessageFactory.text(message_text)) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("Please confirm. (1) Yes or (2) No") - step3 = await step2.send("lala") - step4 = await step3.assert_reply( - "Please confirm, say 'yes' or 'no' or something like that. (1) Yes or (2) No" - ) - step5 = await step4.send("what") - step6 = await step5.assert_reply( - "Please confirm, say 'yes' or 'no' or something like that. (1) Yes or (2) No" - ) - step7 = await step6.send("2") - await step7.assert_reply("Not confirmed") - - @pytest.mark.asyncio - async def test_confirm_prompt_options_no_numbers(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - confirm_prompt = ConfirmPrompt("ConfirmPrompt", default_locale="English") - confirm_prompt.choice_options = ChoiceFactoryOptions( - include_numbers=False, inline_separator="~" - ) - dialogs.add(confirm_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Please confirm."), - retry_prompt=Activity( - type=ActivityTypes.message, - text="Please confirm, say 'yes' or 'no' or something like that.", - ), - ) - await dialog_context.prompt("ConfirmPrompt", options) - elif results.status == DialogTurnStatus.Complete: - message_text = "Confirmed" if results.result else "Not confirmed" - await turn_context.send_activity(MessageFactory.text(message_text)) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("Please confirm. Yes or No") - step3 = await step2.send("2") - step4 = await step3.assert_reply( - "Please confirm, say 'yes' or 'no' or something like that. Yes or No" - ) - step5 = await step4.send("no") - await step5.assert_reply("Not confirmed") - - @pytest.mark.asyncio - async def test_confirm_prompt_should_default_to_english_locale(self): - locales = [None, "", "not-supported"] - - for locale in locales: - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - confirm_prompt = ConfirmPrompt("ConfirmPrompt") - confirm_prompt.choice_options = ChoiceFactoryOptions(include_numbers=True) - dialogs.add(confirm_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please confirm." - ), - retry_prompt=Activity( - type=ActivityTypes.message, - text="Please confirm, say 'yes' or 'no' or something like that.", - ), - ) - await dialog_context.prompt("ConfirmPrompt", options) - elif results.status == DialogTurnStatus.Complete: - message_text = "Confirmed" if results.result else "Not confirmed" - await turn_context.send_activity(MessageFactory.text(message_text)) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - # Activity.locale uses NonEmptyString which rejects None/""; use model_construct to bypass - send_activity = Activity.model_construct( - type=ActivityTypes.message, text="Hello", locale=locale or None - ) - step1 = await adapter.send(send_activity) - step2 = await step1.assert_reply("Please confirm. (1) Yes or (2) No") - step3 = await step2.send("lala") - step4 = await step3.assert_reply( - "Please confirm, say 'yes' or 'no' or something like that. (1) Yes or (2) No" - ) - step5 = await step4.send("2") - await step5.assert_reply("Not confirmed") - - @pytest.mark.asyncio - async def test_should_recognize_locale_variations_of_correct_locales(self): - def cap_ending(locale: str) -> str: - return f"{locale.split('-')[0]}-{locale.split('-')[1].upper()}" - - def title_ending(locale: str) -> str: - return locale[:3] + locale[3].upper() + locale[4:] - - def cap_two_letter(locale: str) -> str: - return locale.split("-")[0].upper() - - def lower_two_letter(locale: str) -> str: - return locale.split("-")[0].upper() - - async def exec_test_for_locale(valid_locale: str, locale_variations: List): - # Hold the correct answer from when a valid locale is used - expected_answer = None - - def inspector(activity: Activity, description: str): - nonlocal expected_answer - - assert not description - - if valid_locale == test_locale: - expected_answer = activity.text - else: - # Ensure we're actually testing a variation. - assert activity.locale != valid_locale - - assert activity.text == expected_answer - return True - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please confirm." - ) - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - confirmed = results.result - if confirmed: - await turn_context.send_activity("true") - else: - await turn_context.send_activity("false") - - await convo_state.save(turn_context) - - async def validator(prompt: PromptValidatorContext) -> bool: - assert prompt - - if not prompt.recognized.succeeded: - await prompt.context.send_activity("Bad input.") - - return prompt.recognized.succeeded - - test_locale = None - for test_locale in locale_variations: - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - choice_prompt = ConfirmPrompt("prompt", validator) - dialogs.add(choice_prompt) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send( - Activity( - type=ActivityTypes.message, text="Hello", locale=test_locale - ) - ) - await step1.assert_reply(inspector) - - locales = [ - "zh-cn", - "nl-nl", - "en-us", - "fr-fr", - "de-de", - "it-it", - "ja-jp", - "ko-kr", - "pt-br", - "es-es", - "tr-tr", - "de-de", - ] - - locale_tests = [] - for locale in locales: - locale_tests.append( - [ - locale, - cap_ending(locale), - title_ending(locale), - cap_two_letter(locale), - lower_two_letter(locale), - ] - ) - - # Test each valid locale - for locale_test in locale_tests: - await exec_test_for_locale(locale_test[0], locale_test) - - @pytest.mark.asyncio - async def test_should_recognize_and_use_custom_locale_dict(self): - async def validator(prompt: PromptValidatorContext) -> bool: - assert prompt - - if not prompt.recognized.succeeded: - await prompt.context.send_activity("Bad input.") - - return prompt.recognized.succeeded - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - culture = PromptCultureModel( - locale="custom-locale", - no_in_language="customNo", - yes_in_language="customYes", - separator="customSeparator", - inline_or="customInlineOr", - inline_or_more="customInlineOrMore", - ) - - custom_dict = { - culture.locale: ( - Choice(culture.yes_in_language), - Choice(culture.no_in_language), - ChoiceFactoryOptions( - culture.separator, culture.inline_or, culture.inline_or_more, True - ), - ) - } - - confirm_prompt = ConfirmPrompt("prompt", validator, choice_defaults=custom_dict) - dialogs.add(confirm_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - - results: DialogTurnResult = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Please confirm.") - ) - await dialog_context.prompt("prompt", options) - elif results.status == DialogTurnStatus.Complete: - selected_choice = results.result - await turn_context.send_activity(selected_choice.value) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send( - Activity(type=ActivityTypes.message, text="Hello", locale=culture.locale) - ) - await step1.assert_reply( - "Please confirm. (1) customYescustomInlineOr(2) customNo" - ) diff --git a/dev/hosting_dialogs/test_date_time_prompt.py b/dev/hosting_dialogs/test_date_time_prompt.py deleted file mode 100644 index ef2aeb9d0..000000000 --- a/dev/hosting_dialogs/test_date_time_prompt.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest - -from microsoft_agents.hosting.dialogs.prompts import DateTimePrompt, PromptOptions -from microsoft_agents.hosting.core import ( - MessageFactory, - ConversationState, - MemoryStorage, - TurnContext, -) -from microsoft_agents.hosting.dialogs import DialogSet, DialogTurnStatus -from tests.hosting_dialogs.helpers import DialogTestAdapter - - -class TestDatetimePrompt: - @pytest.mark.asyncio - async def test_date_time_prompt(self): - # Create new ConversationState with MemoryStorage and register the state as middleware. - conver_state = ConversationState(MemoryStorage()) - - # Create a DialogState property - dialog_state = conver_state.create_property("dialogState") - - # Create new DialogSet. - dialogs = DialogSet(dialog_state) - - # Create and add DateTime prompt to DialogSet. - date_time_prompt = DateTimePrompt("DateTimePrompt") - dialogs.add(date_time_prompt) - - async def exec_test(turn_context: TurnContext) -> None: - prompt_msg = "What date would you like?" - dialog_context = await dialogs.create_context(turn_context) - - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions(prompt=MessageFactory.text(prompt_msg)) - await dialog_context.begin_dialog("DateTimePrompt", options) - else: - if results.status == DialogTurnStatus.Complete: - resolution = results.result[0] - reply = MessageFactory.text( - f"Timex: '{resolution.timex}' Value: '{resolution.value}'" - ) - await turn_context.send_activity(reply) - await conver_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("What date would you like?") - step3 = await step2.send("5th December 2018 at 9am") - await step3.assert_reply("Timex: '2018-12-05T09' Value: '2018-12-05 09:00:00'") - - @pytest.mark.asyncio - async def test_date_time_prompt_retry_on_invalid_input(self): - """DateTimePrompt sends the retry_prompt when input cannot be recognized, then accepts valid input.""" - conver_state = ConversationState(MemoryStorage()) - dialog_state = conver_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - dialogs.add(DateTimePrompt("DateTimePrompt")) - - async def exec_test(turn_context: TurnContext) -> None: - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=MessageFactory.text("What date?"), - retry_prompt=MessageFactory.text("Not a valid date. Try again."), - ) - await dialog_context.begin_dialog("DateTimePrompt", options) - elif results.status == DialogTurnStatus.Complete: - resolution = results.result[0] - await turn_context.send_activity( - MessageFactory.text(f"Timex: '{resolution.timex}'") - ) - await conver_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("What date?") - step3 = await step2.send("not a date at all xyz") - step4 = await step3.assert_reply("Not a valid date. Try again.") - step5 = await step4.send("5th December 2018") - await step5.assert_reply("Timex: '2018-12-05'") diff --git a/dev/hosting_dialogs/test_dialog.py b/dev/hosting_dialogs/test_dialog.py deleted file mode 100644 index 3bf99d2bf..000000000 --- a/dev/hosting_dialogs/test_dialog.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest -from unittest.mock import AsyncMock, MagicMock - -from microsoft_agents.hosting.dialogs import ( - Dialog, - DialogTurnResult, - DialogTurnStatus, - DialogReason, -) - - -class _ConcreteDialog(Dialog): - """Minimal concrete Dialog for testing base class behavior.""" - - async def begin_dialog(self, dialog_context, options=None): - return DialogTurnResult(DialogTurnStatus.Complete) - - -class TestDialog: - def test_null_id_raises(self): - with pytest.raises((TypeError, Exception)): - _ConcreteDialog(None) - - def test_blank_id_raises(self): - with pytest.raises((TypeError, Exception)): - _ConcreteDialog(" ") - - def test_telemetry_client_defaults_non_none(self): - dialog = _ConcreteDialog("A") - assert dialog.telemetry_client is not None - - def test_get_version_returns_id(self): - dialog = _ConcreteDialog("my-dialog") - assert dialog.get_version() == "my-dialog" - - def test_id_property(self): - dialog = _ConcreteDialog("test-id") - assert dialog.id == "test-id" - - @pytest.mark.asyncio - async def test_continue_dialog_calls_end_dialog(self): - """Default continue_dialog calls end_dialog(None) → Complete.""" - dialog = _ConcreteDialog("A") - dc = MagicMock() - dc.end_dialog = AsyncMock( - return_value=DialogTurnResult(DialogTurnStatus.Complete) - ) - - result = await dialog.continue_dialog(dc) - - assert result.status == DialogTurnStatus.Complete - dc.end_dialog.assert_awaited_once_with(None) - - @pytest.mark.asyncio - async def test_resume_dialog_calls_end_dialog_with_result(self): - """Default resume_dialog calls end_dialog(result) → Complete.""" - dialog = _ConcreteDialog("A") - dc = MagicMock() - dc.end_dialog = AsyncMock( - return_value=DialogTurnResult(DialogTurnStatus.Complete, "done") - ) - - result = await dialog.resume_dialog(dc, DialogReason.BeginCalled, "done") - - assert result.status == DialogTurnStatus.Complete - dc.end_dialog.assert_awaited_once_with("done") - - @pytest.mark.asyncio - async def test_reprompt_dialog_is_noop(self): - """Default reprompt_dialog is a no-op (no exception).""" - dialog = _ConcreteDialog("A") - await dialog.reprompt_dialog(MagicMock(), MagicMock()) - - @pytest.mark.asyncio - async def test_end_dialog_is_noop(self): - """Default end_dialog is a no-op (no exception).""" - dialog = _ConcreteDialog("A") - await dialog.end_dialog(MagicMock(), MagicMock(), DialogReason.EndCalled) diff --git a/dev/hosting_dialogs/test_dialog_context.py b/dev/hosting_dialogs/test_dialog_context.py deleted file mode 100644 index 288b7d0ac..000000000 --- a/dev/hosting_dialogs/test_dialog_context.py +++ /dev/null @@ -1,293 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest -from unittest.mock import MagicMock - -from microsoft_agents.activity import Activity, ActivityTypes -from microsoft_agents.hosting.dialogs import ( - ComponentDialog, - Dialog, - DialogContext, - DialogSet, - DialogState, - DialogTurnStatus, - WaterfallDialog, - WaterfallStepContext, - DialogTurnResult, -) -from microsoft_agents.hosting.dialogs.models.dialog_instance import DialogInstance -from microsoft_agents.hosting.dialogs.prompts import TextPrompt, PromptOptions -from microsoft_agents.hosting.core import ConversationState, MemoryStorage -from tests.hosting_dialogs.helpers import DialogTestAdapter - - -def _make_dc(): - """Create a minimal DialogContext backed by a MagicMock TurnContext.""" - - class _Stub(ComponentDialog): - def __init__(self): - super().__init__("stub") - - ds = _Stub()._dialogs - mock_tc = MagicMock() - return DialogContext(ds, mock_tc, DialogState()) - - -class TestDialogContext: - def test_null_dialog_set_raises(self): - with pytest.raises(TypeError): - DialogContext(None, MagicMock(), DialogState()) - - def test_null_turn_context_raises(self): - dc = _make_dc() - with pytest.raises(TypeError): - DialogContext(dc.dialogs, None, DialogState()) - - @pytest.mark.asyncio - async def test_begin_dialog_empty_id_raises(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - adapter = DialogTestAdapter() - - async def callback(tc): - dc = await ds.create_context(tc) - with pytest.raises(TypeError): - await dc.begin_dialog("") - - await adapter.send_text_to_agent_async("hi", callback) - - @pytest.mark.asyncio - async def test_prompt_empty_id_raises(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - adapter = DialogTestAdapter() - - async def callback(tc): - dc = await ds.create_context(tc) - with pytest.raises(TypeError): - await dc.prompt("", MagicMock()) - - await adapter.send_text_to_agent_async("hi", callback) - - @pytest.mark.asyncio - async def test_prompt_none_options_raises(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - adapter = DialogTestAdapter() - - async def callback(tc): - dc = await ds.create_context(tc) - with pytest.raises(TypeError): - await dc.prompt("somePrompt", None) - - await adapter.send_text_to_agent_async("hi", callback) - - @pytest.mark.asyncio - async def test_continue_dialog_empty_stack_returns_empty(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - result_holder = {} - adapter = DialogTestAdapter() - - async def callback(tc): - dc = await ds.create_context(tc) - result = await dc.continue_dialog() - result_holder["status"] = result.status - - await adapter.send_text_to_agent_async("hi", callback) - assert result_holder["status"] == DialogTurnStatus.Empty - - def test_active_dialog_none_on_empty_stack(self): - dc = _make_dc() - assert dc.active_dialog is None - - def test_child_none_when_no_active_dialog(self): - dc = _make_dc() - assert dc.child is None - - def test_find_dialog_sync_returns_none_for_unknown(self): - dc = _make_dc() - assert dc.find_dialog_sync("nonexistent") is None - - @pytest.mark.asyncio - async def test_cancel_all_dialogs_empty_stack_returns_empty(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - result_holder = {} - adapter = DialogTestAdapter() - - async def callback(tc): - dc = await ds.create_context(tc) - result = await dc.cancel_all_dialogs() - result_holder["status"] = result.status - - await adapter.send_text_to_agent_async("hi", callback) - assert result_holder["status"] == DialogTurnStatus.Empty - - @pytest.mark.asyncio - async def test_begin_dialog_unknown_id_raises(self): - """begin_dialog raises when the dialog ID is not registered.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - adapter = DialogTestAdapter() - - async def callback(tc): - dc = await ds.create_context(tc) - with pytest.raises(Exception): - await dc.begin_dialog("does-not-exist") - - await adapter.send_text_to_agent_async("hi", callback) - - @pytest.mark.asyncio - async def test_replace_dialog_ends_active_and_starts_new(self): - """replace_dialog pops the active dialog and starts the replacement.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - completed = {} - - async def step1(step): - await step.context.send_activity("step1") - return Dialog.end_of_turn - - async def step2(step): - await step.context.send_activity("replacement") - return await step.end_dialog() - - ds.add(WaterfallDialog("first", [step1])) - ds.add(WaterfallDialog("second", [step2])) - - adapter = DialogTestAdapter() - - async def callback(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dc.begin_dialog("first") - elif results.status == DialogTurnStatus.Waiting: - await dc.replace_dialog("second") - await convo_state.save(tc) - - step = await adapter.send_text_to_agent_async("hi", callback) - step = await adapter.send_text_to_agent_async("next", callback) - completed["done"] = True - assert completed["done"] - - @pytest.mark.asyncio - async def test_find_dialog_returns_none_for_unknown(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - result_holder = {} - adapter = DialogTestAdapter() - - async def callback(tc): - dc = await ds.create_context(tc) - found = await dc.find_dialog("nonexistent") - result_holder["found"] = found - - await adapter.send_text_to_agent_async("hi", callback) - assert result_holder["found"] is None - - @pytest.mark.asyncio - async def test_reprompt_dialog_resends_prompt_activity(self): - """reprompt_dialog re-sends the original prompt when a TextPrompt is waiting.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - ds.add(TextPrompt("text-prompt")) - - async def start_prompt(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Please enter text." - ) - ) - await dc.prompt("text-prompt", options) - await convo_state.save(tc) - - async def do_reprompt(tc): - dc = await ds.create_context(tc) - await dc.reprompt_dialog() - await convo_state.save(tc) - - adapter = DialogTestAdapter() - - await adapter.send_text_to_agent_async("hi", start_prompt) - first_prompt = adapter.get_next_reply() - assert first_prompt is not None - assert first_prompt.text == "Please enter text." - - adapter.active_queue.clear() - await adapter.send_text_to_agent_async("anything", do_reprompt) - reprompt_reply = adapter.get_next_reply() - assert reprompt_reply is not None - assert reprompt_reply.text == "Please enter text." - - @pytest.mark.asyncio - async def test_emit_event_bubbles_to_parent_dc(self): - """Events emitted with bubble=True propagate from the inner DC to the parent DC's active dialog.""" - from unittest.mock import MagicMock - - captured_events = [] - - class CapturingDialog(WaterfallDialog): - async def _on_pre_bubble_event(self, dc, event): - captured_events.append(event.name) - return True # mark as handled; stop bubbling - - # ComponentDialog subclasses are the only way to get a no-state DialogSet - class _OuterHolder(ComponentDialog): - def __init__(self): - super().__init__("_outer_holder") - - class _InnerHolder(ComponentDialog): - def __init__(self): - super().__init__("_inner_holder") - - # Outer DC: has CapturingDialog active on the stack - outer_ds = _OuterHolder()._dialogs - outer_ds.add(CapturingDialog("capturing", [])) - outer_state = DialogState() - outer_instance = DialogInstance() - outer_instance.id = "capturing" - outer_instance.state = {} - outer_state.dialog_stack.insert(0, outer_instance) - mock_tc = MagicMock() - outer_dc = DialogContext(outer_ds, mock_tc, outer_state) - - # Inner DC: has a plain WaterfallDialog active; parent is outer_dc - inner_ds = _InnerHolder()._dialogs - inner_ds.add(WaterfallDialog("inner-wf", [])) - inner_state = DialogState() - inner_instance = DialogInstance() - inner_instance.id = "inner-wf" - inner_instance.state = {} - inner_state.dialog_stack.insert(0, inner_instance) - inner_dc = DialogContext(inner_ds, mock_tc, inner_state) - inner_dc.parent = outer_dc - - handled = await inner_dc.emit_event("custom.test.event", "payload", bubble=True) - - assert ( - handled - ), "Event should have been caught by the outer dialog's _on_pre_bubble_event" - assert "custom.test.event" in captured_events diff --git a/dev/hosting_dialogs/test_dialog_extensions.py b/dev/hosting_dialogs/test_dialog_extensions.py deleted file mode 100644 index 082e49ed0..000000000 --- a/dev/hosting_dialogs/test_dialog_extensions.py +++ /dev/null @@ -1,188 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# pylint: disable=ungrouped-imports -import enum -from typing import List -import uuid - -import pytest - -from microsoft_agents.hosting.core import ClaimsIdentity -from microsoft_agents.hosting.core.authorization import AuthenticationConstants -from microsoft_agents.hosting.core import ( - TurnContext, - MessageFactory, - MemoryStorage, - ConversationState, - UserState, -) -from microsoft_agents.activity import ActivityTypes, Activity, EndOfConversationCodes -from microsoft_agents.hosting.dialogs import ( - ComponentDialog, - TextPrompt, - WaterfallDialog, - DialogInstance, - DialogReason, - WaterfallStepContext, - PromptOptions, - Dialog, - DialogExtensions, - DialogEvents, -) -from tests.hosting_dialogs.helpers import DialogTestAdapter - - -class SimpleComponentDialog(ComponentDialog): - def __init__(self): - super().__init__("SimpleComponentDialog") - - self.add_dialog(TextPrompt("TextPrompt")) - self.add_dialog( - WaterfallDialog("WaterfallDialog", [self.prompt_for_name, self.final_step]) - ) - - self.initial_dialog_id = "WaterfallDialog" - self.end_reason = DialogReason.BeginCalled - - async def end_dialog( - self, context: TurnContext, instance: DialogInstance, reason: DialogReason - ) -> None: - self.end_reason = reason - return await super().end_dialog(context, instance, reason) - - async def prompt_for_name(self, step_context: WaterfallStepContext): - return await step_context.prompt( - "TextPrompt", - PromptOptions( - prompt=MessageFactory.text("Hello, what is your name?"), - retry_prompt=MessageFactory.text("Hello, what is your name again?"), - ), - ) - - async def final_step(self, step_context: WaterfallStepContext): - await step_context.context.send_activity( - f"Hello {step_context.result}, nice to meet you!" - ) - return await step_context.end_dialog(step_context.result) - - -class FlowTestCase(enum.Enum): - root_bot_only = 1 - root_bot_consuming_skill = 2 - middle_skill = 3 - leaf_skill = 4 - - -class TestDialogExtensions: - def setup_method(self): - self.eoc_sent: Activity = None - self.skill_bot_id = str(uuid.uuid4()) - self.parent_bot_id = str(uuid.uuid4()) - - def _create_test_flow( - self, dialog: Dialog, test_case: FlowTestCase - ) -> DialogTestAdapter: - """ - Creates a DialogTestAdapter configured for the given test case. - Returns the adapter (which supports send/assert_reply as a TestFlow entry point). - """ - conversation_id = str(uuid.uuid4()) - storage = MemoryStorage() - convo_state = ConversationState(storage) - - eoc_sent_ref = [None] - self.eoc_sent = None - - async def logic(context: TurnContext): - if test_case != FlowTestCase.root_bot_only: - claims_identity = ClaimsIdentity( - { - AuthenticationConstants.VERSION_CLAIM: "2.0", - AuthenticationConstants.AUDIENCE_CLAIM: self.skill_bot_id, - AuthenticationConstants.AUTHORIZED_PARTY: self.parent_bot_id, - }, - True, - ) - context._identity = claims_identity - - async def capture_eoc( - inner_context: TurnContext, activities: List[Activity], next - ): # pylint: disable=unused-argument - for activity in activities: - if activity.type == ActivityTypes.end_of_conversation: - self.eoc_sent = activity - eoc_sent_ref[0] = activity - break - return await next() - - context.on_send_activities(capture_eoc) - - await DialogExtensions.run_dialog( - dialog, context, convo_state.create_property("DialogState") - ) - - return DialogTestAdapter(logic) - - @pytest.mark.asyncio - async def test_handles_root_bot_only(self): - dialog = SimpleComponentDialog() - adapter = self._create_test_flow(dialog, FlowTestCase.root_bot_only) - - step1 = await adapter.send("Hi") - step2 = await step1.assert_reply("Hello, what is your name?") - step3 = await step2.send("SomeName") - await step3.assert_reply("Hello SomeName, nice to meet you!") - - assert dialog.end_reason == DialogReason.EndCalled - assert ( - self.eoc_sent is None - ), "Root bot should not send EndConversation to channel" - - @pytest.mark.skip( - reason="Requires skill infrastructure (SkillHandler/SkillConversationReference) not available in new SDK" - ) - @pytest.mark.asyncio - async def test_handles_root_bot_consuming_skill(self): - pass - - @pytest.mark.skip( - reason="Requires skill infrastructure (SkillHandler/SkillConversationReference) not available in new SDK" - ) - @pytest.mark.asyncio - async def test_handles_middle_skill(self): - pass - - @pytest.mark.skip( - reason="Requires skill infrastructure (SkillHandler/SkillConversationReference) not available in new SDK" - ) - @pytest.mark.asyncio - async def test_handles_leaf_skill(self): - pass - - @pytest.mark.skip( - reason="Requires skill infrastructure (SkillHandler/SkillConversationReference) not available in new SDK" - ) - @pytest.mark.asyncio - async def test_skill_handles_eoc_from_parent(self): - pass - - @pytest.mark.asyncio - async def test_skill_handles_reprompt_from_parent(self): - """ - Tests that a reprompt event causes the dialog to re-prompt. - This test does not require skill infrastructure. - """ - dialog = SimpleComponentDialog() - adapter = self._create_test_flow(dialog, FlowTestCase.root_bot_only) - - step1 = await adapter.send("Hi") - step2 = await step1.assert_reply("Hello, what is your name?") - await step2.send( - Activity( - type=ActivityTypes.event, - name=DialogEvents.reprompt_dialog, - ) - ) - - assert dialog.end_reason == DialogReason.BeginCalled diff --git a/dev/hosting_dialogs/test_dialog_manager.py b/dev/hosting_dialogs/test_dialog_manager.py deleted file mode 100644 index 28af29edf..000000000 --- a/dev/hosting_dialogs/test_dialog_manager.py +++ /dev/null @@ -1,281 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -# pylint: disable=pointless-string-statement - -from enum import Enum -from typing import Callable, List, Tuple - -import pytest - -from microsoft_agents.hosting.core import ( - ConversationState, - MemoryStorage, - MessageFactory, - UserState, - TurnContext, -) -from microsoft_agents.hosting.core import ClaimsIdentity -from microsoft_agents.hosting.core.authorization import AuthenticationConstants -from microsoft_agents.hosting.dialogs import ( - ComponentDialog, - Dialog, - DialogContext, - DialogEvents, - DialogInstance, - DialogReason, - TextPrompt, - WaterfallDialog, - DialogManager, - DialogManagerResult, - DialogTurnStatus, - WaterfallStepContext, -) -from microsoft_agents.hosting.dialogs.prompts import PromptOptions -from microsoft_agents.activity import ( - Activity, - ActivityTypes, - ChannelAccount, - ConversationAccount, - EndOfConversationCodes, - InputHints, - Channels, -) -from tests.hosting_dialogs.helpers import DialogTestAdapter - - -class SkillFlowTestCase(str, Enum): - # DialogManager is executing on a root bot with no skills (typical standalone bot). - root_bot_only = "RootBotOnly" - - # DialogManager is executing on a root bot handling replies from a skill. - root_bot_consuming_skill = "RootBotConsumingSkill" - - # DialogManager is executing in a skill that is called from a root and calling another skill. - middle_skill = "MiddleSkill" - - # DialogManager is executing in a skill that is called from a parent (a root or another skill) but doesn't call - # another skill. - leaf_skill = "LeafSkill" - - -class SimpleComponentDialog(ComponentDialog): - # An App ID for a parent bot. - parent_bot_id = "00000000-0000-0000-0000-0000000000PARENT" - - # An App ID for a skill bot. - skill_bot_id = "00000000-0000-0000-0000-00000000000SKILL" - - # Captures an EndOfConversation if it was sent to help with assertions. - eoc_sent: Activity = None - - # Property to capture the DialogManager turn results and do assertions. - dm_turn_result: DialogManagerResult = None - - def __init__( - self, id: str = None, prop: str = None - ): # pylint: disable=unused-argument - super().__init__(id or "SimpleComponentDialog") - self.text_prompt = "TextPrompt" - self.waterfall_dialog = "WaterfallDialog" - self.add_dialog(TextPrompt(self.text_prompt)) - self.add_dialog( - WaterfallDialog( - self.waterfall_dialog, - [ - self.prompt_for_name, - self.final_step, - ], - ) - ) - self.initial_dialog_id = self.waterfall_dialog - self.end_reason = None - - @staticmethod - async def create_test_flow( - dialog: Dialog, - test_case: SkillFlowTestCase = SkillFlowTestCase.root_bot_only, - enabled_trace=False, - ) -> DialogTestAdapter: - conversation_id = "testFlowConversationId" - storage = MemoryStorage() - conversation_state = ConversationState(storage) - user_state = UserState(storage) - - activity = Activity( - type=ActivityTypes.message, - channel_id=Channels.test, - service_url="https://test.com", - from_property=ChannelAccount(id="user1", name="User1"), - recipient=ChannelAccount(id="bot", name="Bot"), - conversation=ConversationAccount( - is_group=False, conversation_type=conversation_id, id=conversation_id - ), - ) - - dialog_manager = DialogManager(dialog) - dialog_manager.user_state = user_state - dialog_manager.conversation_state = conversation_state - - async def logic(context: TurnContext): - if test_case != SkillFlowTestCase.root_bot_only: - # Create a skill ClaimsIdentity and put it in turn_state so isSkillClaim() returns True. - claims_identity = ClaimsIdentity({}, False) - claims_identity.claims["ver"] = ( - "2.0" # AuthenticationConstants.VersionClaim - ) - claims_identity.claims["aud"] = ( - SimpleComponentDialog.skill_bot_id - ) # AuthenticationConstants.AudienceClaim - claims_identity.claims["azp"] = ( - SimpleComponentDialog.parent_bot_id - ) # AuthenticationConstants.AuthorizedParty - context._identity = claims_identity - - # Note: SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY based skill routing - # is not available in the new SDK. Skill flow test cases that require it are skipped. - - async def aux( - turn_context: TurnContext, # pylint: disable=unused-argument - activities: List[Activity], - next: Callable, - ): - for activity in activities: - if activity.type == ActivityTypes.end_of_conversation: - SimpleComponentDialog.eoc_sent = activity - break - - return await next() - - # Interceptor to capture the EoC activity if it was sent so we can assert it in the tests. - context.on_send_activities(aux) - - SimpleComponentDialog.dm_turn_result = await dialog_manager.on_turn(context) - - # Manually save state since AutoSaveStateMiddleware is not available - await conversation_state.save(context) - await user_state.save(context) - - adapter = DialogTestAdapter(logic) - if enabled_trace: - adapter.enable_trace = True - - return adapter - - async def on_end_dialog( - self, context: DialogContext, instance: DialogInstance, reason: DialogReason - ): - self.end_reason = reason - return await super().on_end_dialog(context, instance, reason) - - async def prompt_for_name(self, step: WaterfallStepContext): - return await step.prompt( - self.text_prompt, - PromptOptions( - prompt=MessageFactory.text( - "Hello, what is your name?", None, InputHints.expecting_input - ), - retry_prompt=MessageFactory.text( - "Hello, what is your name again?", None, InputHints.expecting_input - ), - ), - ) - - async def final_step(self, step: WaterfallStepContext): - await step.context.send_activity(f"Hello { step.result }, nice to meet you!") - return await step.end_dialog(step.result) - - -class TestDialogManager: - @pytest.mark.asyncio - async def test_handles_root_bot_only(self): - SimpleComponentDialog.dm_turn_result = None - SimpleComponentDialog.eoc_sent = None - dialog = SimpleComponentDialog() - test_flow = await SimpleComponentDialog.create_test_flow( - dialog, SkillFlowTestCase.root_bot_only - ) - step1 = await test_flow.send("Hi") - step2 = await step1.assert_reply("Hello, what is your name?") - step3 = await step2.send("SomeName") - await step3.assert_reply("Hello SomeName, nice to meet you!") - - assert ( - SimpleComponentDialog.dm_turn_result.turn_result.status - == DialogTurnStatus.Complete - ) - assert dialog.end_reason == DialogReason.EndCalled - assert ( - SimpleComponentDialog.eoc_sent is None - ), "Root bot should not send EndConversation to channel" - - @pytest.mark.skip( - reason="Requires SkillHandler/SkillConversationReference skill infrastructure not available in new SDK" - ) - @pytest.mark.asyncio - async def test_handles_root_bot_consuming_skill(self): - pass - - @pytest.mark.skip( - reason="Requires SkillHandler/SkillConversationReference skill infrastructure not available in new SDK" - ) - @pytest.mark.asyncio - async def test_handles_middle_skill(self): - pass - - @pytest.mark.skip( - reason="Requires SkillHandler/SkillConversationReference skill infrastructure not available in new SDK" - ) - @pytest.mark.asyncio - async def test_handles_leaf_skill(self): - pass - - @pytest.mark.skip( - reason="Requires SkillHandler/SkillConversationReference skill infrastructure not available in new SDK" - ) - @pytest.mark.asyncio - async def test_skill_handles_eoc_from_parent(self): - pass - - @pytest.mark.skip( - reason="Requires SkillHandler/SkillConversationReference skill infrastructure not available in new SDK" - ) - @pytest.mark.asyncio - async def test_skill_handles_reprompt_from_parent(self): - pass - - @pytest.mark.skip( - reason="Requires SkillHandler/SkillConversationReference skill infrastructure not available in new SDK" - ) - @pytest.mark.asyncio - async def test_skill_should_return_empty_on_reprompt_with_no_dialog(self): - pass - - @pytest.mark.asyncio - async def test_trace_bot_state(self): - SimpleComponentDialog.dm_turn_result = None - dialog = SimpleComponentDialog() - - def assert_is_trace(activity, description): # pylint: disable=unused-argument - assert activity.type == ActivityTypes.trace - return True - - def assert_is_trace_and_label(activity, description): - assert_is_trace(activity, description) - assert activity.label == "Bot State" - return True - - test_flow = await SimpleComponentDialog.create_test_flow( - dialog, SkillFlowTestCase.root_bot_only, True - ) - - step1 = await test_flow.send("Hi") - step2 = await step1.assert_reply("Hello, what is your name?") - step3 = await step2.assert_reply(assert_is_trace_and_label) - step4 = await step3.send("SomeName") - step5 = await step4.assert_reply("Hello SomeName, nice to meet you!") - await step5.assert_reply(assert_is_trace_and_label) - - assert ( - SimpleComponentDialog.dm_turn_result.turn_result.status - == DialogTurnStatus.Complete - ) diff --git a/dev/hosting_dialogs/test_dialog_set.py b/dev/hosting_dialogs/test_dialog_set.py deleted file mode 100644 index ef9fee1ef..000000000 --- a/dev/hosting_dialogs/test_dialog_set.py +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest -from microsoft_agents.hosting.dialogs import DialogSet, ComponentDialog, WaterfallDialog -from microsoft_agents.hosting.core import ConversationState, MemoryStorage -from microsoft_agents.hosting.dialogs._telemetry_client import NullTelemetryClient - - -class MyBotTelemetryClient(NullTelemetryClient): - def __init__(self): - super().__init__() - - -class TestDialogSet: - def test_dialogset_constructor_valid(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state_property = convo_state.create_property("dialogstate") - dialog_set = DialogSet(dialog_state_property) - assert dialog_set is not None - - def test_dialogset_constructor_null_property(self): - with pytest.raises(TypeError): - DialogSet(None) - - def test_dialogset_constructor_null_from_componentdialog(self): - ComponentDialog("MyId") - - def test_dialogset_telemetryset(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state_property = convo_state.create_property("dialogstate") - dialog_set = DialogSet(dialog_state_property) - - dialog_set.add(WaterfallDialog("A")) - dialog_set.add(WaterfallDialog("B")) - - assert isinstance( - dialog_set.find_dialog("A").telemetry_client, NullTelemetryClient - ) - assert isinstance( - dialog_set.find_dialog("B").telemetry_client, NullTelemetryClient - ) - - dialog_set.telemetry_client = MyBotTelemetryClient() - - assert isinstance( - dialog_set.find_dialog("A").telemetry_client, MyBotTelemetryClient - ) - assert isinstance( - dialog_set.find_dialog("B").telemetry_client, MyBotTelemetryClient - ) - - def test_add_duplicate_dialog_id_raises(self): - """Adding a second dialog with the same ID raises TypeError.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state_property = convo_state.create_property("dialogstate") - dialog_set = DialogSet(dialog_state_property) - - dialog_set.add(WaterfallDialog("A")) - with pytest.raises(TypeError): - dialog_set.add(WaterfallDialog("A")) - - def test_add_none_dialog_raises(self): - """Adding None raises TypeError.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state_property = convo_state.create_property("dialogstate") - dialog_set = DialogSet(dialog_state_property) - with pytest.raises(TypeError): - dialog_set.add(None) - - def test_get_version_is_stable(self): - """get_version() returns the same hash on repeated calls.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state_property = convo_state.create_property("dialogstate") - dialog_set = DialogSet(dialog_state_property) - dialog_set.add(WaterfallDialog("A")) - v1 = dialog_set.get_version() - v2 = dialog_set.get_version() - assert v1 == v2 - - def test_dialogset_nulltelemetryset(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state_property = convo_state.create_property("dialogstate") - dialog_set = DialogSet(dialog_state_property) - - dialog_set.add(WaterfallDialog("A")) - dialog_set.add(WaterfallDialog("B")) - - dialog_set.telemetry_client = MyBotTelemetryClient() - dialog_set.telemetry_client = None - - assert not isinstance( - dialog_set.find_dialog("A").telemetry_client, MyBotTelemetryClient - ) - assert not isinstance( - dialog_set.find_dialog("B").telemetry_client, MyBotTelemetryClient - ) - assert isinstance( - dialog_set.find_dialog("A").telemetry_client, NullTelemetryClient - ) - assert isinstance( - dialog_set.find_dialog("B").telemetry_client, NullTelemetryClient - ) - - def test_get_version_is_invalidated_after_add(self): - """get_version() must reflect all dialogs even when add() is called after an earlier get_version().""" - convo_state = ConversationState(MemoryStorage()) - dialog_state_property = convo_state.create_property("dialogstate") - dialog_set = DialogSet(dialog_state_property) - - dialog_set.add(WaterfallDialog("A")) - version_before = dialog_set.get_version() - - dialog_set.add(WaterfallDialog("B")) - version_after = dialog_set.get_version() - - assert version_before != version_after, ( - "get_version() returned the same hash after adding a new dialog; " - "the version cache was not invalidated by add()" - ) - - def test_get_version_is_invalidated_after_telemetry_change(self): - """Changing the telemetry client must invalidate the cached version.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state_property = convo_state.create_property("dialogstate") - dialog_set = DialogSet(dialog_state_property) - dialog_set.add(WaterfallDialog("A")) - - version_before = dialog_set.get_version() - dialog_set.telemetry_client = MyBotTelemetryClient() - version_after = dialog_set.get_version() - - # Versions may or may not differ depending on whether telemetry affects - # individual dialog versions, but the cache must at least be recomputed - # (i.e. get_version() must not return the stale pre-change value from cache). - # We verify this by checking the cache was cleared (recomputed == same value is fine). - assert version_after is not None diff --git a/dev/hosting_dialogs/test_number_prompt.py b/dev/hosting_dialogs/test_number_prompt.py deleted file mode 100644 index 06f018276..000000000 --- a/dev/hosting_dialogs/test_number_prompt.py +++ /dev/null @@ -1,378 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from typing import Callable - -import pytest -from recognizers_text import Culture - -from microsoft_agents.hosting.core import ( - TurnContext, - ConversationState, - MemoryStorage, - MessageFactory, -) -from microsoft_agents.hosting.dialogs import DialogSet, DialogTurnStatus, DialogContext -from microsoft_agents.hosting.dialogs.prompts import ( - NumberPrompt, - PromptOptions, - PromptValidatorContext, -) -from microsoft_agents.activity import Activity, ActivityTypes -from tests.hosting_dialogs.helpers import DialogTestAdapter - - -class NumberPromptMock(NumberPrompt): - def __init__( - self, - dialog_id: str, - validator: Callable[[PromptValidatorContext], bool] = None, - default_locale=None, - ): - super().__init__(dialog_id, validator, default_locale) - - async def on_prompt_null_context(self, options: PromptOptions): - # Should throw TypeError - await self.on_prompt( - turn_context=None, state=None, options=options, is_retry=False - ) - - async def on_prompt_null_options(self, dialog_context: DialogContext): - # Should throw TypeError - await self.on_prompt( - dialog_context.context, state=None, options=None, is_retry=False - ) - - async def on_recognize_null_context(self): - # Should throw TypeError - await self.on_recognize(turn_context=None, state=None, options=None) - - -class TestNumberPrompt: - def test_empty_id_should_fail(self): - empty_id = "" - with pytest.raises(TypeError): - NumberPrompt(empty_id) - - def test_none_id_should_fail(self): - with pytest.raises(TypeError): - NumberPrompt(dialog_id=None) - - @pytest.mark.asyncio - async def test_with_null_turn_context_should_fail(self): - number_prompt_mock = NumberPromptMock("NumberPromptMock") - - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Please send a number.") - ) - - with pytest.raises(TypeError): - await number_prompt_mock.on_prompt_null_context(options) - - @pytest.mark.asyncio - async def test_on_prompt_with_null_options_fails(self): - conver_state = ConversationState(MemoryStorage()) - dialog_state = conver_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - number_prompt_mock = NumberPromptMock( - dialog_id="NumberPromptMock", validator=None, default_locale=Culture.English - ) - dialogs.add(number_prompt_mock) - - with pytest.raises(TypeError): - await number_prompt_mock.on_recognize_null_context() - - @pytest.mark.asyncio - async def test_number_prompt(self): - # Create new ConversationState with MemoryStorage and register the state as middleware. - conver_state = ConversationState(MemoryStorage()) - - # Create a DialogState property, DialogSet and register the WaterfallDialog. - dialog_state = conver_state.create_property("dialogState") - - dialogs = DialogSet(dialog_state) - - # Create and add number prompt to DialogSet. - number_prompt = NumberPrompt("NumberPrompt", None, Culture.English) - dialogs.add(number_prompt) - - async def exec_test(turn_context: TurnContext) -> None: - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - await dialog_context.begin_dialog( - "NumberPrompt", - PromptOptions( - prompt=MessageFactory.text("Enter quantity of cable") - ), - ) - else: - if results.status == DialogTurnStatus.Complete: - number_result = results.result - await turn_context.send_activity( - MessageFactory.text( - f"You asked me for '{number_result}' meters of cable." - ) - ) - - await conver_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply("Enter quantity of cable") - step3 = await step2.send("Give me twenty meters of cable") - await step3.assert_reply("You asked me for '20' meters of cable.") - - @pytest.mark.asyncio - async def test_number_prompt_retry(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - number_prompt = NumberPrompt( - dialog_id="NumberPrompt", validator=None, default_locale=Culture.English - ) - dialogs.add(number_prompt) - - async def exec_test(turn_context: TurnContext) -> None: - dialog_context: DialogContext = await dialogs.create_context(turn_context) - - results = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Enter a number."), - retry_prompt=Activity( - type=ActivityTypes.message, text="You must enter a number." - ), - ) - await dialog_context.prompt("NumberPrompt", options) - elif results.status == DialogTurnStatus.Complete: - number_result = results.result - await turn_context.send_activity( - MessageFactory.text(f"Bot received the number '{number_result}'.") - ) - - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("Enter a number.") - step3 = await step2.send("hello") - step4 = await step3.assert_reply("You must enter a number.") - step5 = await step4.send("64") - await step5.assert_reply("Bot received the number '64'.") - - @pytest.mark.asyncio - async def test_number_uses_locale_specified_in_constructor(self): - # Create new ConversationState with MemoryStorage and register the state as middleware. - conver_state = ConversationState(MemoryStorage()) - - # Create a DialogState property, DialogSet and register the WaterfallDialog. - dialog_state = conver_state.create_property("dialogState") - - dialogs = DialogSet(dialog_state) - - # Create and add number prompt to DialogSet. - number_prompt = NumberPrompt( - "NumberPrompt", None, default_locale=Culture.Spanish - ) - dialogs.add(number_prompt) - - async def exec_test(turn_context: TurnContext) -> None: - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - await dialog_context.begin_dialog( - "NumberPrompt", - PromptOptions( - prompt=MessageFactory.text( - "How much money is in your gaming account?" - ) - ), - ) - else: - if results.status == DialogTurnStatus.Complete: - number_result = results.result - await turn_context.send_activity( - MessageFactory.text( - f"You say you have ${number_result} in your gaming account." - ) - ) - - await conver_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply("How much money is in your gaming account?") - step3 = await step2.send("I've got $1.200.555,42 in my account.") - await step3.assert_reply("You say you have $1200555.42 in your gaming account.") - - @pytest.mark.asyncio - async def test_number_prompt_validator(self): - # Create new ConversationState with MemoryStorage and register the state as middleware. - conver_state = ConversationState(MemoryStorage()) - - # Create a DialogState property, DialogSet and register the WaterfallDialog. - dialog_state = conver_state.create_property("dialogState") - - dialogs = DialogSet(dialog_state) - - # Create and add number prompt to DialogSet. - async def validator(prompt_context: PromptValidatorContext): - result = prompt_context.recognized.value - - if 0 < result < 100: - return True - - return False - - number_prompt = NumberPrompt( - "NumberPrompt", validator, default_locale=Culture.English - ) - dialogs.add(number_prompt) - - async def exec_test(turn_context: TurnContext) -> None: - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Enter a number."), - retry_prompt=Activity( - type=ActivityTypes.message, - text="You must enter a positive number less than 100.", - ), - ) - await dialog_context.prompt("NumberPrompt", options) - - elif results.status == DialogTurnStatus.Complete: - number_result = int(results.result) - await turn_context.send_activity( - MessageFactory.text(f"Bot received the number '{number_result}'.") - ) - - await conver_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("Enter a number.") - step3 = await step2.send("150") - step4 = await step3.assert_reply( - "You must enter a positive number less than 100." - ) - step5 = await step4.send("64") - await step5.assert_reply("Bot received the number '64'.") - - @pytest.mark.asyncio - async def test_float_number_prompt(self): - # Create new ConversationState with MemoryStorage and register the state as middleware. - conver_state = ConversationState(MemoryStorage()) - - # Create a DialogState property, DialogSet and register the WaterfallDialog. - dialog_state = conver_state.create_property("dialogState") - - dialogs = DialogSet(dialog_state) - - # Create and add number prompt to DialogSet. - number_prompt = NumberPrompt( - "NumberPrompt", validator=None, default_locale=Culture.English - ) - dialogs.add(number_prompt) - - async def exec_test(turn_context: TurnContext) -> None: - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Enter a number.") - ) - await dialog_context.prompt("NumberPrompt", options) - - elif results.status == DialogTurnStatus.Complete: - number_result = float(results.result) - await turn_context.send_activity( - MessageFactory.text(f"Bot received the number '{number_result}'.") - ) - - await conver_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("Enter a number.") - step3 = await step2.send("3.14") - await step3.assert_reply("Bot received the number '3.14'.") - - @pytest.mark.asyncio - async def test_number_prompt_uses_locale_specified_in_activity(self): - conver_state = ConversationState(MemoryStorage()) - dialog_state = conver_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - number_prompt = NumberPrompt("NumberPrompt", None, None) - dialogs.add(number_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Enter a number.") - ) - await dialog_context.prompt("NumberPrompt", options) - - elif results.status == DialogTurnStatus.Complete: - number_result = float(results.result) - assert 3.14 == number_result - - await conver_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("Enter a number.") - await step2.send( - Activity(type=ActivityTypes.message, text="3,14", locale=Culture.Spanish) - ) - - @pytest.mark.asyncio - async def test_number_prompt_defaults_to_en_us_culture(self): - conver_state = ConversationState(MemoryStorage()) - dialog_state = conver_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - number_prompt = NumberPrompt("NumberPrompt") - dialogs.add(number_prompt) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Enter a number.") - ) - await dialog_context.prompt("NumberPrompt", options) - - elif results.status == DialogTurnStatus.Complete: - number_result = float(results.result) - await turn_context.send_activity( - MessageFactory.text(f"Bot received the number '{number_result}'.") - ) - - await conver_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1 = await adapter.send("hello") - step2 = await step1.assert_reply("Enter a number.") - step3 = await step2.send("3.14") - await step3.assert_reply("Bot received the number '3.14'.") diff --git a/dev/hosting_dialogs/test_oauth_prompt.py b/dev/hosting_dialogs/test_oauth_prompt.py deleted file mode 100644 index 331abe6a5..000000000 --- a/dev/hosting_dialogs/test_oauth_prompt.py +++ /dev/null @@ -1,384 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest -from microsoft_agents.hosting.dialogs.prompts import OAuthPromptSettings -from microsoft_agents.activity import ( - Activity, - ActivityTypes, - ChannelAccount, - ConversationAccount, - InputHints, - SignInConstants, - TokenResponse, -) -from microsoft_agents.hosting.core import ( - CardFactory, - ConversationState, - MemoryStorage, - TurnContext, -) -from microsoft_agents.hosting.dialogs import DialogSet, DialogTurnStatus -from microsoft_agents.hosting.dialogs.prompts import OAuthPrompt, PromptOptions -from tests.hosting_dialogs.helpers import DialogTestAdapter - - -def create_reply(activity): - return Activity( - type=ActivityTypes.message, - from_property=ChannelAccount( - id=activity.recipient.id, name=activity.recipient.name - ), - recipient=ChannelAccount( - id=activity.from_property.id, name=activity.from_property.name - ), - reply_to_id=activity.id, - service_url=activity.service_url, - channel_id=activity.channel_id, - conversation=ConversationAccount( - is_group=activity.conversation.is_group, - id=activity.conversation.id, - name=activity.conversation.name, - ), - ) - - -class TestOAuthPrompt: - @pytest.mark.skip( - reason="tokens/response event path not supported in new OAuthPrompt internals" - ) - @pytest.mark.asyncio - async def test_should_call_oauth_prompt(self): - connection_name = "myConnection" - token = "abc123" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - dialogs.add( - OAuthPrompt( - "prompt", OAuthPromptSettings(connection_name, "Login", None, 300000) - ) - ) - - async def callback_handler(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dialog_context.prompt("prompt", PromptOptions()) - elif results.status == DialogTurnStatus.Complete: - if results.result.token: - await turn_context.send_activity("Logged in.") - else: - await turn_context.send_activity("Failed") - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(callback_handler) - - async def inspector(activity: Activity, description: str = None): - assert len(activity.attachments) == 1 - assert ( - activity.attachments[0].content_type - == CardFactory.content_types.oauth_card - ) - - adapter.add_user_token( - connection_name, activity.channel_id, activity.recipient.id, token - ) - - event_activity = create_reply(activity) - event_activity.type = ActivityTypes.event - event_activity.from_property, event_activity.recipient = ( - event_activity.recipient, - event_activity.from_property, - ) - event_activity.name = "tokens/response" - event_activity.value = TokenResponse( - connection_name=connection_name, token=token - ) - - context = adapter.create_turn_context(event_activity) - await callback_handler(context) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply(inspector) - await step2.assert_reply("Logged in.") - - @pytest.mark.asyncio - async def test_should_call_oauth_prompt_with_code(self): - connection_name = "myConnection" - token = "abc123" - magic_code = "888999" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialog_state") - dialogs = DialogSet(dialog_state) - dialogs.add( - OAuthPrompt( - "prompt", OAuthPromptSettings(connection_name, "Login", None, 300000) - ) - ) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dialog_context.prompt("prompt", PromptOptions()) - elif results.status == DialogTurnStatus.Complete: - if results.result.token: - await turn_context.send_activity("Logged in.") - else: - await turn_context.send_activity("Failed") - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - def inspector(activity: Activity, description: str = None): - assert len(activity.attachments) == 1 - assert ( - activity.attachments[0].content_type - == CardFactory.content_types.oauth_card - ) - adapter.add_user_token( - connection_name, - activity.channel_id, - activity.recipient.id, - token, - magic_code, - ) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply(inspector) - step3 = await step2.send(magic_code) - await step3.assert_reply("Logged in.") - - @pytest.mark.asyncio - async def test_oauth_prompt_doesnt_detect_code_in_begin_dialog(self): - connection_name = "myConnection" - token = "abc123" - magic_code = "888999" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialog_state") - dialogs = DialogSet(dialog_state) - dialogs.add( - OAuthPrompt( - "prompt", OAuthPromptSettings(connection_name, "Login", None, 300000) - ) - ) - - async def exec_test(turn_context: TurnContext): - adapter.add_user_token( - connection_name, - turn_context.activity.channel_id, - turn_context.activity.from_property.id, - token, - magic_code, - ) - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - token_result = await dialog_context.prompt("prompt", PromptOptions()) - if isinstance(token_result.result, TokenResponse): - assert False, "Should not have returned token during begin" - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - def inspector(activity: Activity, description: str = None): - assert len(activity.attachments) == 1 - assert ( - activity.attachments[0].content_type - == CardFactory.content_types.oauth_card - ) - - step1 = await adapter.send(magic_code) - await step1.assert_reply(inspector) - - @pytest.mark.asyncio - async def test_should_add_accepting_input_hint_oauth_prompt(self): - connection_name = "myConnection" - called = False - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - dialogs.add( - OAuthPrompt( - "prompt", OAuthPromptSettings(connection_name, "Login", None, 300000) - ) - ) - - async def callback_handler(turn_context: TurnContext): - nonlocal called - dialog_context = await dialogs.create_context(turn_context) - await dialog_context.continue_dialog() - await dialog_context.prompt( - "prompt", - PromptOptions( - prompt=Activity(type=ActivityTypes.message), - retry_prompt=Activity(type=ActivityTypes.message), - ), - ) - assert ( - dialog_context.active_dialog.state["options"].prompt.input_hint - == InputHints.accepting_input - ) - assert ( - dialog_context.active_dialog.state["options"].retry_prompt.input_hint - == InputHints.accepting_input - ) - await convo_state.save(turn_context) - called = True - - adapter = DialogTestAdapter(callback_handler) - await adapter.send("Hello") - assert called - - @pytest.mark.asyncio - async def test_should_end_oauth_prompt_on_invalid_message_when_end_on_invalid_message( - self, - ): - connection_name = "myConnection" - token = "abc123" - magic_code = "888999" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialog_state") - dialogs = DialogSet(dialog_state) - dialogs.add( - OAuthPrompt( - "prompt", - OAuthPromptSettings(connection_name, "Login", None, 300000, None, True), - ) - ) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dialog_context.prompt("prompt", PromptOptions()) - elif results.status == DialogTurnStatus.Complete: - if results.result and results.result.token: - await turn_context.send_activity("Failed") - else: - await turn_context.send_activity("Ended") - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - def inspector(activity: Activity, description: str = None): - assert len(activity.attachments) == 1 - assert ( - activity.attachments[0].content_type - == CardFactory.content_types.oauth_card - ) - adapter.add_user_token( - connection_name, - activity.channel_id, - activity.recipient.id, - token, - magic_code, - ) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply(inspector) - step3 = await step2.send("test invalid message") - await step3.assert_reply("Ended") - - @pytest.mark.asyncio - async def test_should_timeout_oauth_prompt_with_message_activity(self): - activity = Activity(type=ActivityTypes.message, text="any") - await self._run_timeout_test(activity) - - @pytest.mark.asyncio - async def test_should_timeout_oauth_prompt_with_token_response_event_activity(self): - activity = Activity( - type=ActivityTypes.event, name=SignInConstants.token_response_event_name - ) - await self._run_timeout_test(activity) - - @pytest.mark.asyncio - async def test_should_timeout_oauth_prompt_with_verify_state_operation_activity( - self, - ): - activity = Activity( - type=ActivityTypes.invoke, name=SignInConstants.verify_state_operation_name - ) - await self._run_timeout_test(activity) - - @pytest.mark.asyncio - async def test_should_not_timeout_oauth_prompt_with_custom_event_activity(self): - activity = Activity(type=ActivityTypes.event, name="custom event name") - await self._run_timeout_test(activity, False, "Ended", "Failed") - - async def _run_timeout_test( - self, - activity: Activity, - should_succeed: bool = True, - token_response: str = "Failed", - no_token_response: str = "Ended", - ): - connection_name = "myConnection" - token = "abc123" - magic_code = "888999" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialog_state") - dialogs = DialogSet(dialog_state) - dialogs.add( - OAuthPrompt( - "prompt", - OAuthPromptSettings(connection_name, "Login", None, 1), - ) - ) - - async def exec_test(turn_context: TurnContext): - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dialog_context.prompt("prompt", PromptOptions()) - elif results.status == DialogTurnStatus.Complete or ( - results.status == DialogTurnStatus.Waiting and not should_succeed - ): - if results.result and results.result.token: - await turn_context.send_activity(token_response) - else: - await turn_context.send_activity(no_token_response) - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - def inspector(activity_: Activity, description: str = None): - assert len(activity_.attachments) == 1 - assert ( - activity_.attachments[0].content_type - == CardFactory.content_types.oauth_card - ) - adapter.add_user_token( - connection_name, - activity_.channel_id, - activity_.recipient.id, - token, - magic_code, - ) - - step1 = await adapter.send("Hello") - step2 = await step1.assert_reply(inspector) - step3 = await step2.send(activity) - await step3.assert_reply(no_token_response) - - -class TestOAuthPromptSettings: - def test_oauth_app_credentials_stored_correctly(self): - """Constructor param oauth_app_credentials must be accessible as oauth_app_credentials.""" - sentinel = object() - settings = OAuthPromptSettings("conn", "title", oauth_app_credentials=sentinel) - assert settings.oauth_app_credentials is sentinel - - def test_oath_typo_alias_reads_same_value(self): - """oath_app_credentials (typo alias) must return the same value as oauth_app_credentials.""" - sentinel = object() - settings = OAuthPromptSettings("conn", "title", oauth_app_credentials=sentinel) - assert settings.oath_app_credentials is sentinel - - def test_oath_typo_alias_setter_writes_to_canonical(self): - """Writing via the typo alias updates oauth_app_credentials.""" - sentinel = object() - settings = OAuthPromptSettings("conn", "title") - settings.oath_app_credentials = sentinel - assert settings.oauth_app_credentials is sentinel diff --git a/dev/hosting_dialogs/test_object_path.py b/dev/hosting_dialogs/test_object_path.py deleted file mode 100644 index db7ff202d..000000000 --- a/dev/hosting_dialogs/test_object_path.py +++ /dev/null @@ -1,209 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest -from microsoft_agents.hosting.dialogs import ObjectPath - - -class Location: - def __init__(self, lat: float = None, long: float = None): - self.lat = lat - self.long = long - - -class Options: - def __init__( - self, - first_name: str = None, - last_name: str = None, - age: int = None, - boolean: bool = None, - dictionary: dict = None, - location: Location = None, - ): - self.first_name = first_name - self.last_name = last_name - self.age = age - self.boolean = boolean - self.dictionary = dictionary - self.location = location - - -class TestObjectPath: - def test_typed_only_default(self): - default_options = Options( - last_name="Smith", - first_name="Fred", - age=22, - location=Location(lat=1.2312312, long=3.234234), - ) - overlay = Options() - result = ObjectPath.assign(default_options, overlay) - assert result.last_name == default_options.last_name - assert result.first_name == default_options.first_name - assert result.age == default_options.age - assert result.boolean == default_options.boolean - assert result.location.lat == default_options.location.lat - assert result.location.long == default_options.location.long - - def test_typed_only_overlay(self): - default_options = Options() - overlay = Options( - last_name="Smith", - first_name="Fred", - age=22, - location=Location(lat=1.2312312, long=3.234234), - ) - result = ObjectPath.assign(default_options, overlay) - assert result.last_name == overlay.last_name - assert result.first_name == overlay.first_name - assert result.age == overlay.age - assert result.boolean == overlay.boolean - assert result.location.lat == overlay.location.lat - assert result.location.long == overlay.location.long - - def test_typed_full_overlay(self): - default_options = Options( - last_name="Smith", - first_name="Fred", - age=22, - location=Location(lat=1.2312312, long=3.234234), - dictionary={"one": 1, "two": 2}, - ) - overlay = Options( - last_name="Grant", - first_name="Eddit", - age=32, - location=Location(lat=2.2312312, long=2.234234), - dictionary={"one": 99, "three": 3}, - ) - result = ObjectPath.assign(default_options, overlay) - assert result.last_name == overlay.last_name - assert result.first_name == overlay.first_name - assert result.age == overlay.age - assert result.boolean == overlay.boolean - assert result.location.lat == overlay.location.lat - assert result.location.long == overlay.location.long - assert "one" in result.dictionary - assert result.dictionary["one"] == 99 - assert "two" in result.dictionary - assert "three" in result.dictionary - - def test_typed_partial_overlay(self): - default_options = Options( - last_name="Smith", - first_name="Fred", - age=22, - location=Location(lat=1.2312312, long=3.234234), - ) - overlay = Options(last_name="Grant") - result = ObjectPath.assign(default_options, overlay) - assert result.last_name == overlay.last_name - assert result.first_name == default_options.first_name - assert result.age == default_options.age - assert result.boolean == default_options.boolean - assert result.location.lat == default_options.location.lat - assert result.location.long == default_options.location.long - - def test_typed_no_target(self): - overlay = Options( - last_name="Smith", - first_name="Fred", - age=22, - location=Location(lat=1.2312312, long=3.234234), - ) - result = ObjectPath.assign(None, overlay) - assert result.last_name == overlay.last_name - assert result.first_name == overlay.first_name - assert result.age == overlay.age - assert result.boolean == overlay.boolean - assert result.location.lat == overlay.location.lat - assert result.location.long == overlay.location.long - - def test_typed_no_overlay(self): - default_options = Options( - last_name="Smith", - first_name="Fred", - age=22, - location=Location(lat=1.2312312, long=3.234234), - ) - result = ObjectPath.assign(default_options, None) - assert result.last_name == default_options.last_name - assert result.first_name == default_options.first_name - assert result.age == default_options.age - assert result.boolean == default_options.boolean - assert result.location.lat == default_options.location.lat - assert result.location.long == default_options.location.long - - def test_no_target_or_overlay(self): - result = ObjectPath.assign(None, None, Options) - assert result - - def test_dict_partial_overlay(self): - default_options = { - "last_name": "Smith", - "first_name": "Fred", - "age": 22, - "location": Location(lat=1.2312312, long=3.234234), - } - overlay = {"last_name": "Grant"} - result = ObjectPath.assign(default_options, overlay) - assert result["last_name"] == overlay["last_name"] - assert result["first_name"] == default_options["first_name"] - assert result["age"] == default_options["age"] - assert result["location"].lat == default_options["location"].lat - assert result["location"].long == default_options["location"].long - - def test_dict_to_typed_overlay(self): - default_options = Options( - last_name="Smith", - first_name="Fred", - age=22, - location=Location(lat=1.2312312, long=3.234234), - ) - overlay = {"last_name": "Grant"} - result = ObjectPath.assign(default_options, overlay) - assert result.last_name == overlay["last_name"] - assert result.first_name == default_options.first_name - assert result.age == default_options.age - assert result.boolean == default_options.boolean - assert result.location.lat == default_options.location.lat - assert result.location.long == default_options.location.long - - def test_set_value(self): - test = {} - ObjectPath.set_path_value(test, "x.y.z", 15) - ObjectPath.set_path_value(test, "x.p", "hello") - ObjectPath.set_path_value(test, "foo", {"Bar": 15, "Blat": "yo"}) - ObjectPath.set_path_value(test, "x.a[1]", "yabba") - ObjectPath.set_path_value(test, "x.a[0]", "dabba") - ObjectPath.set_path_value(test, "null", None) - - assert ObjectPath.get_path_value(test, "x.y.z") == 15 - assert ObjectPath.get_path_value(test, "x.p") == "hello" - assert ObjectPath.get_path_value(test, "foo.bar") == 15 - - assert not ObjectPath.try_get_path_value(test, "foo.Blatxxx") - assert ObjectPath.try_get_path_value(test, "x.a[1]") == "yabba" - assert ObjectPath.try_get_path_value(test, "x.a[0]") == "dabba" - - assert not ObjectPath.try_get_path_value(test, "null") - - def test_remove_path_value(self): - test = {} - ObjectPath.set_path_value(test, "x.y.z", 15) - ObjectPath.set_path_value(test, "x.p", "hello") - ObjectPath.set_path_value(test, "foo", {"Bar": 15, "Blat": "yo"}) - ObjectPath.set_path_value(test, "x.a[1]", "yabba") - ObjectPath.set_path_value(test, "x.a[0]", "dabba") - - ObjectPath.remove_path_value(test, "x.y.z") - with pytest.raises(KeyError): - ObjectPath.get_path_value(test, "x.y.z") - - assert ObjectPath.get_path_value(test, "x.y.z", 99) == 99 - - ObjectPath.remove_path_value(test, "x.a[1]") - assert not ObjectPath.try_get_path_value(test, "x.a[1]") - - assert ObjectPath.try_get_path_value(test, "x.a[0]") == "dabba" diff --git a/dev/hosting_dialogs/test_prompt_culture_models.py b/dev/hosting_dialogs/test_prompt_culture_models.py deleted file mode 100644 index 138d3a7e2..000000000 --- a/dev/hosting_dialogs/test_prompt_culture_models.py +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest -from microsoft_agents.hosting.dialogs.prompts.prompt_culture_models import ( - PromptCultureModels, -) - -SUPPORTED_CULTURES = [ - PromptCultureModels.Bulgarian, - PromptCultureModels.Chinese, - PromptCultureModels.Dutch, - PromptCultureModels.English, - PromptCultureModels.French, - PromptCultureModels.German, - PromptCultureModels.Hindi, - PromptCultureModels.Italian, - PromptCultureModels.Japanese, - PromptCultureModels.Korean, - PromptCultureModels.Portuguese, - PromptCultureModels.Spanish, - PromptCultureModels.Swedish, - PromptCultureModels.Turkish, -] - - -def _locale_variations(culture): - """Generate (input_variation, expected_locale) tuples for a culture.""" - locale = culture.locale # e.g. "en-us" - parts = locale.split("-") - prefix = parts[0] - suffix = parts[1] if len(parts) > 1 else "" - return [ - (locale, locale), # exact: "en-us" - (f"{prefix}-{suffix.upper()}", locale), # cap ending: "en-US" - (f"{prefix.capitalize()}-{suffix.capitalize()}", locale), # title: "En-Us" - (prefix.upper(), locale), # all-caps two-letter: "EN" - (prefix, locale), # lowercase two-letter: "en" - ] - - -LOCALE_VARIATIONS = [ - variation - for culture in SUPPORTED_CULTURES - for variation in _locale_variations(culture) -] - - -@pytest.mark.parametrize("locale_variation,expected", LOCALE_VARIATIONS) -def test_map_to_nearest_language(locale_variation, expected): - result = PromptCultureModels.map_to_nearest_language(locale_variation) - assert result == expected - - -def test_null_locale_does_not_raise(): - result = PromptCultureModels.map_to_nearest_language(None) - assert result is None - - -def test_get_supported_cultures_returns_all(): - expected_locales = {c.locale for c in SUPPORTED_CULTURES} - actual_locales = {c.locale for c in PromptCultureModels.get_supported_cultures()} - assert expected_locales == actual_locales - - -def test_supported_cultures_have_required_fields(): - for culture in PromptCultureModels.get_supported_cultures(): - assert culture.locale, f"Culture missing locale" - assert culture.separator, f"Culture {culture.locale} missing separator" - assert culture.inline_or, f"Culture {culture.locale} missing inline_or" - assert ( - culture.yes_in_language - ), f"Culture {culture.locale} missing yes_in_language" - assert ( - culture.no_in_language - ), f"Culture {culture.locale} missing no_in_language" diff --git a/dev/hosting_dialogs/test_prompt_validator_context.py b/dev/hosting_dialogs/test_prompt_validator_context.py deleted file mode 100644 index f8536da4c..000000000 --- a/dev/hosting_dialogs/test_prompt_validator_context.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest - -from microsoft_agents.hosting.dialogs import DialogSet, DialogTurnStatus -from microsoft_agents.hosting.core import MemoryStorage, ConversationState -from microsoft_agents.hosting.dialogs.prompts import ( - TextPrompt, - PromptOptions, - PromptValidatorContext, -) -from microsoft_agents.activity import Activity, ActivityTypes -from tests.hosting_dialogs.helpers import DialogTestAdapter - - -class TestPromptValidatorContext: - - @pytest.mark.asyncio - async def test_prompt_validator_context_end(self): - storage = MemoryStorage() - conv = ConversationState(storage) - accessor = conv.create_property("dialogstate") - dialog_set = DialogSet(accessor) - assert dialog_set is not None - - def test_prompt_validator_context_retry_end(self): - storage = MemoryStorage() - conv = ConversationState(storage) - accessor = conv.create_property("dialogstate") - dialog_set = DialogSet(accessor) - assert dialog_set is not None - - @pytest.mark.asyncio - async def test_attempt_count_starts_at_one_on_first_validation(self): - """attempt_count is 1 on the first validator call and increments on each - subsequent user reply (mirrors ActivityPrompt behaviour).""" - observed_attempt_counts = [] - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - async def validator(pc: PromptValidatorContext) -> bool: - observed_attempt_counts.append(pc.attempt_count) - return bool(pc.recognized.value) - - ds.add(TextPrompt("TextPrompt", validator)) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Enter text."), - retry_prompt=Activity(type=ActivityTypes.message, text="Again."), - ) - await dc.prompt("TextPrompt", options) - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - flow = await adapter.send("hello") - await flow.assert_reply("Enter text.") - await adapter.send("something") - - assert observed_attempt_counts == [1] - - @pytest.mark.asyncio - async def test_attempt_count_increments_across_retries(self): - """attempt_count increments with each user reply, so retry #1 = 2, retry #2 = 3, etc.""" - observed_attempt_counts = [] - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - async def validator(pc: PromptValidatorContext) -> bool: - observed_attempt_counts.append(pc.attempt_count) - # Only accept the literal word "yes" - return pc.recognized.succeeded and pc.recognized.value == "yes" - - ds.add(TextPrompt("TextPrompt", validator)) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Say yes."), - retry_prompt=Activity( - type=ActivityTypes.message, text="Please say yes." - ), - ) - await dc.prompt("TextPrompt", options) - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - flow = await adapter.send("start") - await flow.assert_reply("Say yes.") - await adapter.send("no") # attempt 1 — rejected - await adapter.send("nope") # attempt 2 — rejected - await adapter.send("yes") # attempt 3 — accepted - - assert observed_attempt_counts == [1, 2, 3] diff --git a/dev/hosting_dialogs/test_replace_dialog.py b/dev/hosting_dialogs/test_replace_dialog.py deleted file mode 100644 index 86ebb8ec7..000000000 --- a/dev/hosting_dialogs/test_replace_dialog.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest - -from microsoft_agents.activity import Activity, ActivityTypes -from microsoft_agents.hosting.core import ConversationState, MemoryStorage, TurnContext -from microsoft_agents.hosting.dialogs import ( - ComponentDialog, - DialogSet, - DialogTurnStatus, - WaterfallDialog, -) -from microsoft_agents.hosting.dialogs.models.dialog_instance import DialogInstance -from microsoft_agents.hosting.dialogs.models.dialog_reason import DialogReason -from microsoft_agents.hosting.dialogs.prompts import TextPrompt, PromptOptions -from tests.hosting_dialogs.helpers import DialogTestAdapter - - -def _text_prompt_options(text: str) -> PromptOptions: - return PromptOptions(prompt=Activity(type=ActivityTypes.message, text=text)) - - -class _WaterfallWithEndDialog(WaterfallDialog): - """WaterfallDialog that announces itself when it ends.""" - - async def end_dialog( - self, context: TurnContext, instance: DialogInstance, reason: DialogReason - ): - await context.send_activity("*** WaterfallDialog End ***") - await super().end_dialog(context, instance, reason) - - -class _SecondDialog(ComponentDialog): - def __init__(self): - super().__init__("SecondDialog") - - async def action_four(step): - return await step.prompt("TextPrompt", _text_prompt_options("prompt four")) - - async def action_five(step): - return await step.prompt("TextPrompt", _text_prompt_options("prompt five")) - - async def last_action(step): - return await step.end_dialog() - - self.add_dialog(TextPrompt("TextPrompt")) - self.add_dialog( - WaterfallDialog("WaterfallDialog", [action_four, action_five, last_action]) - ) - self.initial_dialog_id = "WaterfallDialog" - - -class _FirstDialog(ComponentDialog): - def __init__(self): - super().__init__("FirstDialog") - - async def action_one(step): - return await step.prompt("TextPrompt", _text_prompt_options("prompt one")) - - async def action_two(step): - return await step.prompt("TextPrompt", _text_prompt_options("prompt two")) - - async def replace_action(step): - if step.result == "replace": - return await step.replace_dialog("SecondDialog") - return await step.next(None) - - async def action_three(step): - return await step.prompt("TextPrompt", _text_prompt_options("prompt three")) - - async def last_action(step): - return await step.end_dialog() - - self.add_dialog(TextPrompt("TextPrompt")) - self.add_dialog(_SecondDialog()) - self.add_dialog( - _WaterfallWithEndDialog( - "WaterfallWithEndDialog", - [action_one, action_two, replace_action, action_three, last_action], - ) - ) - self.initial_dialog_id = "WaterfallWithEndDialog" - - -class TestReplaceDialog: - @pytest.mark.asyncio - async def test_replace_dialog_no_branch(self): - """Dialog flows through all three prompts without branching.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - ds.add(_FirstDialog()) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dc.begin_dialog("FirstDialog") - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - flow = await adapter.send("hello") - flow = await flow.assert_reply("prompt one") - flow = await flow.send("hello") - flow = await flow.assert_reply("prompt two") - flow = await flow.send("hello") - flow = await flow.assert_reply("prompt three") - flow = await flow.send("hello") - await flow.assert_reply("*** WaterfallDialog End ***") - - @pytest.mark.asyncio - async def test_replace_dialog_branch(self): - """Sending 'replace' causes replace_dialog to switch to SecondDialog.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - ds.add(_FirstDialog()) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dc.begin_dialog("FirstDialog") - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - flow = await adapter.send("hello") - flow = await flow.assert_reply("prompt one") - flow = await flow.send("hello") - flow = await flow.assert_reply("prompt two") - flow = await flow.send("replace") - flow = await flow.assert_reply("*** WaterfallDialog End ***") - flow = await flow.assert_reply("prompt four") - flow = await flow.send("hello") - await flow.assert_reply("prompt five") diff --git a/dev/hosting_dialogs/test_text_prompt.py b/dev/hosting_dialogs/test_text_prompt.py deleted file mode 100644 index 6103d9099..000000000 --- a/dev/hosting_dialogs/test_text_prompt.py +++ /dev/null @@ -1,195 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest - -from microsoft_agents.activity import Activity, ActivityTypes -from microsoft_agents.hosting.core import ConversationState, MemoryStorage -from microsoft_agents.hosting.dialogs import DialogSet, DialogTurnStatus -from microsoft_agents.hosting.dialogs.prompts import ( - TextPrompt, - PromptOptions, - PromptValidatorContext, -) -from tests.hosting_dialogs.helpers import DialogTestAdapter - - -class TestTextPrompt: - def test_empty_id_raises(self): - with pytest.raises((TypeError, Exception)): - TextPrompt("") - - def test_null_id_raises(self): - with pytest.raises((TypeError, Exception)): - TextPrompt(None) - - @pytest.mark.asyncio - async def test_basic_text_prompt(self): - """TextPrompt echoes user input back after the prompt.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - ds.add(TextPrompt("TextPrompt")) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Enter some text.") - ) - await dc.prompt("TextPrompt", options) - elif results.status == DialogTurnStatus.Complete: - await tc.send_activity(f"Bot received: {results.result}") - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - flow = await adapter.send("hello") - flow = await flow.assert_reply("Enter some text.") - flow = await flow.send("some text") - await flow.assert_reply("Bot received: some text") - - @pytest.mark.asyncio - async def test_text_prompt_with_validator(self): - """A validator can reject short inputs and trigger the retry prompt.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - async def validator(pc: PromptValidatorContext) -> bool: - return pc.recognized.value is not None and len(pc.recognized.value) > 3 - - ds.add(TextPrompt("TextPrompt", validator)) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Enter some text." - ), - retry_prompt=Activity( - type=ActivityTypes.message, text="That's not long enough." - ), - ) - await dc.prompt("TextPrompt", options) - elif results.status == DialogTurnStatus.Complete: - await tc.send_activity(f"Bot received: {results.result}") - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - flow = await adapter.send("hello") - flow = await flow.assert_reply("Enter some text.") - flow = await flow.send("hi") # Too short — validator rejects - flow = await flow.assert_reply("That's not long enough.") - flow = await flow.send("hello world") # Long enough - await flow.assert_reply("Bot received: hello world") - - @pytest.mark.asyncio - async def test_text_prompt_retry_on_failed_validation(self): - """Without a retry_prompt the original prompt is re-sent on failure.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - async def validator(pc: PromptValidatorContext) -> bool: - return pc.recognized.value is not None and pc.recognized.value != "bad" - - ds.add(TextPrompt("TextPrompt", validator)) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity( - type=ActivityTypes.message, text="Enter something." - ), - ) - await dc.prompt("TextPrompt", options) - elif results.status == DialogTurnStatus.Complete: - await tc.send_activity(f"Got: {results.result}") - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - flow = await adapter.send("hello") - flow = await flow.assert_reply("Enter something.") - flow = await flow.send("bad") # Rejected by validator - flow = await flow.assert_reply("Enter something.") # Re-prompted - flow = await flow.send("good") - await flow.assert_reply("Got: good") - - @pytest.mark.asyncio - async def test_text_prompt_non_message_activity_does_not_succeed(self): - """A non-message activity (e.g. event) causes recognition to fail. - - The base Prompt.continue_dialog returns end_of_turn immediately for - non-message activities without calling the recognizer or validator. - The dialog remains active (Waiting) and the user is not re-prompted. - """ - from microsoft_agents.activity import ActivityTypes - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - ds.add(TextPrompt("TextPrompt")) - - completed = [] - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Enter something.") - ) - await dc.prompt("TextPrompt", options) - elif results.status == DialogTurnStatus.Complete: - completed.append(results.result) - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - # Start the prompt - flow = await adapter.send("hello") - await flow.assert_reply("Enter something.") - - # Send a non-message event — dialog must NOT complete - event_activity = Activity(type=ActivityTypes.event, name="custom") - await adapter.process_activity_async(event_activity, exec) - assert len(completed) == 0, "Prompt must not complete on non-message activity" - - @pytest.mark.asyncio - async def test_text_prompt_with_custom_message_validator(self): - """Validator can send its own message and return False.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - async def validator(pc: PromptValidatorContext) -> bool: - if pc.recognized.value and len(pc.recognized.value) > 5: - return True - await pc.context.send_activity("Please enter more than 5 characters.") - return False - - ds.add(TextPrompt("TextPrompt", validator)) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - options = PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Enter text."), - ) - await dc.prompt("TextPrompt", options) - elif results.status == DialogTurnStatus.Complete: - await tc.send_activity(f"Done: {results.result}") - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - flow = await adapter.send("hello") - flow = await flow.assert_reply("Enter text.") - flow = await flow.send("hi") - flow = await flow.assert_reply("Please enter more than 5 characters.") - flow = await flow.send("hello world") - await flow.assert_reply("Done: hello world") diff --git a/dev/hosting_dialogs/test_waterfall.py b/dev/hosting_dialogs/test_waterfall.py deleted file mode 100644 index c07d1b5a3..000000000 --- a/dev/hosting_dialogs/test_waterfall.py +++ /dev/null @@ -1,314 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest -from recognizers_text import Culture - -from microsoft_agents.activity import Activity, ActivityTypes -from microsoft_agents.hosting.core import ConversationState, MemoryStorage, TurnContext -from microsoft_agents.hosting.dialogs import ( - ComponentDialog, - Dialog, - DialogSet, - WaterfallDialog, - WaterfallStepContext, - DialogTurnResult, - DialogTurnStatus, -) -from microsoft_agents.hosting.dialogs.prompts import ( - NumberPrompt, - DateTimePrompt, - PromptOptions, -) -from tests.hosting_dialogs.helpers import DialogTestAdapter - - -class TestWaterfallDialog: - def test_waterfall_none_name(self): - with pytest.raises((TypeError, Exception)): - WaterfallDialog(None) - - def test_waterfall_add_none_step(self): - waterfall = WaterfallDialog("test") - with pytest.raises((TypeError, Exception)): - waterfall.add_step(None) - - def test_waterfall_with_set_instead_of_array(self): - with pytest.raises((TypeError, Exception)): - WaterfallDialog("a", {1, 2}) - - @pytest.mark.asyncio - async def test_execute_sequence_waterfall_steps(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - async def step1(step: WaterfallStepContext) -> DialogTurnResult: - await step.context.send_activity("bot responding.") - return Dialog.end_of_turn - - async def step2(step: WaterfallStepContext) -> DialogTurnResult: - return await step.end_dialog("ending WaterfallDialog.") - - my_dialog = WaterfallDialog("test", [step1, step2]) - dialogs.add(my_dialog) - - async def exec_test(turn_context: TurnContext) -> None: - dialog_context = await dialogs.create_context(turn_context) - results = await dialog_context.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dialog_context.begin_dialog("test") - else: - if results.status == DialogTurnStatus.Complete: - await turn_context.send_activity(results.result) - await convo_state.save(turn_context) - - adapter = DialogTestAdapter(exec_test) - - step1_flow = await adapter.send("begin") - step2_flow = await step1_flow.assert_reply("bot responding.") - step3_flow = await step2_flow.send("continue") - await step3_flow.assert_reply("ending WaterfallDialog.") - - def test_waterfall_callback(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - async def step_callback1(step: WaterfallStepContext) -> DialogTurnResult: - await step.context.send_activity("step1") - - async def step_callback2(step: WaterfallStepContext) -> DialogTurnResult: - await step.context.send_activity("step2") - - async def step_callback3(step: WaterfallStepContext) -> DialogTurnResult: - await step.context.send_activity("step3") - - steps = [step_callback1, step_callback2, step_callback3] - dialogs.add(WaterfallDialog("test", steps)) - assert dialogs is not None - assert len(dialogs._dialogs) == 1 # pylint: disable=protected-access - - def test_waterfall_with_class(self): - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - - class MyWaterfallDialog(WaterfallDialog): - def __init__(self, dialog_id: str): - async def step1(step: WaterfallStepContext) -> DialogTurnResult: - await step.context.send_activity("step1") - return Dialog.end_of_turn - - async def step2(step: WaterfallStepContext) -> DialogTurnResult: - await step.context.send_activity("step2") - return Dialog.end_of_turn - - super().__init__(dialog_id, [step1, step2]) - - dialogs.add(MyWaterfallDialog("test")) - assert dialogs is not None - assert len(dialogs._dialogs) == 1 # pylint: disable=protected-access - - @pytest.mark.asyncio - async def test_waterfall_step_parent_is_waterfall_parent(self): - """WaterfallStepContext.parent should be the ComponentDialog containing the waterfall.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - - result_holder = {} - - class WaterfallParentDialog(ComponentDialog): - def __init__(self): - super().__init__("waterfall-parent-test-dialog") - - async def step1(step: WaterfallStepContext) -> DialogTurnResult: - # Parent context should have the component dialog as its active dialog - parent_id = ( - step.parent.active_dialog.id - if step.parent and step.parent.active_dialog - else None - ) - result_holder["parent_id"] = parent_id - await step.context.send_activity("verified") - return Dialog.end_of_turn - - self.add_dialog(WaterfallDialog("test", [step1])) - self.initial_dialog_id = "test" - - ds = DialogSet(dialog_state) - ds.add(WaterfallParentDialog()) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dc.begin_dialog("waterfall-parent-test-dialog") - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - flow = await adapter.send("hello") - await flow.assert_reply("verified") - - assert result_holder.get("parent_id") == "waterfall-parent-test-dialog" - - @pytest.mark.asyncio - async def test_waterfall_prompt(self): - """Waterfall with NumberPrompt: invalid inputs trigger retry, valid inputs advance steps.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - - async def step1(step: WaterfallStepContext) -> DialogTurnResult: - await step.context.send_activity("step1") - return await step.prompt( - "number", - PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Enter a number."), - retry_prompt=Activity( - type=ActivityTypes.message, text="It must be a number" - ), - ), - ) - - async def step2(step: WaterfallStepContext) -> DialogTurnResult: - await step.context.send_activity(f"Thanks for '{int(step.result)}'") - await step.context.send_activity("step2") - return await step.prompt( - "number", - PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Enter a number."), - retry_prompt=Activity( - type=ActivityTypes.message, text="It must be a number" - ), - ), - ) - - async def step3(step: WaterfallStepContext) -> DialogTurnResult: - await step.context.send_activity(f"Thanks for '{int(step.result)}'") - await step.context.send_activity("step3") - return await step.end_dialog() - - ds = DialogSet(dialog_state) - ds.add(WaterfallDialog("test-waterfall", [step1, step2, step3])) - ds.add(NumberPrompt("number", default_locale=Culture.English)) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dc.begin_dialog("test-waterfall") - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - flow = await adapter.send("hello") - flow = await flow.assert_reply("step1") - flow = await flow.assert_reply("Enter a number.") - flow = await flow.send("hello again") - flow = await flow.assert_reply("It must be a number") - flow = await flow.send("42") - flow = await flow.assert_reply("Thanks for '42'") - flow = await flow.assert_reply("step2") - flow = await flow.assert_reply("Enter a number.") - flow = await flow.send("apple") - flow = await flow.assert_reply("It must be a number") - flow = await flow.send("orange") - flow = await flow.assert_reply("It must be a number") - flow = await flow.send("64") - flow = await flow.assert_reply("Thanks for '64'") - await flow.assert_reply("step3") - - @pytest.mark.asyncio - async def test_waterfall_nested(self): - """Nested waterfall dialogs chain correctly when each ends.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - - async def waterfall_a_step1(step: WaterfallStepContext) -> DialogTurnResult: - await step.context.send_activity("step1") - return await step.begin_dialog("test-waterfall-b") - - async def waterfall_a_step2(step: WaterfallStepContext) -> DialogTurnResult: - await step.context.send_activity("step2") - return await step.begin_dialog("test-waterfall-c") - - async def waterfall_b_step1(step: WaterfallStepContext) -> DialogTurnResult: - await step.context.send_activity("step1.1") - return Dialog.end_of_turn - - async def waterfall_b_step2(step: WaterfallStepContext) -> DialogTurnResult: - await step.context.send_activity("step1.2") - return Dialog.end_of_turn - - async def waterfall_c_step1(step: WaterfallStepContext) -> DialogTurnResult: - await step.context.send_activity("step2.1") - return Dialog.end_of_turn - - async def waterfall_c_step2(step: WaterfallStepContext) -> DialogTurnResult: - await step.context.send_activity("step2.2") - return await step.end_dialog() - - ds = DialogSet(dialog_state) - ds.add( - WaterfallDialog("test-waterfall-a", [waterfall_a_step1, waterfall_a_step2]) - ) - ds.add( - WaterfallDialog("test-waterfall-b", [waterfall_b_step1, waterfall_b_step2]) - ) - ds.add( - WaterfallDialog("test-waterfall-c", [waterfall_c_step1, waterfall_c_step2]) - ) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dc.begin_dialog("test-waterfall-a") - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - flow = await adapter.send("hello") - flow = await flow.assert_reply("step1") - flow = await flow.assert_reply("step1.1") - flow = await flow.send("hello") - flow = await flow.assert_reply("step1.2") - flow = await flow.send("hello") - flow = await flow.assert_reply("step2") - flow = await flow.assert_reply("step2.1") - flow = await flow.send("hello") - await flow.assert_reply("step2.2") - - @pytest.mark.asyncio - async def test_waterfall_datetime_prompt_first_invalid_then_valid(self): - """DateTimePrompt re-prompts on invalid input and accepts valid date/time.""" - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - - async def step1(step: WaterfallStepContext) -> DialogTurnResult: - return await step.prompt( - "dateTimePrompt", - PromptOptions( - prompt=Activity(type=ActivityTypes.message, text="Provide a date") - ), - ) - - async def step2(step: WaterfallStepContext) -> DialogTurnResult: - assert step.result is not None - return await step.end_dialog() - - ds = DialogSet(dialog_state) - ds.add(DateTimePrompt("dateTimePrompt", default_locale=Culture.English)) - ds.add(WaterfallDialog("test-dateTimePrompt", [step1, step2])) - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - await dc.begin_dialog("test-dateTimePrompt") - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - flow = await adapter.send("hello") - flow = await flow.assert_reply("Provide a date") - flow = await flow.send("hello again") - flow = await flow.assert_reply("Provide a date") - await flow.send("Wednesday 4 oclock") diff --git a/dev/hosting_dialogs/test_waterfall_dialog.py b/dev/hosting_dialogs/test_waterfall_dialog.py deleted file mode 100644 index de56539f9..000000000 --- a/dev/hosting_dialogs/test_waterfall_dialog.py +++ /dev/null @@ -1,171 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest -from unittest.mock import MagicMock - -from microsoft_agents.activity import ActivityTypes -from microsoft_agents.hosting.dialogs import ( - ComponentDialog, - Dialog, - DialogContext, - DialogReason, - DialogSet, - DialogState, - DialogTurnResult, - DialogTurnStatus, - WaterfallDialog, -) - - -def _make_dc(activity_type="message"): - """Create a minimal DialogContext with the given activity type.""" - - class _Stub(ComponentDialog): - def __init__(self): - super().__init__("stub") - - ds = _Stub()._dialogs - mock_tc = MagicMock() - mock_tc.activity.type = activity_type - return DialogContext(ds, mock_tc, DialogState()) - - -class TestWaterfallDialogValidation: - @pytest.mark.asyncio - async def test_begin_dialog_null_dc_raises(self): - dialog = WaterfallDialog("A", []) - with pytest.raises((TypeError, Exception)): - await dialog.begin_dialog(None) - - @pytest.mark.asyncio - async def test_continue_dialog_null_dc_raises(self): - dialog = WaterfallDialog("A", []) - with pytest.raises((TypeError, Exception)): - await dialog.continue_dialog(None) - - @pytest.mark.asyncio - async def test_continue_dialog_returns_waiting_for_non_message_activity(self): - """WaterfallDialog.continue_dialog returns end_of_turn for non-message activities.""" - dialog = WaterfallDialog("A", []) - dc = _make_dc(activity_type=ActivityTypes.event) - - result = await dialog.continue_dialog(dc) - - # end_of_turn is DialogTurnResult(Waiting) - assert result.status == DialogTurnStatus.Waiting - - @pytest.mark.asyncio - async def test_resume_dialog_null_dc_raises(self): - dialog = WaterfallDialog("A", []) - with pytest.raises((TypeError, Exception)): - await dialog.resume_dialog(None, DialogReason.BeginCalled, "result") - - @pytest.mark.asyncio - async def test_run_step_null_dc_raises(self): - dialog = WaterfallDialog("A", []) - with pytest.raises((TypeError, Exception)): - await dialog.run_step(None, 0, DialogReason.BeginCalled, None) - - def test_none_name_raises(self): - with pytest.raises((TypeError, Exception)): - WaterfallDialog(None, []) - - def test_add_none_step_raises(self): - dialog = WaterfallDialog("A", []) - with pytest.raises((TypeError, Exception)): - dialog.add_step(None) - - def test_steps_must_be_list(self): - with pytest.raises((TypeError, Exception)): - WaterfallDialog("A", {1, 2}) - - @pytest.mark.asyncio - async def test_empty_steps_completes_immediately(self): - """WaterfallDialog with no steps ends immediately with None result.""" - from microsoft_agents.hosting.core import ConversationState, MemoryStorage - from microsoft_agents.hosting.dialogs import DialogSet - from tests.hosting_dialogs.helpers import DialogTestAdapter - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - dialogs = DialogSet(dialog_state) - dialogs.add(WaterfallDialog("empty-waterfall", [])) - - result_holder = {} - - async def exec(tc): - dc = await dialogs.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - results = await dc.begin_dialog("empty-waterfall") - result_holder["status"] = results.status - result_holder["result"] = results.result - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - await adapter.send("hi") - assert result_holder["status"] == DialogTurnStatus.Complete - assert result_holder["result"] is None - - @pytest.mark.asyncio - async def test_continue_dialog_non_message_returns_waiting(self): - """WaterfallDialog.continue_dialog ignores non-message activities.""" - dialog = WaterfallDialog("A", [lambda step: Dialog.end_of_turn]) - dc = _make_dc(activity_type=ActivityTypes.event) - result = await dialog.continue_dialog(dc) - assert result.status == DialogTurnStatus.Waiting - - -class TestWaterfallStepName: - def test_get_step_name_named_function_uses_qualname(self): - """Named step functions expose their __qualname__ as the step name.""" - - async def my_named_step(step): - return Dialog.end_of_turn - - dialog = WaterfallDialog("A", [my_named_step]) - assert "my_named_step" in dialog.get_step_name(0) - - def test_get_step_name_lambda_uses_fallback_format(self): - """Lambda steps fall back to 'Step{n}of{total}' because their __qualname__ ends in ''.""" - dialog = WaterfallDialog("A", [lambda s: None, lambda s: None]) - assert dialog.get_step_name(0) == "Step1of2" - assert dialog.get_step_name(1) == "Step2of2" - - -class TestWaterfallStepNoneReturn: - @pytest.mark.asyncio - async def test_step_returning_none_raises_type_error(self): - """A step returning None raises a clear TypeError instead of a confusing AttributeError.""" - from microsoft_agents.hosting.core import ConversationState, MemoryStorage - from microsoft_agents.hosting.dialogs import DialogSet - from tests.hosting_dialogs.helpers import DialogTestAdapter - - convo_state = ConversationState(MemoryStorage()) - dialog_state = convo_state.create_property("dialogState") - ds = DialogSet(dialog_state) - - async def bad_step(step): - return None # programmer forgot to return Dialog.end_of_turn - - ds.add(WaterfallDialog("bad-waterfall", [bad_step])) - - error_holder = {} - - async def exec(tc): - dc = await ds.create_context(tc) - results = await dc.continue_dialog() - if results.status == DialogTurnStatus.Empty: - try: - await dc.begin_dialog("bad-waterfall") - except TypeError as e: - error_holder["error"] = e - await convo_state.save(tc) - - adapter = DialogTestAdapter(exec) - await adapter.send("hi") - - assert "error" in error_holder, "Expected TypeError but none was raised" - error_msg = str(error_holder["error"]).lower() - assert "none" in error_msg or "step" in error_msg diff --git a/dev/hosting_dialogs/test_waterfall_step_context.py b/dev/hosting_dialogs/test_waterfall_step_context.py deleted file mode 100644 index 1f0409460..000000000 --- a/dev/hosting_dialogs/test_waterfall_step_context.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import pytest -from unittest.mock import AsyncMock, MagicMock - -from microsoft_agents.hosting.dialogs import ( - ComponentDialog, - DialogContext, - DialogReason, - DialogSet, - DialogState, - DialogTurnResult, - DialogTurnStatus, - WaterfallDialog, -) -from microsoft_agents.hosting.dialogs.waterfall_step_context import WaterfallStepContext - - -def _make_step_context(): - """Create a WaterfallStepContext backed by mock objects.""" - - class _Stub(ComponentDialog): - def __init__(self): - super().__init__("stub") - - ds = _Stub()._dialogs - mock_tc = MagicMock() - dc = DialogContext(ds, mock_tc, DialogState()) - - wf_dialog = WaterfallDialog("wf", []) - wf_dialog.resume_dialog = AsyncMock( - return_value=DialogTurnResult(DialogTurnStatus.Complete) - ) - - return WaterfallStepContext(wf_dialog, dc, None, {}, 0, DialogReason.BeginCalled) - - -class TestWaterfallStepContext: - @pytest.mark.asyncio - async def test_next_called_twice_raises(self): - """Calling next() a second time on the same step context must raise.""" - step_ctx = _make_step_context() - - # First call succeeds - await step_ctx.next(None) - - # Second call raises - with pytest.raises(Exception, match="already called"): - await step_ctx.next(None) - - @pytest.mark.asyncio - async def test_next_calls_resume_on_parent_dialog(self): - """next() delegates to the parent WaterfallDialog's resume_dialog.""" - step_ctx = _make_step_context() - await step_ctx.next("my-result") - step_ctx._wf_parent.resume_dialog.assert_awaited_once() - - def test_step_context_properties(self): - """WaterfallStepContext exposes index, options, reason, result, values.""" - step_ctx = _make_step_context() - assert step_ctx.index == 0 - assert step_ctx.options is None - assert step_ctx.reason == DialogReason.BeginCalled - assert step_ctx.result is None - assert step_ctx.values == {} - - def test_step_context_with_result(self): - class _Stub(ComponentDialog): - def __init__(self): - super().__init__("stub") - - ds = _Stub()._dialogs - dc = DialogContext(ds, MagicMock(), DialogState()) - wf = WaterfallDialog("wf", []) - wf.resume_dialog = AsyncMock( - return_value=DialogTurnResult(DialogTurnStatus.Complete) - ) - - step_ctx = WaterfallStepContext( - wf, - dc, - {"key": "val"}, - {"v": 1}, - 2, - DialogReason.NextCalled, - "previous-result", - ) - - assert step_ctx.index == 2 - assert step_ctx.options == {"key": "val"} - assert step_ctx.values == {"v": 1} - assert step_ctx.result == "previous-result" - assert step_ctx.reason == DialogReason.NextCalled diff --git a/dev/integration/README.md b/dev/integration/README.md new file mode 100644 index 000000000..42177e068 --- /dev/null +++ b/dev/integration/README.md @@ -0,0 +1,15 @@ +# Python SDK (Integration) Tests + +## Description + +This directory contains integration tests for the Python SDK. + +## How to run + +Fill in the `.env` file based on `env.TEMPLATE`. + +Run with + +```bash +uv run pytest +``` \ No newline at end of file diff --git a/dev/hosting_dialogs/__init__.py b/dev/integration/__init__.py similarity index 100% rename from dev/hosting_dialogs/__init__.py rename to dev/integration/__init__.py diff --git a/dev/testing/microsoft-agents-testing/env.TEMPLATE b/dev/integration/env.TEMPLATE similarity index 100% rename from dev/testing/microsoft-agents-testing/env.TEMPLATE rename to dev/integration/env.TEMPLATE diff --git a/dev/integration/pyproject.toml b/dev/integration/pyproject.toml new file mode 100644 index 000000000..0e0ea7111 --- /dev/null +++ b/dev/integration/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "integration" +version = "0.0.0" +requires-python = ">=3.13" +dependencies = [ + "pytest", + "pytest-asyncio", + "microsoft-agents-hosting-dialogs", + "microsoft-agents-testing @ file:///${PROJECT_ROOT}/../microsoft-agents-testing", +] \ No newline at end of file diff --git a/dev/testing/python-sdk-tests/pytest.ini b/dev/integration/pytest.ini similarity index 100% rename from dev/testing/python-sdk-tests/pytest.ini rename to dev/integration/pytest.ini diff --git a/dev/hosting_dialogs/choices/__init__.py b/dev/integration/tests/__init__.py similarity index 100% rename from dev/hosting_dialogs/choices/__init__.py rename to dev/integration/tests/__init__.py diff --git a/dev/hosting_dialogs/memory/__init__.py b/dev/integration/tests/activity_handler/__init__.py similarity index 100% rename from dev/hosting_dialogs/memory/__init__.py rename to dev/integration/tests/activity_handler/__init__.py diff --git a/dev/hosting_dialogs/memory/scopes/__init__.py b/dev/integration/tests/activity_handler/dialogs/__init__.py similarity index 100% rename from dev/hosting_dialogs/memory/scopes/__init__.py rename to dev/integration/tests/activity_handler/dialogs/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/__init__.py b/dev/integration/tests/activity_handler/dialogs/sample/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/__init__.py rename to dev/integration/tests/activity_handler/dialogs/sample/__init__.py diff --git a/dev/testing/python-sdk-tests/tests/activity_handler/dialogs/sample/booking_dialog.py b/dev/integration/tests/activity_handler/dialogs/sample/booking_dialog.py similarity index 100% rename from dev/testing/python-sdk-tests/tests/activity_handler/dialogs/sample/booking_dialog.py rename to dev/integration/tests/activity_handler/dialogs/sample/booking_dialog.py diff --git a/dev/testing/python-sdk-tests/tests/activity_handler/dialogs/sample/dialog_agent.py b/dev/integration/tests/activity_handler/dialogs/sample/dialog_agent.py similarity index 100% rename from dev/testing/python-sdk-tests/tests/activity_handler/dialogs/sample/dialog_agent.py rename to dev/integration/tests/activity_handler/dialogs/sample/dialog_agent.py diff --git a/dev/testing/python-sdk-tests/tests/activity_handler/dialogs/sample/user_profile.py b/dev/integration/tests/activity_handler/dialogs/sample/user_profile.py similarity index 100% rename from dev/testing/python-sdk-tests/tests/activity_handler/dialogs/sample/user_profile.py rename to dev/integration/tests/activity_handler/dialogs/sample/user_profile.py diff --git a/dev/testing/python-sdk-tests/tests/activity_handler/dialogs/sample/user_profile_dialog.py b/dev/integration/tests/activity_handler/dialogs/sample/user_profile_dialog.py similarity index 100% rename from dev/testing/python-sdk-tests/tests/activity_handler/dialogs/sample/user_profile_dialog.py rename to dev/integration/tests/activity_handler/dialogs/sample/user_profile_dialog.py diff --git a/dev/testing/python-sdk-tests/tests/activity_handler/dialogs/scenario.py b/dev/integration/tests/activity_handler/dialogs/scenario.py similarity index 84% rename from dev/testing/python-sdk-tests/tests/activity_handler/dialogs/scenario.py rename to dev/integration/tests/activity_handler/dialogs/scenario.py index c01365225..6cb914c2d 100644 --- a/dev/testing/python-sdk-tests/tests/activity_handler/dialogs/scenario.py +++ b/dev/integration/tests/activity_handler/dialogs/scenario.py @@ -6,8 +6,8 @@ from microsoft_agents.hosting.core import ConversationState, UserState, Storage from microsoft_agents.testing import ActivityHandlerScenario, ScenarioConfig -from tests.activity_handler.dialogs.sample.dialog_agent import DialogAgent -from tests.activity_handler.dialogs.sample.user_profile_dialog import UserProfileDialog +from .sample.dialog_agent import DialogAgent +from .sample.user_profile_dialog import UserProfileDialog def _create_handler( diff --git a/dev/testing/python-sdk-tests/tests/activity_handler/dialogs/test_booking_dialog.py b/dev/integration/tests/activity_handler/dialogs/test_booking_dialog.py similarity index 98% rename from dev/testing/python-sdk-tests/tests/activity_handler/dialogs/test_booking_dialog.py rename to dev/integration/tests/activity_handler/dialogs/test_booking_dialog.py index 79b876c14..62064d405 100644 --- a/dev/testing/python-sdk-tests/tests/activity_handler/dialogs/test_booking_dialog.py +++ b/dev/integration/tests/activity_handler/dialogs/test_booking_dialog.py @@ -29,8 +29,8 @@ ActivityTemplate, ) -from tests.activity_handler.dialogs.sample.dialog_agent import DialogAgent -from tests.activity_handler.dialogs.sample.booking_dialog import BookingDialog +from .sample.dialog_agent import DialogAgent +from .sample.booking_dialog import BookingDialog # --------------------------------------------------------------------------- # Shared scenario diff --git a/dev/testing/python-sdk-tests/tests/activity_handler/dialogs/test_user_profile_dialog.py b/dev/integration/tests/activity_handler/dialogs/test_user_profile_dialog.py similarity index 99% rename from dev/testing/python-sdk-tests/tests/activity_handler/dialogs/test_user_profile_dialog.py rename to dev/integration/tests/activity_handler/dialogs/test_user_profile_dialog.py index b5be3d9ab..55988fde9 100644 --- a/dev/testing/python-sdk-tests/tests/activity_handler/dialogs/test_user_profile_dialog.py +++ b/dev/integration/tests/activity_handler/dialogs/test_user_profile_dialog.py @@ -25,7 +25,7 @@ from microsoft_agents.activity import Activity, Attachment, ActivityTypes from microsoft_agents.testing import AgentClient, ScenarioConfig, ClientConfig, ActivityTemplate -from tests.activity_handler.dialogs.scenario import create_dialog_scenario +from .scenario import create_dialog_scenario # --------------------------------------------------------------------------- # Shared activity template — identifies the test user and conversation diff --git a/dev/testing/python-sdk-tests/tests/scenarios/__init__.py b/dev/integration/tests/scenarios/__init__.py similarity index 100% rename from dev/testing/python-sdk-tests/tests/scenarios/__init__.py rename to dev/integration/tests/scenarios/__init__.py diff --git a/dev/testing/python-sdk-tests/tests/scenarios/quickstart.py b/dev/integration/tests/scenarios/quickstart.py similarity index 100% rename from dev/testing/python-sdk-tests/tests/scenarios/quickstart.py rename to dev/integration/tests/scenarios/quickstart.py diff --git a/dev/testing/python-sdk-tests/tests/integration/test_expect_replies.py b/dev/integration/tests/test_expect_replies.py similarity index 95% rename from dev/testing/python-sdk-tests/tests/integration/test_expect_replies.py rename to dev/integration/tests/test_expect_replies.py index 993e56c0f..4367fd91f 100644 --- a/dev/testing/python-sdk-tests/tests/integration/test_expect_replies.py +++ b/dev/integration/tests/test_expect_replies.py @@ -1,7 +1,7 @@ import pytest from microsoft_agents.activity import Activity from microsoft_agents.testing import AgentClient -from tests.scenarios import load_scenario +from .scenarios import load_scenario @pytest.mark.agent_test(load_scenario("quickstart")) diff --git a/dev/testing/python-sdk-tests/tests/integration/test_quickstart.py b/dev/integration/tests/test_quickstart.py similarity index 89% rename from dev/testing/python-sdk-tests/tests/integration/test_quickstart.py rename to dev/integration/tests/test_quickstart.py index 58e07e5d9..726d08b20 100644 --- a/dev/testing/python-sdk-tests/tests/integration/test_quickstart.py +++ b/dev/integration/tests/test_quickstart.py @@ -7,7 +7,7 @@ ScenarioConfig, ) -from tests.scenarios import load_scenario +from .scenarios import load_scenario _TEMPLATE = { "channel_id": "webchat", @@ -62,10 +62,4 @@ async def test_send_hi(self, agent_client: AgentClient): """Test sending a 'hi' message and receiving a response.""" await agent_client.send("hi", wait=1.0) - responses = agent_client.recent() - - assert len(responses) == 2 - assert len(agent_client.history()) == 2 - - agent_client.expect().that_for_one(type="message", text="you said: hi") - agent_client.expect().that_for_one(type="typing") \ No newline at end of file + agent_client.expect().that_for_one(type="message", text="~hi") \ No newline at end of file diff --git a/dev/testing/python-sdk-tests/tests/integration/test_streaming_response.py b/dev/integration/tests/test_streaming_response.py similarity index 100% rename from dev/testing/python-sdk-tests/tests/integration/test_streaming_response.py rename to dev/integration/tests/test_streaming_response.py diff --git a/dev/testing/python-sdk-tests/tests/integration/test_telemetry.py b/dev/integration/tests/test_telemetry.py similarity index 88% rename from dev/testing/python-sdk-tests/tests/integration/test_telemetry.py rename to dev/integration/tests/test_telemetry.py index 9d233d44b..4faf3ac57 100644 --- a/dev/testing/python-sdk-tests/tests/integration/test_telemetry.py +++ b/dev/integration/tests/test_telemetry.py @@ -1,18 +1,7 @@ import pytest -from opentelemetry import trace, metrics -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import SimpleSpanProcessor -from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter -from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export import InMemoryMetricReader - from microsoft_agents.activity import DeliveryModes -from microsoft_agents.hosting.core.telemetry import ( - attributes, - SERVICE_NAME, - SERVICE_VERSION -) +from microsoft_agents.hosting.core.telemetry import attributes from microsoft_agents.hosting.core.telemetry.adapter import constants as adapter_constants from microsoft_agents.hosting.core.telemetry.turn_context import constants as turn_context_constants from microsoft_agents.hosting.core.app.telemetry import constants as app_constants @@ -21,14 +10,14 @@ from microsoft_agents.hosting.core.connector.telemetry import constants as connector_constants from microsoft_agents.hosting.core.storage.telemetry import constants as storage_constants -from tests.scenarios import load_scenario +from .scenarios import load_scenario -from tests.utils.telemetry_fixtures import ( # unused imports are needed for fixtures +from .utils.telemetry_fixtures import ( # unused imports are needed for fixtures test_telemetry, test_exporter, test_metric_reader, ) -from tests.utils.telemetry_utils import ( +from .utils.telemetry_utils import ( sum_counter, sum_hist_count, find_metric diff --git a/dev/testing/python-sdk-tests/tests/utils/telemetry_fixtures.py b/dev/integration/tests/utils/telemetry_fixtures.py similarity index 100% rename from dev/testing/python-sdk-tests/tests/utils/telemetry_fixtures.py rename to dev/integration/tests/utils/telemetry_fixtures.py diff --git a/dev/testing/python-sdk-tests/tests/utils/telemetry_utils.py b/dev/integration/tests/utils/telemetry_utils.py similarity index 100% rename from dev/testing/python-sdk-tests/tests/utils/telemetry_utils.py rename to dev/integration/tests/utils/telemetry_utils.py diff --git a/dev/integration/uv.lock b/dev/integration/uv.lock new file mode 100644 index 000000000..2465e6444 --- /dev/null +++ b/dev/integration/uv.lock @@ -0,0 +1,1282 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" }, + { url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" }, + { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" }, + { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" }, + { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" }, + { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" }, + { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" }, + { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" }, + { url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" }, + { url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" }, + { url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" }, + { url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" }, + { url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" }, + { url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" }, + { url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" }, + { url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" }, + { url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" }, + { url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" }, + { url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" }, + { url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" }, + { url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" }, + { url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" }, + { url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" }, + { url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" }, + { url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" }, + { url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" }, + { url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" }, + { url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" }, + { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "azure-core" +version = "1.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/f3/b416179e408990df5db0d516283022dde0f5d0111d98c1a848e41853e81c/azure_core-1.41.0.tar.gz", hash = "sha256:f46ff5dfcd230f25cf1c19e8a34b8dc08a337b2503e268bb600a16c00db8ad5a", size = 381042, upload-time = "2026-05-07T23:30:54.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/db/325c6d7312d2200251c52323878281045aaffcb5586612296484e4280eaa/azure_core-1.41.0-py3-none-any.whl", hash = "sha256:522b4011e8180b1a3dcd2024396a4e7fe9ac37fb8597db47163d230b5efe892d", size = 220920, upload-time = "2026-05-07T23:30:56.357Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, + { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, + { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, + { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, + { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, +] + +[[package]] +name = "datedelta" +version = "1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/5d/a34fd53137d287fd08182d9530941f651a88de67bddc68292954166355e0/datedelta-1.4.tar.gz", hash = "sha256:3f1ef319ead642a76a3cab731917bf14a0ced0d91943f33ff57ae615837cab97", size = 8049, upload-time = "2022-01-01T13:06:06.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/04/72573efc775afde1c0a69f30e1f0f5585fee421a6abf1bec5eefec5980ec/datedelta-1.4-py3-none-any.whl", hash = "sha256:9f7a8a2bd80d29d6cfe1036990979ea404c8e1ddbc5a73cff74d283186d12b4d", size = 6426, upload-time = "2022-01-01T13:06:04.464Z" }, +] + +[[package]] +name = "emoji" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/8a/b08dd0b946f0843cba782e3784cd979c33f144e5e7c37fb0162dc47acd43/emoji-1.7.0.tar.gz", hash = "sha256:65c54533ea3c78f30d0729288998715f418d7467de89ec258a31c0ce8660a1d1", size = 175436, upload-time = "2022-03-08T17:51:05.642Z" } + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "grapheme" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/e7/bbaab0d2a33e07c8278910c1d0d8d4f3781293dfbc70b5c38197159046bf/grapheme-0.6.0.tar.gz", hash = "sha256:44c2b9f21bbe77cfb05835fec230bd435954275267fea1858013b102f8603cca", size = 207306, upload-time = "2020-03-07T17:13:55.492Z" } + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "integration" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "microsoft-agents-hosting-dialogs" }, + { name = "microsoft-agents-testing" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, +] + +[package.metadata] +requires-dist = [ + { name = "microsoft-agents-hosting-dialogs" }, + { name = "microsoft-agents-testing", directory = "../microsoft-agents-testing" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, +] + +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + +[[package]] +name = "microsoft-agents-activity" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/e7/54116195448e8db78701294d27da29071e32351d6672985361732072cb6e/microsoft_agents_activity-1.1.0.tar.gz", hash = "sha256:ed930777677be683603dcfc5d470e66d15db4a261f878c193076b1742391777b", size = 62563, upload-time = "2026-06-19T21:01:45.359Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/20/b015df33634477c43bcfb24ffd443d3cf3d62cecc543f7faa7bdba106584/microsoft_agents_activity-1.1.0-py3-none-any.whl", hash = "sha256:6bd5203bbb9abd876ea427bdfa99078528f414b3f2cf6b5a58ef5232743d7331", size = 134834, upload-time = "2026-06-19T21:01:55.882Z" }, +] + +[[package]] +name = "microsoft-agents-authentication-msal" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "microsoft-agents-hosting-core" }, + { name = "msal" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/38/7c6c9d12e8d22f6c94499b10868adf4e87148be7849ab8a722f8e5bc906d/microsoft_agents_authentication_msal-1.1.0.tar.gz", hash = "sha256:6912e45aa57b02d747ffa61696b04f4e88e8e97cb7d276b781897cd66b58ebaf", size = 14012, upload-time = "2026-06-19T21:01:46.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/6c/07bc3d640d4db684f267ca07f04c4e3522d401cb12c61b654499042bc77c/microsoft_agents_authentication_msal-1.1.0-py3-none-any.whl", hash = "sha256:c4ea4fc468390cccc3a763602590236d332764576b521eeba75eec8afd9194e5", size = 12942, upload-time = "2026-06-19T21:01:56.923Z" }, +] + +[[package]] +name = "microsoft-agents-hosting-aiohttp" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "microsoft-agents-hosting-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/1e/50661b83ef531a168437d117b975d65c5950a5e49f8f61de0419194c6a90/microsoft_agents_hosting_aiohttp-1.1.0.tar.gz", hash = "sha256:cd84c9e63cd3ac317b6a5de6ce7cccc9743a0f24226cfc21a95850b01365c93e", size = 10750, upload-time = "2026-06-19T21:01:48.581Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b8/bec79da936a80a753f13208a04d636cccdc1f7aa2036c9127210020115d1/microsoft_agents_hosting_aiohttp-1.1.0-py3-none-any.whl", hash = "sha256:1adf264efbbe942957ea4c512bf48e77ac792f427a67daa1bdb347ebf1029dfe", size = 9909, upload-time = "2026-06-19T21:01:58.887Z" }, +] + +[[package]] +name = "microsoft-agents-hosting-core" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "isodate" }, + { name = "microsoft-agents-activity" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "pyjwt" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/e2/115cf2de5290ed7c6908b95e8ef5c5d15adee5fc972e0f083b794172aba3/microsoft_agents_hosting_core-1.1.0.tar.gz", hash = "sha256:c609f7d5ea40350911dffa25383b8d9745786e89fb587da0443203d64ff476c6", size = 122893, upload-time = "2026-06-19T21:01:49.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/71/19e09401ada8ae5caf53119721a3e171c9b03a9ae1ff73f6b8eb32f4468f/microsoft_agents_hosting_core-1.1.0-py3-none-any.whl", hash = "sha256:747ab6f48b73ddb330ea85f6431ee4257c7b2faced7fa779ee3f149e60ec8e70", size = 186849, upload-time = "2026-06-19T21:01:59.85Z" }, +] + +[[package]] +name = "microsoft-agents-hosting-dialogs" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "emoji" }, + { name = "microsoft-agents-hosting-core" }, + { name = "recognizers-text-choice" }, + { name = "recognizers-text-date-time" }, + { name = "recognizers-text-number" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/a2/986859bebef45f57912e1df75fda087d6207d4f262f781fa0ae3e89174a8/microsoft_agents_hosting_dialogs-1.1.0.tar.gz", hash = "sha256:6776ce6b165e86e0e4e852d34e10433c325b28f10e5d67c04b31982d22683989", size = 60084, upload-time = "2026-06-19T21:01:50.588Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/fd/38019dce6f72b176b272bd65da47c44ca7167a3fbe6ba6f9c30964c5d141/microsoft_agents_hosting_dialogs-1.1.0-py3-none-any.whl", hash = "sha256:111639af55d363506378dae9a0a83670f6f180eaba0d9b76e646d0748d841282", size = 98362, upload-time = "2026-06-19T21:02:01.232Z" }, +] + +[[package]] +name = "microsoft-agents-testing" +source = { directory = "../microsoft-agents-testing" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "microsoft-agents-activity" }, + { name = "microsoft-agents-authentication-msal" }, + { name = "microsoft-agents-hosting-aiohttp" }, + { name = "microsoft-agents-hosting-core" }, + { name = "psutil" }, + { name = "pydantic" }, + { name = "pytest" }, + { name = "pytest-aiohttp" }, + { name = "pytest-asyncio" }, + { name = "pytest-mock" }, + { name = "python-dotenv" }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "microsoft-agents-activity" }, + { name = "microsoft-agents-authentication-msal" }, + { name = "microsoft-agents-hosting-aiohttp" }, + { name = "microsoft-agents-hosting-core" }, + { name = "psutil" }, + { name = "pydantic" }, + { name = "pytest" }, + { name = "pytest-aiohttp" }, + { name = "pytest-asyncio" }, + { name = "pytest-mock" }, + { name = "python-dotenv" }, + { name = "requests" }, +] + +[[package]] +name = "msal" +version = "1.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/99/d840198ecf6e8057bbc937f129ae940404485d736cda73253bbff9537f01/msal-1.37.0.tar.gz", hash = "sha256:1b1672a33ee467c1d70b341bb16cafd51bb3c817147a95b93263794b03971bec", size = 182444, upload-time = "2026-05-29T19:49:05.561Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/b0/d807279f4b55d16d1f120d5ac4344c6e39b56732e2a224d40bded7fd67ad/msal-1.37.0-py3-none-any.whl", hash = "sha256:dd17e95a7c71bce75e8108113438ba7c4a086b3bcad4f57a8c09b7af3d753c2d", size = 123725, upload-time = "2026-05-29T19:49:04.335Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "multipledispatch" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/3e/a62c3b824c7dec33c4a1578bcc842e6c30300051033a4e5975ed86cc2536/multipledispatch-1.0.0.tar.gz", hash = "sha256:5c839915465c68206c3e9c473357908216c28383b425361e5d144594bf85a7e0", size = 12385, upload-time = "2023-06-27T16:45:11.074Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/c0/00c9809d8b9346eb238a6bbd5f83e846a4ce4503da94a4c08cb7284c325b/multipledispatch-1.0.0-py3-none-any.whl", hash = "sha256:0c53cd8b077546da4e48869f49b13164bebafd0c2a5afceb6bb6a316e7fb46e4", size = 12818, upload-time = "2023-06-27T16:45:09.418Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/cc/e4c9584181f86494df0f6bdec1a4f3280c50db44704dc2a407e994fc87bb/opentelemetry_api-1.43.0.tar.gz", hash = "sha256:107d0d03857ea8fc7c5fcbbbd83f800c281f0d560553d61c1d675fccfd1761c1", size = 73476, upload-time = "2026-06-24T15:19:55.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/83/6dba32b85f31868400440dc7ad2ca1eab94cbbf3a7b0459ed39f8311a9e2/opentelemetry_api-1.43.0-py3-none-any.whl", hash = "sha256:20acf45e9b21851926835292e4045d290acade1edd2ff3de86d2f069687ba1fd", size = 61912, upload-time = "2026-06-24T15:19:35.434Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/eb/5041074274ac0956b03637cc039d434569112468e875eddfcc9a0674ce06/opentelemetry_sdk-1.43.0.tar.gz", hash = "sha256:d8187c81c162df9913e4003dd6485f7390d9a24fc17026ec7387b8b8218b08e9", size = 254744, upload-time = "2026-06-24T15:20:08.467Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/e3/b17be23af124201c9f52eececd4cc8ddfed1597d37b4ee771895d325805c/opentelemetry_sdk-1.43.0-py3-none-any.whl", hash = "sha256:d1323a547c1ce69d6a069a17a44b7da82bb8b332051ecb074041f87642c86823", size = 178852, upload-time = "2026-06-24T15:19:52.169Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.64b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/30/5f26df29509eccd86b99b481ac9ffa39da49ba9577cc69071c552ae30447/opentelemetry_semantic_conventions-0.64b0.tar.gz", hash = "sha256:72f76fb2d1582d9d033dd1fcd84532e961e6ff3d90d24ba6fabc72975a83864c", size = 148340, upload-time = "2026-06-24T15:20:09.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/ca/23ba87a221b574a7c5a99d48849d80bfe8b047624681357e2b002e566187/opentelemetry_semantic_conventions-0.64b0-py3-none-any.whl", hash = "sha256:ea77e85e354b8f604ddbe5f3d9135216f982fa4d77e5859ac30f6d8a50505aa6", size = 203713, upload-time = "2026-06-24T15:19:53.339Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-aiohttp" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/4d/c6621fc79022f6c84a806e23d9b7eca24fae4f3ee779219bbe524339d666/pytest_aiohttp-1.1.1.tar.gz", hash = "sha256:3aa9c9fe26e543eaccc7eb0add381c685ba3ed3e2fed0af74540f63bcd31458d", size = 13704, upload-time = "2026-06-07T23:56:34.173Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b0/5056ed4c3f68a4db2b4a39fb0ec61b1e4cf1d89ee14effe5261cc587264c/pytest_aiohttp-1.1.1-py3-none-any.whl", hash = "sha256:f293441ad4f8446a1e12257130c26c7de03a615c2a5572a8cb046e5b3b4e5211", size = 9007, upload-time = "2026-06-07T23:56:33.333Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "recognizers-text" +version = "1.0.2a2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "emoji" }, + { name = "multipledispatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/ce/ffae848912bbeab712585a2ffc9f7d8d39ae3a9675fade393111560eadf6/recognizers-text-1.0.2a2.tar.gz", hash = "sha256:866cb469c9d5bcc1d230bd31cb684bd5577bdda3c732f5e17817e515c9d69d7c", size = 8229, upload-time = "2019-11-12T20:42:34.442Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/01/aca90bb6c21bd7513522182916a9e150f1b7a5e5832924067706b305c66e/recognizers_text-1.0.2a2-py3-none-any.whl", hash = "sha256:0679d60e952fa3c55b1d459da8f7881425900b0aad29fdfd9291d053168f86af", size = 14514, upload-time = "2019-11-12T20:42:33.164Z" }, +] + +[[package]] +name = "recognizers-text-choice" +version = "1.0.2a2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grapheme" }, + { name = "recognizers-text" }, + { name = "regex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/85/eda554018644534b67a50fe29f88c47b93643f72a3ad417e1ead17e93658/recognizers-text-choice-1.0.2a2.tar.gz", hash = "sha256:fbae3ccf5dc3689055c15e3633767bc9ed0478058bd09c71000550c5684af69d", size = 5098, upload-time = "2019-11-12T20:44:57.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/9a/5e59b7b01c98bf082c77ddff4552a459b10184bb61ea9fc9ce08ca1f0df0/recognizers_text_choice-1.0.2a2-py3-none-any.whl", hash = "sha256:129f78009a81b66e90bd51099ba9af018e282c8dccdeec9999039aea636a8f11", size = 7988, upload-time = "2019-11-12T20:44:56.392Z" }, +] + +[[package]] +name = "recognizers-text-date-time" +version = "1.0.2a2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "datedelta" }, + { name = "recognizers-text" }, + { name = "recognizers-text-number" }, + { name = "recognizers-text-number-with-unit" }, + { name = "regex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/7d/e66b9d8f0b750ebf22a735cda915a0b9767377f706b263bc26acb10119fc/recognizers-text-date-time-1.0.2a2.tar.gz", hash = "sha256:e27b98b4f426ffa17eccff5fa1bcda7c4e00c1fbbd079a693c9838925bf59501", size = 169295, upload-time = "2019-11-12T21:00:19.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/08/ed7e943ceaa14c79cd1406aa4e4c16dade596fdcec8edb465b11fddd7c31/recognizers_text_date_time-1.0.2a2-py3-none-any.whl", hash = "sha256:701819711f4c23c33f5748fc2135c0e65dc2ad27356b20479cccb1d49da9f627", size = 248534, upload-time = "2019-11-12T21:00:16.904Z" }, +] + +[[package]] +name = "recognizers-text-number" +version = "1.0.2a2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "recognizers-text" }, + { name = "regex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/75/de617710c012c896178711f20c7239d87653eb55b39664f07d9d90e1badf/recognizers-text-number-1.0.2a2.tar.gz", hash = "sha256:06d41a87989e874eda3926ca02f800773a7c522f237403241a0183baa0b426d6", size = 42138, upload-time = "2019-11-12T20:45:24.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/ff/85fef63ee912fbc28ba2eca5335642d04dbc2bdc5b9ab2a0bcca252e285d/recognizers_text_number-1.0.2a2-py3-none-any.whl", hash = "sha256:b6b098dee2333362e4ba5a410550eef77019940a8f6dbd32aa7342c53359700e", size = 60927, upload-time = "2019-11-12T20:45:23.362Z" }, +] + +[[package]] +name = "recognizers-text-number-with-unit" +version = "1.0.2a2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "recognizers-text" }, + { name = "recognizers-text-number" }, + { name = "regex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/41/63ffb71a77474a3a0638bc295870360b441e769d4490f34144927e730daf/recognizers-text-number-with-unit-1.0.2a2.tar.gz", hash = "sha256:6b8aa9efcfa13f701932dfbc9f10cabcae0d7cff76ca8da1461878f53e2ec404", size = 62788, upload-time = "2019-11-12T20:45:53.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/24/041e9a1b48e2ee02c471716e4f8e7db907f3d3323e617a080598878d59c3/recognizers_text_number_with_unit-1.0.2a2-py3-none-any.whl", hash = "sha256:c7b118cfcbc4435fefec0ecc17a0954fe37e7248648a50953912dac12240058a", size = 75018, upload-time = "2019-11-12T20:45:52.366Z" }, +] + +[[package]] +name = "regex" +version = "2026.6.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/05/e4f219230e11e774a6c9987d2ab0d0c6b8573e13a17e143d0015bee710ef/regex-2026.6.28.tar.gz", hash = "sha256:3cb4b6c5cb3060cc31efdc1fbb27c25fb9b29044afd87e40601a1c4d9db54342", size = 416101, upload-time = "2026-06-28T19:56:55.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/53/d5c1b3cc0b5a0c985563ad6fac93d73ff2b300cb84342d89f044625d6bc7/regex-2026.6.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b295a83426e0e44e9e60fde99789e181bd26788a1890ae7fe2a24c69bb6246ca", size = 490329, upload-time = "2026-06-28T19:54:35.775Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9f/0c3503e819e91ca0e7a901a8e989ebf840ac7c7aea20b1fc7f31b6759f77/regex-2026.6.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c31665c0deb5c111557a1cac8c27bd5629e2f9e7fd5058900a03576c33b601c", size = 292039, upload-time = "2026-06-28T19:54:37.977Z" }, + { url = "https://files.pythonhosted.org/packages/bb/7f/cd004e13fcad23b3794a82307dfd222e6365eb7f598bd3caab148a830bff/regex-2026.6.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6bf295f2c59de77d1ea7de053607ae4dc9ceb3d57bbb6c7ec51ef4acc4ccff94", size = 289488, upload-time = "2026-06-28T19:54:39.545Z" }, + { url = "https://files.pythonhosted.org/packages/73/4c/293fb34586fbcdc47eac436069e9c11f71fae5dadfd4889b475d7d2e5f7a/regex-2026.6.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17c077586770f67e05bbffeba07fbee6b2b22244f4d4caf8d94e59d574befe04", size = 796772, upload-time = "2026-06-28T19:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/92/fa/c0cd1a90b7d12d9dc155cfc8bdea8df9720988ea5b07e8fa1eccbd0ab2dd/regex-2026.6.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e6cb5a61486f9062397d2e189573b39d38ecfaed698fd9fb6e2756a8ebb8762", size = 865467, upload-time = "2026-06-28T19:54:43.485Z" }, + { url = "https://files.pythonhosted.org/packages/4e/db/0b479973046d005a1eaea299d5d536aeecb9488a16d9cbb8286338102e2d/regex-2026.6.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e86e91a2664f44c3a4e363a7d78fb17c27d5046882e30ea5a877f5e89b28d2ba", size = 912345, upload-time = "2026-06-28T19:54:46.091Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5b/d65adfbd02f32212431bca1f06d1e2eb763a20b12978b454bafaf23dacb7/regex-2026.6.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4dfd1331c49233998d84fc5f1f4436cf7a435a7655f6cf0f490229bb5c7254e5", size = 801291, upload-time = "2026-06-28T19:54:48.3Z" }, + { url = "https://files.pythonhosted.org/packages/fc/09/2103686defaf9a0a31c1663782359d5b45f42524c64cca681f5481e44a5e/regex-2026.6.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cadea12805a1bce0b091c302b814207be26fb60a9c0e7f9ad2f9e21790a429fe", size = 777106, upload-time = "2026-06-28T19:54:50.326Z" }, + { url = "https://files.pythonhosted.org/packages/85/5a/b57593c0aa23ed269ec332fbcf07852abcb6b746e811d9464e0d09b4e25f/regex-2026.6.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f2c1682b67ad5d2376498f2a5a2a8f782fa2e4a06d0465b5e357799806e8a20", size = 785175, upload-time = "2026-06-28T19:54:52.172Z" }, + { url = "https://files.pythonhosted.org/packages/79/59/c36e756ad29bf14d7b6c6d7138952476b21f6160286cedb98ac13481c993/regex-2026.6.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:64e142eb55e84868087da1375d7c36ff97d55010951849f515322a91d5fef1b4", size = 860186, upload-time = "2026-06-28T19:54:54.11Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/49808aea0da9649c300139360708fb91b7144be1f962fcebf96755fde948/regex-2026.6.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:abb4daabe7be63273787a62dfd6164dadf8f7a63fbec3d2730e5e5e7126d858c", size = 765754, upload-time = "2026-06-28T19:54:56.04Z" }, + { url = "https://files.pythonhosted.org/packages/be/c5/52bbd436cf2200decdf48825fa38363eaaeebb77011ea9928a1ef9e0b9f2/regex-2026.6.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec2b2ad00ab8c16a2798cc8db80c53c4d5b8b3a2441f6cbaef06625f5ca25854", size = 852085, upload-time = "2026-06-28T19:54:57.988Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c3/0390b66e3019497143fe768b3ba567b64d8b24f3812d09506deb86f4a0f0/regex-2026.6.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bfc9677982c914d9085b8e1c3b3ae6e88f139fb56531c2416d6c8f338093c22b", size = 789600, upload-time = "2026-06-28T19:54:59.977Z" }, + { url = "https://files.pythonhosted.org/packages/88/fd/ab5b03653a244975069fed93d73f4f5f7484c03a84cedb238292510d7182/regex-2026.6.28-cp313-cp313-win32.whl", hash = "sha256:bf54bc693fc4e0530e666ba5ec4bcba14dbe8f66b7cfc15c27317d1a6e40b9a5", size = 267088, upload-time = "2026-06-28T19:55:02.159Z" }, + { url = "https://files.pythonhosted.org/packages/68/55/21022f7d3143210ae8d4ff905c45306237b657375cc0b97883f49db3d423/regex-2026.6.28-cp313-cp313-win_amd64.whl", hash = "sha256:e128feaf65bf3d9eb91bec92322a8f7e4835e9c798f3e9ea4b69f4def85620e3", size = 277680, upload-time = "2026-06-28T19:55:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/b6/99/7f664804f1aef924542b0b233996b78b3e4d0a52d9951358aac99f129f51/regex-2026.6.28-cp313-cp313-win_arm64.whl", hash = "sha256:695873e0ea8d3815ea9e92e2c68faf039cc450e2c0a62a31afe2049eb11be767", size = 277017, upload-time = "2026-06-28T19:55:06.29Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e1/9eb83518e159d719fd681c4932dc2aaff855ce72451e1d05d69466f25a96/regex-2026.6.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:189dbf9fc4252d9f1352bf4bd1bef885edb6cc4b7341df202a65f821aaa3891c", size = 494195, upload-time = "2026-06-28T19:55:08.292Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e2/e259c5f2f7be269d0e2fb54275c1fa6a13fb47019f389c3f3ae457447825/regex-2026.6.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9277a4c6503390aa39cb4483b87ec0384faee0850a23b5cea33d008b5d8d83f1", size = 293976, upload-time = "2026-06-28T19:55:10.014Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4e/9bdf444014d22b045d0c82ca114fac7e07a597b5b5331b7c4ce6328426e2/regex-2026.6.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17eddca4e8ea9af0b5739314776cdf0172a49731ab61f2e1ea66e066ddd46c97", size = 292340, upload-time = "2026-06-28T19:55:11.88Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3a/f49b11e59cbfe187ace0053a460bd72a0169b8cd52e7db9421a074ce7a43/regex-2026.6.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4466b8641e00c697aab5a73150150d2b2ea96b131c595691f42031abafd9f4d", size = 811704, upload-time = "2026-06-28T19:55:13.612Z" }, + { url = "https://files.pythonhosted.org/packages/2f/fb/ad04c39e149bf8b6cf357df5fff78341733ec366780a00c803a36735818c/regex-2026.6.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cfcd4b0bdcf768c498415c170d1ed2a25a99bf0b65fa253bbd02f68ceba6475", size = 871157, upload-time = "2026-06-28T19:55:15.797Z" }, + { url = "https://files.pythonhosted.org/packages/7f/64/0e5ba31c11eb8ef7aac19a690c1211fc9aa9990caf09565785ebb0081b9a/regex-2026.6.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:80c7adf1ef647f6b1e8aa2ca280e517174cd08bdf7a2e412cdfb68bd6a0917cb", size = 917287, upload-time = "2026-06-28T19:55:18.692Z" }, + { url = "https://files.pythonhosted.org/packages/11/75/6b78df2b858c2fcbbc4858fdc3f2975cf2703be374b2842db7d2c32591a7/regex-2026.6.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a043f5770e82283a22aed4cefef1a4e0f9dd8fd7184cb6ce0ad2e579e2134a9e", size = 816333, upload-time = "2026-06-28T19:55:20.973Z" }, + { url = "https://files.pythonhosted.org/packages/b4/01/ecfe665a3694d5eda9f3ec686c856438ada0943947b6005e90556a1e2cdf/regex-2026.6.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3bd630a8dba06b55254ea5ee862194edab52ec783100d2ef1cd15a9c512fee27", size = 785518, upload-time = "2026-06-28T19:55:23.003Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0a/88f9cd88ff1e82881605c4ffd62d77ee67d051232cfe6f8e9a64b86cf0e8/regex-2026.6.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b77207e3cee13086f1906a6a2a12b41244c577e8ad9370d4b35ae1d548d354f3", size = 801371, upload-time = "2026-06-28T19:55:24.888Z" }, + { url = "https://files.pythonhosted.org/packages/a8/97/601483732f93275482ceb9fed57813dfed7c47d3a019db6ec4a3bb6e23e0/regex-2026.6.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:6de82c268e5d101ee9e3ffd869924aa9a371e3a21e752cf4fa17b6ce50d219f7", size = 866517, upload-time = "2026-06-28T19:55:27.232Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/385c2a0351b994a693453c1d1a6e9af9eb35db3c9460d76b5078acd70c62/regex-2026.6.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b15859e3908544fb99cf47341dcf0bfd089147d258c4c4d8a29e5b087f8085cb", size = 772834, upload-time = "2026-06-28T19:55:29.154Z" }, + { url = "https://files.pythonhosted.org/packages/06/bc/bbf4a5b3b29770d7f307d3c28b5b1bca0105b0cb424be0a4eb1339bc92cf/regex-2026.6.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c91487a917edd48a1ea646fdf60d7936d304f0e686fa7ea8326e47efca51d816", size = 856606, upload-time = "2026-06-28T19:55:32.186Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/51d74fff82f682819979249f8d700267108ba5dc4eb284b0e11b9c85e4b3/regex-2026.6.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4ac65f3e3a99fd8f3a4a74e7a6610acd1ce9dfe9b8a03d346a4922380d68aeb", size = 803475, upload-time = "2026-06-28T19:55:34.328Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3e/6be10cefdc813533fe604dbf5d3c77d2638e7ee658b2749ebadc113b6b2e/regex-2026.6.28-cp313-cp313t-win32.whl", hash = "sha256:3f6316f258bc7e6c9c2acbe9954947bbd397a81be3742a637a555f1855d6618d", size = 269126, upload-time = "2026-06-28T19:55:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3c/32cda905ea1a6eeeb798291c294d8ec66ee0efe0cdba28b061e248b1d396/regex-2026.6.28-cp313-cp313t-win_amd64.whl", hash = "sha256:1484bdd6fba28422df9b5ebb04055b2e1b680e8e4f08490bb21ff0f3cc50d0ab", size = 279961, upload-time = "2026-06-28T19:55:38.456Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b9/69f4e5cd6fbe0bb420cb2dbae441ca118f2495bdda522a74da75aa9829e7/regex-2026.6.28-cp313-cp313t-win_arm64.whl", hash = "sha256:3f15020f0b69cafe57baa067ff65b29acef68ff6b1670a53bef1ca11d708e02d", size = 279266, upload-time = "2026-06-28T19:55:40.62Z" }, + { url = "https://files.pythonhosted.org/packages/3b/fb/fad3b810a5bb1e09b9e5d6913fc6ba88cab738fdf283196827a3c59a4c10/regex-2026.6.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:f7c032b0c8a73739ff8ff1aaf30c281fa19c17bf7f1543256c8507390db7807c", size = 490407, upload-time = "2026-06-28T19:55:42.724Z" }, + { url = "https://files.pythonhosted.org/packages/d6/52/b8c79d12276d93e90e707e939b396034c04980caf1235312ef790f8e11fc/regex-2026.6.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f6710f512c57b84f127a23d0f59560a03b64136eff419ae1be5ab557577fe5e3", size = 291988, upload-time = "2026-06-28T19:55:44.549Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/6a911f18279daa8d7bb8b20d771ddb6ef31fabd35f5921f9d3ba21640e80/regex-2026.6.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c0013958f427bd82509a186b9ff206d66cb8d60a81fc797a4c717afd18c5b0ba", size = 289704, upload-time = "2026-06-28T19:55:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/fd/22/ad1955c47c669291a05804d53d7071cc0732dfdf166857be38003cedc2d1/regex-2026.6.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f06cdcd6421f8e194ad312ea608020381250df9b8a57661c1b57e9e5273878", size = 797017, upload-time = "2026-06-28T19:55:48.166Z" }, + { url = "https://files.pythonhosted.org/packages/e5/67/a83159ff8703ab4d0c2cf99e76ebf289b7b4a501623241d09f88f3614f80/regex-2026.6.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec9689392f7494ff4e3f8e7e8522f9158f11023f337eaaf04a64542fc45bbf26", size = 866112, upload-time = "2026-06-28T19:55:51.047Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/7bff2d6dbbd77421b3274aa51db1c887381cbc5b6eda93598c3e882ea345/regex-2026.6.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aa084684e6d2078bf6139e374d1fc2af5ddc1ac7122759a2db716d68169f6fd0", size = 911554, upload-time = "2026-06-28T19:55:53.707Z" }, + { url = "https://files.pythonhosted.org/packages/29/44/ae59c3826e7ba492e56795cdf74ea2a7b5b7c5ea116afb79ee4956a5dff1/regex-2026.6.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40455e6840dc4e96a6fe50f4cedc957de2752c954d91e789812be55d49be199a", size = 800665, upload-time = "2026-06-28T19:55:55.875Z" }, + { url = "https://files.pythonhosted.org/packages/d6/19/6fd033d2ab00f35d445aaeaf3307c1e721424dcbfd48f6f65c857cb939cf/regex-2026.6.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:530b5c223b9ca5dd8370ac502e080aee0e4ded32be987c6564b425fb5523d581", size = 777243, upload-time = "2026-06-28T19:55:57.909Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9d/99730f26df4938049ab1e652ca75e967b4c6739444e18d9707bfdb8af20c/regex-2026.6.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e0ed273ecd1a89be84466c1749bfe58609cc2a32b5d5e05006c4625ba96411b", size = 785784, upload-time = "2026-06-28T19:56:00.072Z" }, + { url = "https://files.pythonhosted.org/packages/48/49/105cd57162f5fc5c04cc917a1388a060cf8427e5c14353cd9044660fbf4d/regex-2026.6.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ab0d5344311fc8e8667078942056c3b9c9b4a4b1cc99f2eb8a5af54554f4acc", size = 860914, upload-time = "2026-06-28T19:56:02.017Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a5/788245a95b69018f58bff2f4fd27d007cacaea088cdb390979743f1b2571/regex-2026.6.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:eacb79625323d9f7e7925366b917f492b8356fad58f5dc4fa12ff8c21d8f4ca9", size = 765915, upload-time = "2026-06-28T19:56:05.021Z" }, + { url = "https://files.pythonhosted.org/packages/ca/01/292065a39a004b05e67a337b18213670a7cb919d6856ac2d7df7f1a10dbb/regex-2026.6.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20f4d87702702aa1d572721e146f301660c50eef6fd6cb596e48a22b0ace17db", size = 851404, upload-time = "2026-06-28T19:56:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/98/9e/a93d865db0e13483ae1a01d81e2ce16d4a7fe2f9b9fe4aac4cc08590b136/regex-2026.6.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e693940a3b9e6d6e4dc2a54ecaa74b74934f77af1ef95f518a74261ef7cc1bc", size = 789373, upload-time = "2026-06-28T19:56:09.894Z" }, + { url = "https://files.pythonhosted.org/packages/82/0c/38b1685ad4017d78efbc8fa7dbbf96d8113b53750c8aa2d3609defd46605/regex-2026.6.28-cp314-cp314-win32.whl", hash = "sha256:234a51e20ebc18ab83b2c0600cf28f2e884560a0e00f743878f0b7d8e7c4cf03", size = 272496, upload-time = "2026-06-28T19:56:11.83Z" }, + { url = "https://files.pythonhosted.org/packages/55/50/e19f261ff9ba9b50722a529e09b1743ecf65eb348be99d0fd2cd7fcede1c/regex-2026.6.28-cp314-cp314-win_amd64.whl", hash = "sha256:7b15c437bc4604f03ceb3f8d37eae2f8930e320e1bc556b259848c639d9eec1a", size = 280754, upload-time = "2026-06-28T19:56:13.758Z" }, + { url = "https://files.pythonhosted.org/packages/36/b8/c9e68f3a9e33be73f20990b2c065b144ff2d0aa242608a950d8c4f3b56e8/regex-2026.6.28-cp314-cp314-win_arm64.whl", hash = "sha256:c6e6f790d01380a74ad564f216c533b86504afb61bf66f2b2e11e7f1a3e287a7", size = 280979, upload-time = "2026-06-28T19:56:15.928Z" }, + { url = "https://files.pythonhosted.org/packages/03/e6/21c425a37880c650d007c4171c6a80325446d830d85f5fbf335e7205b1e7/regex-2026.6.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3527a72adcbe9e3600f1553b497d397c1a371d227580d41d96c3c5964109b65c", size = 494282, upload-time = "2026-06-28T19:56:18.049Z" }, + { url = "https://files.pythonhosted.org/packages/07/50/6647a7ccf5ffff995ba955a0b7d766440f4e58ce1666549c8ee998f2b972/regex-2026.6.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a644f6408692812f5ead82519eed680e08d5d546fddbd9f7d9514e3c73899aa5", size = 293977, upload-time = "2026-06-28T19:56:20.145Z" }, + { url = "https://files.pythonhosted.org/packages/8c/dc/a3e141a4eaf125e50f63105570c01fa477c06ac5259dcfa95e9b90760e84/regex-2026.6.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8e2fae6bb883648346f84db270dc9aafc29d8e895f62b88a75ccc83b09519820", size = 292432, upload-time = "2026-06-28T19:56:22.345Z" }, + { url = "https://files.pythonhosted.org/packages/35/ee/2ac1a6b9f167f8ff69f5a789938cc103b60cff41b24a6990daced8b88e34/regex-2026.6.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debe623e09cee97ef9404575e936c610aac9bb08358c5099aaef14644a6871f2", size = 811877, upload-time = "2026-06-28T19:56:25.056Z" }, + { url = "https://files.pythonhosted.org/packages/df/7b/9a5505ee92180bcae300b1018b9ff3d3c19962436e66f2505f255e9fde35/regex-2026.6.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc579c91fb4605773483a8d940b136bcc5b854fff44fa14a1572a038f46563f1", size = 871212, upload-time = "2026-06-28T19:56:27.352Z" }, + { url = "https://files.pythonhosted.org/packages/24/4d/d61a702a9f9d1bd29b22cbef1aed6d477baa961232a7eb4d91b7775b0b3e/regex-2026.6.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7c42be203d84ecf7d487ff23f8a61ef0eb0534fa0fc317a2fce8c065d20618f", size = 917507, upload-time = "2026-06-28T19:56:29.762Z" }, + { url = "https://files.pythonhosted.org/packages/d4/60/1308066f5966b65fbb6905b99ba37e9f1cd753dd0ac08485f8257334ee92/regex-2026.6.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8184b4e2fdaf9cdfe77e38f15a4d9dc149168c9c29eb0ea17c5481d3bb80546", size = 816389, upload-time = "2026-06-28T19:56:32.043Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5c/57ce2cb8d714ee0b7f11c7ee4cfe2af66df2b90f147feadcb538609a3a02/regex-2026.6.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:697f103104f5872d64078d8eeac59979960be8ee76115a2d3f31096312e2a400", size = 785890, upload-time = "2026-06-28T19:56:34.492Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fd/1d5350d3a8a327bff0fccacb911732baf7b5b6f5529c0e3fa602a23e7dad/regex-2026.6.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:714d2b1aa29beef0ddfcdc72ad0771c05326551a8bb0680b0ddf74bfaad87387", size = 801451, upload-time = "2026-06-28T19:56:36.749Z" }, + { url = "https://files.pythonhosted.org/packages/f3/79/3c9e4f8a0306e030ad5a43bbbc01625fb28d58a813bc52d42fd1cc63fb2e/regex-2026.6.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0f09f62e450cc2f113018cc8412aeea3a120a04e1ca7e801a0d441583f9a3b06", size = 866504, upload-time = "2026-06-28T19:56:38.994Z" }, + { url = "https://files.pythonhosted.org/packages/65/12/f747de475b54f4709efb24dd0fbc8467c64cec91f5db0d047b079646ee78/regex-2026.6.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:731ea12d5aeb2577eaef2393d6428b995f76eb35f68a89e03e15a97719d1de19", size = 773047, upload-time = "2026-06-28T19:56:41.061Z" }, + { url = "https://files.pythonhosted.org/packages/58/3c/f02f860e0500c1b2d61a79dec7e214b37fb9656281dcddc92397edf96678/regex-2026.6.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:51e952c8783eabd4706d0f63922f219bcfc1bef9b8cb35941c0d1a0396578858", size = 856665, upload-time = "2026-06-28T19:56:43.466Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6c/28b3fa222513484be9dee26b7222bda109056c43ea28aa2314262ca48816/regex-2026.6.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43248fe4c0ab8fbb223588a0795b11268940072c97bba30ea8f9b49d8cdfde34", size = 803573, upload-time = "2026-06-28T19:56:45.791Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/8f86cf1a1fd85c5ab0c503c9fe4607ad4ad48978b2d8b435d94465e134c7/regex-2026.6.28-cp314-cp314t-win32.whl", hash = "sha256:fc1eddc25ad23c0f1344ab280d961ac595ead48292d7c779497975942373f493", size = 274515, upload-time = "2026-06-28T19:56:47.948Z" }, + { url = "https://files.pythonhosted.org/packages/0f/de/f8613c03b36786ddef2c930d28f9bcae861fcd541cc9203a870956cf1e83/regex-2026.6.28-cp314-cp314t-win_amd64.whl", hash = "sha256:ede8d8e53b6dde0a50f7eca902f0af76d87ab02a55aba7542da68ae3e5dfe83d", size = 283650, upload-time = "2026-06-28T19:56:50.614Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f3/f5ec86839bbabe33b6dee649b62ff9a445d43de6b0ad780cf6b83c56f61e/regex-2026.6.28-cp314-cp314t-win_arm64.whl", hash = "sha256:4da6f6a72f8700b97a1a765e837fb7d5750bfd9f13acea7bae498f573e3a70a8", size = 283338, upload-time = "2026-06-28T19:56:52.879Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "yarl" +version = "1.24.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" }, + { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" }, + { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" }, + { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, + { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, + { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" }, + { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" }, + { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" }, + { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" }, + { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" }, + { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" }, + { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" }, + { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" }, + { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" }, + { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" }, + { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" }, + { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" }, + { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" }, + { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" }, + { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, +] diff --git a/dev/testing/microsoft-agents-testing/README.md b/dev/microsoft-agents-testing/README.md similarity index 100% rename from dev/testing/microsoft-agents-testing/README.md rename to dev/microsoft-agents-testing/README.md diff --git a/dev/testing/microsoft-agents-testing/docs/API.md b/dev/microsoft-agents-testing/docs/API.md similarity index 100% rename from dev/testing/microsoft-agents-testing/docs/API.md rename to dev/microsoft-agents-testing/docs/API.md diff --git a/dev/testing/microsoft-agents-testing/docs/MOTIVATION.md b/dev/microsoft-agents-testing/docs/MOTIVATION.md similarity index 100% rename from dev/testing/microsoft-agents-testing/docs/MOTIVATION.md rename to dev/microsoft-agents-testing/docs/MOTIVATION.md diff --git a/dev/testing/microsoft-agents-testing/docs/README.md b/dev/microsoft-agents-testing/docs/README.md similarity index 100% rename from dev/testing/microsoft-agents-testing/docs/README.md rename to dev/microsoft-agents-testing/docs/README.md diff --git a/dev/testing/microsoft-agents-testing/docs/SAMPLES.md b/dev/microsoft-agents-testing/docs/SAMPLES.md similarity index 100% rename from dev/testing/microsoft-agents-testing/docs/SAMPLES.md rename to dev/microsoft-agents-testing/docs/SAMPLES.md diff --git a/dev/testing/microsoft-agents-testing/docs/samples/__init__.py b/dev/microsoft-agents-testing/docs/samples/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/docs/samples/__init__.py rename to dev/microsoft-agents-testing/docs/samples/__init__.py diff --git a/dev/testing/microsoft-agents-testing/docs/samples/interactive.py b/dev/microsoft-agents-testing/docs/samples/interactive.py similarity index 100% rename from dev/testing/microsoft-agents-testing/docs/samples/interactive.py rename to dev/microsoft-agents-testing/docs/samples/interactive.py diff --git a/dev/testing/microsoft-agents-testing/docs/samples/multi_client.py b/dev/microsoft-agents-testing/docs/samples/multi_client.py similarity index 100% rename from dev/testing/microsoft-agents-testing/docs/samples/multi_client.py rename to dev/microsoft-agents-testing/docs/samples/multi_client.py diff --git a/dev/testing/microsoft-agents-testing/docs/samples/pytest_plugin_usage.py b/dev/microsoft-agents-testing/docs/samples/pytest_plugin_usage.py similarity index 100% rename from dev/testing/microsoft-agents-testing/docs/samples/pytest_plugin_usage.py rename to dev/microsoft-agents-testing/docs/samples/pytest_plugin_usage.py diff --git a/dev/testing/microsoft-agents-testing/docs/samples/quickstart.py b/dev/microsoft-agents-testing/docs/samples/quickstart.py similarity index 100% rename from dev/testing/microsoft-agents-testing/docs/samples/quickstart.py rename to dev/microsoft-agents-testing/docs/samples/quickstart.py diff --git a/dev/testing/microsoft-agents-testing/docs/samples/scenario_registry_demo.py b/dev/microsoft-agents-testing/docs/samples/scenario_registry_demo.py similarity index 100% rename from dev/testing/microsoft-agents-testing/docs/samples/scenario_registry_demo.py rename to dev/microsoft-agents-testing/docs/samples/scenario_registry_demo.py diff --git a/dev/testing/microsoft-agents-testing/docs/samples/test_motivation_assertions.py b/dev/microsoft-agents-testing/docs/samples/test_motivation_assertions.py similarity index 100% rename from dev/testing/microsoft-agents-testing/docs/samples/test_motivation_assertions.py rename to dev/microsoft-agents-testing/docs/samples/test_motivation_assertions.py diff --git a/dev/testing/microsoft-agents-testing/docs/samples/transcript_formatting.py b/dev/microsoft-agents-testing/docs/samples/transcript_formatting.py similarity index 100% rename from dev/testing/microsoft-agents-testing/docs/samples/transcript_formatting.py rename to dev/microsoft-agents-testing/docs/samples/transcript_formatting.py diff --git a/dev/testing/python-sdk-tests/env.TEMPLATE b/dev/microsoft-agents-testing/env.TEMPLATE similarity index 100% rename from dev/testing/python-sdk-tests/env.TEMPLATE rename to dev/microsoft-agents-testing/env.TEMPLATE diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/activity_handler_scenario.py b/dev/microsoft-agents-testing/microsoft_agents/testing/activity_handler_scenario.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/activity_handler_scenario.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/activity_handler_scenario.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/aiohttp_scenario.py b/dev/microsoft-agents-testing/microsoft_agents/testing/aiohttp_scenario.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/aiohttp_scenario.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/aiohttp_scenario.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/env.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/env.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/env.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/env.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/environment/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/environment/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/environment/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/environment/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/environment/_help.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/environment/_help.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/environment/_help.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/environment/_help.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/environment/_show.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/environment/_show.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/environment/_show.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/environment/_show.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/environment/env_group.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/environment/env_group.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/environment/env_group.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/environment/env_group.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/init.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/init.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/init.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/init.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_chat.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_chat.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_chat.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_chat.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_list.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_list.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_list.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_list.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_load.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_load.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_load.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_load.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_post.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_post.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_post.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_post.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_run.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_run.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_run.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_run.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_utils.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_utils.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_utils.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/_utils.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/scenario_group.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/scenario_group.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/scenario_group.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/commands/scenario/scenario_group.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/core/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/core/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/core/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/core/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/core/cli_config.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/core/cli_config.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/core/cli_config.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/core/cli_config.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/core/decorators.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/core/decorators.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/core/decorators.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/core/decorators.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/core/output.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/core/output.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/core/output.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/core/output.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/core/utils.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/core/utils.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/core/utils.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/core/utils.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/main.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/main.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/main.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/main.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/scenarios/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/scenarios/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/scenarios/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/scenarios/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/scenarios/auth_scenario.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/scenarios/auth_scenario.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/scenarios/auth_scenario.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/scenarios/auth_scenario.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/scenarios/basic_scenario.py b/dev/microsoft-agents-testing/microsoft_agents/testing/cli/scenarios/basic_scenario.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/cli/scenarios/basic_scenario.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/cli/scenarios/basic_scenario.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/constants.py b/dev/microsoft-agents-testing/microsoft_agents/testing/constants.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/constants.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/constants.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/_aiohttp_client_factory.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/_aiohttp_client_factory.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/_aiohttp_client_factory.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/_aiohttp_client_factory.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/agent_client.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/agent_client.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/agent_client.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/agent_client.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/config.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/config.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/config.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/config.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/external_scenario.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/external_scenario.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/external_scenario.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/external_scenario.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/describe.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/describe.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/describe.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/describe.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/model_predicate.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/model_predicate.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/model_predicate.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/model_predicate.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/quantifier.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/quantifier.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/quantifier.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/quantifier.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/transform.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/transform.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/transform.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/transform.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/types/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/types/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/types/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/types/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/types/readonly.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/types/readonly.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/types/readonly.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/types/readonly.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/types/safe_object.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/types/safe_object.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/types/safe_object.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/types/safe_object.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/types/unset.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/types/unset.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/types/unset.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/types/unset.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/utils.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/utils.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/utils.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/backend/utils.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/expect.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/expect.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/expect.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/expect.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/model_template.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/model_template.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/model_template.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/model_template.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/select.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/select.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/select.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/select.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/utils.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/utils.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/fluent/utils.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/fluent/utils.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/scenario.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/scenario.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/scenario.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/scenario.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/transport/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/transport/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/transport/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/transport/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/transport/aiohttp_callback_server.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/transport/aiohttp_callback_server.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/transport/aiohttp_callback_server.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/transport/aiohttp_callback_server.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/transport/aiohttp_sender.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/transport/aiohttp_sender.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/transport/aiohttp_sender.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/transport/aiohttp_sender.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/transport/callback_server.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/transport/callback_server.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/transport/callback_server.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/transport/callback_server.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/transport/sender.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/transport/sender.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/transport/sender.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/transport/sender.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/transport/transcript/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/transport/transcript/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/transport/transcript/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/transport/transcript/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/transport/transcript/exchange.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/transport/transcript/exchange.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/transport/transcript/exchange.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/transport/transcript/exchange.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/transport/transcript/transcript.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/transport/transcript/transcript.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/transport/transcript/transcript.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/transport/transcript/transcript.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/utils.py b/dev/microsoft-agents-testing/microsoft_agents/testing/core/utils.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/core/utils.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/core/utils.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/formatting/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/formatting/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/formatting/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/formatting/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/formatting/activity_transcript_formatter.py b/dev/microsoft-agents-testing/microsoft_agents/testing/formatting/activity_transcript_formatter.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/formatting/activity_transcript_formatter.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/formatting/activity_transcript_formatter.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/formatting/conversation_transcript_formatter.py b/dev/microsoft-agents-testing/microsoft_agents/testing/formatting/conversation_transcript_formatter.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/formatting/conversation_transcript_formatter.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/formatting/conversation_transcript_formatter.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/formatting/json_transcript_formatter.py b/dev/microsoft-agents-testing/microsoft_agents/testing/formatting/json_transcript_formatter.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/formatting/json_transcript_formatter.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/formatting/json_transcript_formatter.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/formatting/print.py b/dev/microsoft-agents-testing/microsoft_agents/testing/formatting/print.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/formatting/print.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/formatting/print.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/formatting/transcript_formatter.py b/dev/microsoft-agents-testing/microsoft_agents/testing/formatting/transcript_formatter.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/formatting/transcript_formatter.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/formatting/transcript_formatter.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/formatting/utils.py b/dev/microsoft-agents-testing/microsoft_agents/testing/formatting/utils.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/formatting/utils.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/formatting/utils.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/config.json b/dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/config.json similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/config.json rename to dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/config.json diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/env.TEMPLATE b/dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/env.TEMPLATE similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/env.TEMPLATE rename to dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/env.TEMPLATE diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/pyproject.toml b/dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/pyproject.toml similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/pyproject.toml rename to dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/pyproject.toml diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/pytest.ini b/dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/pytest.ini similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/pytest.ini rename to dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/pytest.ini diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/tests/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/tests/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/tests/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/tests/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/tests/test_my_agent.py b/dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/tests/test_my_agent.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/tests/test_my_agent.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/e2e-tests/tests/test_my_agent.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/env.TEMPLATE b/dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/env.TEMPLATE similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/env.TEMPLATE rename to dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/env.TEMPLATE diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/pyproject.toml b/dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/pyproject.toml similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/pyproject.toml rename to dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/pyproject.toml diff --git a/dev/testing/microsoft-agents-testing/tests/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/main.py b/dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/main.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/main.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/main.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/start_server.py b/dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/start_server.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/start_server.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/presets/basic/my_agent/src/start_server.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/config.json b/dev/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/config.json similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/config.json rename to dev/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/config.json diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/env.TEMPLATE b/dev/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/env.TEMPLATE similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/env.TEMPLATE rename to dev/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/env.TEMPLATE diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pyproject.toml b/dev/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pyproject.toml similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pyproject.toml rename to dev/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pyproject.toml diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pytest.ini b/dev/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pytest.ini similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pytest.ini rename to dev/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/pytest.ini diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/test_my_agent.py b/dev/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/test_my_agent.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/test_my_agent.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/presets/localhost/e2e-tests/tests/test_my_agent.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/pytest_plugin.py b/dev/microsoft-agents-testing/microsoft_agents/testing/pytest_plugin.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/pytest_plugin.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/pytest_plugin.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py b/dev/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/scenario_registry.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/source_scenario.py b/dev/microsoft-agents-testing/microsoft_agents/testing/source_scenario.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/source_scenario.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/source_scenario.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/utils/__init__.py b/dev/microsoft-agents-testing/microsoft_agents/testing/utils/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/utils/__init__.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/utils/__init__.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/utils/poll.py b/dev/microsoft-agents-testing/microsoft_agents/testing/utils/poll.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/utils/poll.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/utils/poll.py diff --git a/dev/testing/microsoft-agents-testing/microsoft_agents/testing/utils/send.py b/dev/microsoft-agents-testing/microsoft_agents/testing/utils/send.py similarity index 100% rename from dev/testing/microsoft-agents-testing/microsoft_agents/testing/utils/send.py rename to dev/microsoft-agents-testing/microsoft_agents/testing/utils/send.py diff --git a/dev/testing/microsoft-agents-testing/payload.json b/dev/microsoft-agents-testing/payload.json similarity index 100% rename from dev/testing/microsoft-agents-testing/payload.json rename to dev/microsoft-agents-testing/payload.json diff --git a/dev/testing/microsoft-agents-testing/pyproject.toml b/dev/microsoft-agents-testing/pyproject.toml similarity index 100% rename from dev/testing/microsoft-agents-testing/pyproject.toml rename to dev/microsoft-agents-testing/pyproject.toml diff --git a/dev/testing/microsoft-agents-testing/pytest.ini b/dev/microsoft-agents-testing/pytest.ini similarity index 100% rename from dev/testing/microsoft-agents-testing/pytest.ini rename to dev/microsoft-agents-testing/pytest.ini diff --git a/dev/testing/microsoft-agents-testing/tests/core/__init__.py b/dev/microsoft-agents-testing/tests/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/__init__.py rename to dev/microsoft-agents-testing/tests/__init__.py diff --git a/dev/testing/microsoft-agents-testing/tests/cli/test_cli_integration.py b/dev/microsoft-agents-testing/tests/cli/test_cli_integration.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/cli/test_cli_integration.py rename to dev/microsoft-agents-testing/tests/cli/test_cli_integration.py diff --git a/dev/testing/microsoft-agents-testing/tests/cli/test_output.py b/dev/microsoft-agents-testing/tests/cli/test_output.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/cli/test_output.py rename to dev/microsoft-agents-testing/tests/cli/test_output.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/fluent/__init__.py b/dev/microsoft-agents-testing/tests/core/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/fluent/__init__.py rename to dev/microsoft-agents-testing/tests/core/__init__.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/fluent/backend/__init__.py b/dev/microsoft-agents-testing/tests/core/fluent/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/fluent/backend/__init__.py rename to dev/microsoft-agents-testing/tests/core/fluent/__init__.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/fluent/backend/types/__init__.py b/dev/microsoft-agents-testing/tests/core/fluent/backend/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/fluent/backend/types/__init__.py rename to dev/microsoft-agents-testing/tests/core/fluent/backend/__init__.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/fluent/backend/test_describe.py b/dev/microsoft-agents-testing/tests/core/fluent/backend/test_describe.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/fluent/backend/test_describe.py rename to dev/microsoft-agents-testing/tests/core/fluent/backend/test_describe.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/fluent/backend/test_model_predicate.py b/dev/microsoft-agents-testing/tests/core/fluent/backend/test_model_predicate.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/fluent/backend/test_model_predicate.py rename to dev/microsoft-agents-testing/tests/core/fluent/backend/test_model_predicate.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/fluent/backend/test_quantifier.py b/dev/microsoft-agents-testing/tests/core/fluent/backend/test_quantifier.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/fluent/backend/test_quantifier.py rename to dev/microsoft-agents-testing/tests/core/fluent/backend/test_quantifier.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/fluent/backend/test_transform.py b/dev/microsoft-agents-testing/tests/core/fluent/backend/test_transform.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/fluent/backend/test_transform.py rename to dev/microsoft-agents-testing/tests/core/fluent/backend/test_transform.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/fluent/backend/test_utils.py b/dev/microsoft-agents-testing/tests/core/fluent/backend/test_utils.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/fluent/backend/test_utils.py rename to dev/microsoft-agents-testing/tests/core/fluent/backend/test_utils.py diff --git a/dev/testing/microsoft-agents-testing/tests/utils/__init__.py b/dev/microsoft-agents-testing/tests/core/fluent/backend/types/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/utils/__init__.py rename to dev/microsoft-agents-testing/tests/core/fluent/backend/types/__init__.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/fluent/backend/types/test_readonly.py b/dev/microsoft-agents-testing/tests/core/fluent/backend/types/test_readonly.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/fluent/backend/types/test_readonly.py rename to dev/microsoft-agents-testing/tests/core/fluent/backend/types/test_readonly.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/fluent/backend/types/test_unset.py b/dev/microsoft-agents-testing/tests/core/fluent/backend/types/test_unset.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/fluent/backend/types/test_unset.py rename to dev/microsoft-agents-testing/tests/core/fluent/backend/types/test_unset.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/fluent/test_expect.py b/dev/microsoft-agents-testing/tests/core/fluent/test_expect.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/fluent/test_expect.py rename to dev/microsoft-agents-testing/tests/core/fluent/test_expect.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/fluent/test_model_template.py b/dev/microsoft-agents-testing/tests/core/fluent/test_model_template.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/fluent/test_model_template.py rename to dev/microsoft-agents-testing/tests/core/fluent/test_model_template.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/fluent/test_select.py b/dev/microsoft-agents-testing/tests/core/fluent/test_select.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/fluent/test_select.py rename to dev/microsoft-agents-testing/tests/core/fluent/test_select.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/fluent/test_utils.py b/dev/microsoft-agents-testing/tests/core/fluent/test_utils.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/fluent/test_utils.py rename to dev/microsoft-agents-testing/tests/core/fluent/test_utils.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/test_agent_client.py b/dev/microsoft-agents-testing/tests/core/test_agent_client.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/test_agent_client.py rename to dev/microsoft-agents-testing/tests/core/test_agent_client.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/test_aiohttp_client_factory.py b/dev/microsoft-agents-testing/tests/core/test_aiohttp_client_factory.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/test_aiohttp_client_factory.py rename to dev/microsoft-agents-testing/tests/core/test_aiohttp_client_factory.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/test_config.py b/dev/microsoft-agents-testing/tests/core/test_config.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/test_config.py rename to dev/microsoft-agents-testing/tests/core/test_config.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/test_external_scenario.py b/dev/microsoft-agents-testing/tests/core/test_external_scenario.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/test_external_scenario.py rename to dev/microsoft-agents-testing/tests/core/test_external_scenario.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/test_integration.py b/dev/microsoft-agents-testing/tests/core/test_integration.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/test_integration.py rename to dev/microsoft-agents-testing/tests/core/test_integration.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/transport/__init__.py b/dev/microsoft-agents-testing/tests/core/transport/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/transport/__init__.py rename to dev/microsoft-agents-testing/tests/core/transport/__init__.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/transport/test_aiohttp_callback_server.py b/dev/microsoft-agents-testing/tests/core/transport/test_aiohttp_callback_server.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/transport/test_aiohttp_callback_server.py rename to dev/microsoft-agents-testing/tests/core/transport/test_aiohttp_callback_server.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/transport/test_aiohttp_sender.py b/dev/microsoft-agents-testing/tests/core/transport/test_aiohttp_sender.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/transport/test_aiohttp_sender.py rename to dev/microsoft-agents-testing/tests/core/transport/test_aiohttp_sender.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/transport/transcript/__init__.py b/dev/microsoft-agents-testing/tests/core/transport/transcript/__init__.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/transport/transcript/__init__.py rename to dev/microsoft-agents-testing/tests/core/transport/transcript/__init__.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/transport/transcript/test_exchange.py b/dev/microsoft-agents-testing/tests/core/transport/transcript/test_exchange.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/transport/transcript/test_exchange.py rename to dev/microsoft-agents-testing/tests/core/transport/transcript/test_exchange.py diff --git a/dev/testing/microsoft-agents-testing/tests/core/transport/transcript/test_transcript.py b/dev/microsoft-agents-testing/tests/core/transport/transcript/test_transcript.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/core/transport/transcript/test_transcript.py rename to dev/microsoft-agents-testing/tests/core/transport/transcript/test_transcript.py diff --git a/dev/testing/microsoft-agents-testing/tests/manual.py b/dev/microsoft-agents-testing/tests/manual.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/manual.py rename to dev/microsoft-agents-testing/tests/manual.py diff --git a/dev/testing/microsoft-agents-testing/tests/test_aiohttp_scenario.py b/dev/microsoft-agents-testing/tests/test_aiohttp_scenario.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/test_aiohttp_scenario.py rename to dev/microsoft-agents-testing/tests/test_aiohttp_scenario.py diff --git a/dev/testing/microsoft-agents-testing/tests/test_aiohttp_scenario_integration.py b/dev/microsoft-agents-testing/tests/test_aiohttp_scenario_integration.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/test_aiohttp_scenario_integration.py rename to dev/microsoft-agents-testing/tests/test_aiohttp_scenario_integration.py diff --git a/dev/testing/microsoft-agents-testing/tests/test_examples.py b/dev/microsoft-agents-testing/tests/test_examples.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/test_examples.py rename to dev/microsoft-agents-testing/tests/test_examples.py diff --git a/dev/testing/microsoft-agents-testing/tests/test_pytest_plugin.py b/dev/microsoft-agents-testing/tests/test_pytest_plugin.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/test_pytest_plugin.py rename to dev/microsoft-agents-testing/tests/test_pytest_plugin.py diff --git a/dev/testing/microsoft-agents-testing/tests/test_scenario_registry.py b/dev/microsoft-agents-testing/tests/test_scenario_registry.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/test_scenario_registry.py rename to dev/microsoft-agents-testing/tests/test_scenario_registry.py diff --git a/dev/testing/microsoft-agents-testing/tests/test_scenario_registry_plugin.py b/dev/microsoft-agents-testing/tests/test_scenario_registry_plugin.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/test_scenario_registry_plugin.py rename to dev/microsoft-agents-testing/tests/test_scenario_registry_plugin.py diff --git a/dev/testing/microsoft-agents-testing/tests/test_source_scenario.py b/dev/microsoft-agents-testing/tests/test_source_scenario.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/test_source_scenario.py rename to dev/microsoft-agents-testing/tests/test_source_scenario.py diff --git a/dev/testing/microsoft-agents-testing/tests/test_transcript_formatter.py b/dev/microsoft-agents-testing/tests/test_transcript_formatter.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/test_transcript_formatter.py rename to dev/microsoft-agents-testing/tests/test_transcript_formatter.py diff --git a/dev/testing/python-sdk-tests/__init__.py b/dev/microsoft-agents-testing/tests/utils/__init__.py similarity index 100% rename from dev/testing/python-sdk-tests/__init__.py rename to dev/microsoft-agents-testing/tests/utils/__init__.py diff --git a/dev/testing/microsoft-agents-testing/tests/utils/test_poll.py b/dev/microsoft-agents-testing/tests/utils/test_poll.py similarity index 100% rename from dev/testing/microsoft-agents-testing/tests/utils/test_poll.py rename to dev/microsoft-agents-testing/tests/utils/test_poll.py diff --git a/dev/testing/microsoft-agents-testing/uv.lock b/dev/microsoft-agents-testing/uv.lock similarity index 100% rename from dev/testing/microsoft-agents-testing/uv.lock rename to dev/microsoft-agents-testing/uv.lock diff --git a/dev/requirements.txt b/dev/requirements.txt deleted file mode 100644 index 544778fb1..000000000 --- a/dev/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -microsoft-agents-activity -microsoft-agents-hosting-core -pytest -pytest-asyncio -pytest-mock -aiohttp -requests -pydantic -python-dotenv -pytest-aiohttp -click \ No newline at end of file diff --git a/dev/testing/README.md b/dev/testing/README.md deleted file mode 100644 index ff0ee91a3..000000000 --- a/dev/testing/README.md +++ /dev/null @@ -1,13 +0,0 @@ -## Testing - -This folder contains three test-related directories: - -`cross-sdk-tests`: End-to-end tests across all SDKs (Python, JavaScript, .NET). This is a work in progress. - -`microsoft-agents-testing`: This is the testing framework used to facilitate testing agents. This is only for internal development purposes. - -`python-sdk-tests`: These are integration tests related to the Python SDK. These are an extension of the Python SDK's unit tests. These tests are more specific that the ones in `cross-sdk-tests` because they look into the internals of Python SDK components for the running agents while the other test suite communicates purely over HTTP/HTTPS. - -## Running tests and installation - -The instructions to install the `microsoft-agents-testing` library are specificed in `microsoft-agents-testing/README.md`. To run the `python-sdk-tests`, `cd` into that directory and run `pytest` via Powershell. `cross-sdk-tests` still does not have an entry point for testing. \ No newline at end of file diff --git a/dev/testing/cross-sdk-tests/Program.cs b/dev/testing/cross-sdk-tests/Program.cs deleted file mode 100644 index a685ba668..000000000 --- a/dev/testing/cross-sdk-tests/Program.cs +++ /dev/null @@ -1 +0,0 @@ -// to avoid CodeQL problem \ No newline at end of file diff --git a/dev/testing/python-sdk-tests/README.md b/dev/testing/python-sdk-tests/README.md deleted file mode 100644 index 3a3a5f2ff..000000000 --- a/dev/testing/python-sdk-tests/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Python SDK (Integration) Tests - -## Description - -This directory contains integration tests for the Python SDK. - -## Directory structure - -`integration`: general-purpose tests that run with automated, locally-running agents -`scenarios`: agent scenarios used for testing -`sdk`: integration for specific SDK components. This directory mirrors the structure of the Python SDK libraries. \ No newline at end of file diff --git a/dev/testing/python-sdk-tests/run_tests.ps1 b/dev/testing/python-sdk-tests/run_tests.ps1 deleted file mode 100644 index e35a25315..000000000 --- a/dev/testing/python-sdk-tests/run_tests.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -Get-ChildItem -Path tests -Filter test_*.py -Recurse | ForEach-Object { - pytest $_.FullName -} \ No newline at end of file diff --git a/dev/testing/python-sdk-tests/tests/activity_handler/__init__.py b/dev/testing/python-sdk-tests/tests/activity_handler/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/dev/testing/python-sdk-tests/tests/activity_handler/dialogs/__init__.py b/dev/testing/python-sdk-tests/tests/activity_handler/dialogs/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/dev/testing/python-sdk-tests/tests/activity_handler/dialogs/sample/__init__.py b/dev/testing/python-sdk-tests/tests/activity_handler/dialogs/sample/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/dev/testing/python-sdk-tests/tests/integration/__init__.py b/dev/testing/python-sdk-tests/tests/integration/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/dev/testing/python-sdk-tests/tests/__init__.py b/dev/tmp/Program.cs similarity index 100% rename from dev/testing/python-sdk-tests/tests/__init__.py rename to dev/tmp/Program.cs