From a48a27b47913aeb1a24832154c0e0986da765361 Mon Sep 17 00:00:00 2001 From: Aamir Jawaid <48929123+heyitsaamir@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:44:21 -0700 Subject: [PATCH 01/17] fix: ensure copilot agent ends with the action plan (#353) - The post-processing script in the issue analysis workflow extracts the last block of plain text from copilot output - Without explicit instruction, the agent sometimes ends with meta-commentary (e.g. "My action plan above is complete") instead of the actual plan - Added a prompt instruction requiring the agent's final message to be the complete action plan in markdown --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .github/workflows/issue-analysis.yml | 31 ++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/.github/workflows/issue-analysis.yml b/.github/workflows/issue-analysis.yml index ec7d91ad5..ca15a46fe 100644 --- a/.github/workflows/issue-analysis.yml +++ b/.github/workflows/issue-analysis.yml @@ -105,13 +105,32 @@ jobs: Issue URL: ${ISSUE_URL} - Read the codebase to understand the architecture, then provide a concrete action plan in markdown: - 1. **Root cause** — What's likely going wrong or what's missing - 2. **Files to investigate** — Specific file paths to look at (use actual paths you found) - 3. **Proposed approach** — Step-by-step what a developer should do to resolve this - 4. **Estimated complexity** — Small (< 1 day), Medium (1-3 days), or Large (3+ days) + Follow this workflow: + 1. Parse the issue for concrete signals — keywords, error messages, stack traces, config names, package references. + 2. Use those signals to search the codebase (grep, glob, read files). Trace execution paths to identify failure points or missing functionality. + 3. Do not claim confirmation without code-based evidence. - Be specific. Reference actual files and code you found in the repo." \ + Your FINAL message must be the complete analysis in this markdown structure: + + ## Issue Summary + One-paragraph summary of what the issue is reporting or requesting. + + ## Evidence + Concrete file pointers with line references and what you found. Quote relevant code snippets. + + ## Root-Cause Hypothesis + State your hypothesis with a confidence level (high/medium/low). Explain what evidence supports it and what is uncertain. + + ## Proposed Fix + Step-by-step what a developer should do to resolve this. Reference specific files and functions. + + ## Estimated Complexity + Small (< 1 day), Medium (1-3 days), or Large (3+ days) — with justification. + + ## Open Questions + Anything you could not confirm or that needs clarification from the issue author. + + Do NOT end with a summary or meta-commentary — end with the structured analysis itself." \ --allow-tool='shell(git:*)' \ --allow-tool='read' \ --allow-tool='glob' \ From 3ece4ba3edee43c38f4feecfc848d3a062fe0925 Mon Sep 17 00:00:00 2001 From: Lily Du Date: Fri, 3 Apr 2026 15:01:03 -0700 Subject: [PATCH 02/17] feat/bug: add custom feedback form & fix pydantic valid error on MessageUpdateActivity (#349) resolves: #326 and #347 - added default empty string for `text` in `MessageUpdateActivity` (otherwise throws pydantic error) - added support for custom feedback form (through `ChannelData.feedback_loop`). **Custom Feedback Context:** [Learn Doc](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bot-messages-ai-generated-content?tabs=desktop%2Cjs%2Cbotmessage#feedback-buttons) - previously we had a flag called `enable_feedback_loop` which **only** enables the default feedback form - now, we have a param called `feedback_loop` that can either be default or custom. - however, the service doesn't accept both at once. Hence I normalized `enable_feedback_loop` into `feedback_loop`. whenever `enable_feedback_loop` is set to True, it'll set `feedback_loop` to default and itself back to `None`. image **Example Usage:** ``` @app.on_message async def on_message(ctx: ActivityContext[MessageActivity]): if ctx.activity.text == "feedback default": # This is the legacy approach that normalizes. Recommended to use `feedback_loop` instead. await ctx.send( MessageActivityInput(text="Rate this response!").with_channel_data(ChannelData(feedback_loop_enabled=True)) ) elif ctx.activity.text == "feedback custom": await ctx.send( MessageActivityInput(text="Rate this response!").add_feedback( mode="custom" ) # triggers message/fetchTask invoke ) @app.on_message_fetch_task async def on_feedback_fetch_task(ctx: ActivityContext[MessageFetchTaskInvokeActivity]): card = AdaptiveCard.model_validate( { "type": "AdaptiveCard", "version": "1.4", "body": [ {"type": "TextBlock", "text": "Tell us more about your feedback:"}, { "type": "Input.Text", "id": "feedbackText", "placeholder": "Enter your feedback here...", "isMultiline": True, }, ], "actions": [ {"type": "Action.Submit", "title": "Submit"}, ], } ) return TaskModuleInvokeResponse( task=TaskModuleContinueResponse( value=CardTaskModuleTaskInfo( title="Feedback", card=card_attachment(AdaptiveCardAttachment(content=card)), ) ) ) ``` --------- Co-authored-by: lilydu Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../api/activities/invoke/__init__.py | 13 ++- .../api/activities/invoke/message/__init__.py | 14 ++- .../activities/invoke/message/fetch_task.py | 49 ++++++++++ .../api/activities/message/message_update.py | 2 +- .../microsoft_teams/api/models/activity.py | 21 ++-- .../api/models/channel_data/__init__.py | 11 ++- .../api/models/channel_data/channel_data.py | 29 +++++- .../api/models/channel_data/feedback_loop.py | 15 +++ packages/api/tests/unit/test_activity.py | 12 ++- .../api/tests/unit/test_message_activities.py | 37 ++++++- .../apps/routing/activity_route_configs.py | 12 +++ .../apps/routing/generated_handlers.py | 34 +++++++ packages/apps/tests/test_feedback_routing.py | 98 +++++++++++++++++++ 13 files changed, 332 insertions(+), 15 deletions(-) create mode 100644 packages/api/src/microsoft_teams/api/activities/invoke/message/fetch_task.py create mode 100644 packages/api/src/microsoft_teams/api/models/channel_data/feedback_loop.py create mode 100644 packages/apps/tests/test_feedback_routing.py diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/__init__.py b/packages/api/src/microsoft_teams/api/activities/invoke/__init__.py index 4b647d987..14a87969a 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/__init__.py @@ -14,7 +14,13 @@ from .execute_action import ExecuteActionInvokeActivity from .file_consent import FileConsentInvokeActivity from .handoff_action import HandoffActionInvokeActivity -from .message import MessageSubmitActionInvokeActivity +from .message import ( + MessageFetchTaskActionValue, + MessageFetchTaskData, + MessageFetchTaskInvokeActivity, + MessageFetchTaskInvokeValue, + MessageSubmitActionInvokeActivity, +) from .message_extension import * # noqa: F403 from .message_extension import MessageExtensionInvokeActivity from .sign_in import * # noqa: F403 @@ -32,6 +38,7 @@ ConfigInvokeActivity, TabInvokeActivity, TaskInvokeActivity, + MessageFetchTaskInvokeActivity, MessageSubmitActionInvokeActivity, HandoffActionInvokeActivity, SignInInvokeActivity, @@ -48,6 +55,10 @@ "ConfigInvokeActivity", "TabInvokeActivity", "TaskInvokeActivity", + "MessageFetchTaskActionValue", + "MessageFetchTaskData", + "MessageFetchTaskInvokeActivity", + "MessageFetchTaskInvokeValue", "MessageSubmitActionInvokeActivity", "HandoffActionInvokeActivity", "SignInInvokeActivity", diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/message/__init__.py b/packages/api/src/microsoft_teams/api/activities/invoke/message/__init__.py index a2bbc3e36..53c1d3c5a 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/message/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/message/__init__.py @@ -3,6 +3,18 @@ Licensed under the MIT License. """ +from .fetch_task import ( + MessageFetchTaskActionValue, + MessageFetchTaskData, + MessageFetchTaskInvokeActivity, + MessageFetchTaskInvokeValue, +) from .submit_action import MessageSubmitActionInvokeActivity -__all__ = ["MessageSubmitActionInvokeActivity"] +__all__ = [ + "MessageFetchTaskActionValue", + "MessageFetchTaskData", + "MessageFetchTaskInvokeActivity", + "MessageFetchTaskInvokeValue", + "MessageSubmitActionInvokeActivity", +] diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/message/fetch_task.py b/packages/api/src/microsoft_teams/api/activities/invoke/message/fetch_task.py new file mode 100644 index 000000000..52b8596ce --- /dev/null +++ b/packages/api/src/microsoft_teams/api/activities/invoke/message/fetch_task.py @@ -0,0 +1,49 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Literal + +from ....models import CustomBaseModel +from ...invoke_activity import InvokeActivity + + +class MessageFetchTaskActionValue(CustomBaseModel): + """The nested action value containing the user's reaction.""" + + reaction: Literal["like", "dislike"] + """The feedback button the user clicked.""" + + +class MessageFetchTaskData(CustomBaseModel): + """The data payload nested inside the fetch task value.""" + + action_name: Literal["feedback"] = "feedback" + """The name of the action.""" + + action_value: MessageFetchTaskActionValue + """Contains the user's reaction.""" + + +class MessageFetchTaskInvokeValue(CustomBaseModel): + """ + Represents the value associated with a message fetch task. + """ + + data: MessageFetchTaskData + """The data payload containing action name and value.""" + + +class MessageFetchTaskInvokeActivity(InvokeActivity): + """ + Represents an activity sent when a message has a custom feedback loop + and the user clicks a feedback button. + The bot should respond with a task module (dialog) to collect feedback. + """ + + name: Literal["message/fetchTask"] = "message/fetchTask" + """The name of the operation associated with an invoke or event activity.""" + + value: MessageFetchTaskInvokeValue + """The value associated with the activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/message/message_update.py b/packages/api/src/microsoft_teams/api/activities/message/message_update.py index 78f6ab80e..cb342e9d0 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/message_update.py +++ b/packages/api/src/microsoft_teams/api/activities/message/message_update.py @@ -49,7 +49,7 @@ class _MessageUpdateBase(CustomBaseModel): class MessageUpdateActivity(_MessageUpdateBase, ActivityBase): """Output model for received message update activities with required fields and read-only properties.""" - text: str # pyright: ignore [reportGeneralTypeIssues] + text: str = "" # pyright: ignore [reportGeneralTypeIssues, reportIncompatibleVariableOverride] """The text content of the message.""" channel_data: MessageUpdateChannelData # pyright: ignore [reportGeneralTypeIssues] diff --git a/packages/api/src/microsoft_teams/api/models/activity.py b/packages/api/src/microsoft_teams/api/models/activity.py index 68db57bda..cf957a01c 100644 --- a/packages/api/src/microsoft_teams/api/models/activity.py +++ b/packages/api/src/microsoft_teams/api/models/activity.py @@ -5,10 +5,10 @@ import warnings from datetime import datetime -from typing import Any, List, Optional, Self +from typing import Any, List, Literal, Optional, Self from microsoft_teams.api.models.account import Account, ConversationAccount -from microsoft_teams.api.models.channel_data.channel_data import ChannelData +from microsoft_teams.api.models.channel_data.channel_data import ChannelData, FeedbackLoop from microsoft_teams.api.models.channel_data.channel_info import ChannelInfo from microsoft_teams.api.models.channel_data.notification_info import NotificationInfo from microsoft_teams.api.models.channel_data.team_info import TeamInfo @@ -229,12 +229,19 @@ def add_ai_generated(self) -> Self: return self - def add_feedback(self) -> Self: - """Enable message feedback.""" + def add_feedback(self, mode: Literal["default", "custom"] = "default") -> Self: + """ + Enable message feedback. + + Args: + mode: "default" shows Teams' built-in thumbs up/down UI. + "custom" triggers a message/fetchTask invoke so the bot + can return its own task module dialog. + """ if not self.channel_data: - self.channel_data = ChannelData(feedback_loop_enabled=True) - else: - self.channel_data.feedback_loop_enabled = True + self.channel_data = ChannelData() + self.channel_data.feedback_loop = FeedbackLoop(type=mode) + self.channel_data.feedback_loop_enabled = None return self def add_citation(self, position: int, appearance: CitationAppearance) -> Self: diff --git a/packages/api/src/microsoft_teams/api/models/channel_data/__init__.py b/packages/api/src/microsoft_teams/api/models/channel_data/__init__.py index dd960c93a..3f808b278 100644 --- a/packages/api/src/microsoft_teams/api/models/channel_data/__init__.py +++ b/packages/api/src/microsoft_teams/api/models/channel_data/__init__.py @@ -5,9 +5,18 @@ from .channel_data import ChannelData from .channel_info import ChannelInfo +from .feedback_loop import FeedbackLoop from .notification_info import NotificationInfo from .settings import ChannelDataSettings from .team_info import TeamInfo from .tenant_info import TenantInfo -__all__ = ["ChannelInfo", "NotificationInfo", "ChannelDataSettings", "TeamInfo", "TenantInfo", "ChannelData"] +__all__ = [ + "ChannelInfo", + "NotificationInfo", + "ChannelDataSettings", + "TeamInfo", + "TenantInfo", + "ChannelData", + "FeedbackLoop", +] diff --git a/packages/api/src/microsoft_teams/api/models/channel_data/channel_data.py b/packages/api/src/microsoft_teams/api/models/channel_data/channel_data.py index 216d56e02..171ad49c6 100644 --- a/packages/api/src/microsoft_teams/api/models/channel_data/channel_data.py +++ b/packages/api/src/microsoft_teams/api/models/channel_data/channel_data.py @@ -3,11 +3,14 @@ Licensed under the MIT License. """ -from typing import Literal, Optional +from typing import Literal, Optional, Self + +from pydantic import model_validator from ..custom_base_model import CustomBaseModel from ..meetings import MeetingInfo from .channel_info import ChannelInfo +from .feedback_loop import FeedbackLoop from .notification_info import NotificationInfo from .settings import ChannelDataSettings from .team_info import TeamInfo @@ -41,7 +44,29 @@ class ChannelData(CustomBaseModel): "Information about the settings in which the message was sent." feedback_loop_enabled: Optional[bool] = None - "Whether or not the feedback loop feature is enabled." + """ + Legacy feedback loop flag. Setting this to True is equivalent to feedback_loop=FeedbackLoop(type="default"). + Recommended to use feedback_loop directly. This field is normalized on model creation. + """ + + feedback_loop: Optional[FeedbackLoop] = None + """ + Feedback loop configuration. Set type to 'custom' to show a task module dialog. + Set to 'default' otherwise for standard feedback handling. + """ + + @model_validator(mode="after") + def normalize_feedback(self) -> Self: + """ + Normalize the feedback loop configuration. + This is necessary as the client only accepts either/or. + """ + if self.feedback_loop is not None: + self.feedback_loop_enabled = None + elif self.feedback_loop_enabled is True: + self.feedback_loop = FeedbackLoop(type="default") + self.feedback_loop_enabled = None + return self stream_id: Optional[str] = None "ID of the stream. Assigned after the initial update is sent." diff --git a/packages/api/src/microsoft_teams/api/models/channel_data/feedback_loop.py b/packages/api/src/microsoft_teams/api/models/channel_data/feedback_loop.py new file mode 100644 index 000000000..029749168 --- /dev/null +++ b/packages/api/src/microsoft_teams/api/models/channel_data/feedback_loop.py @@ -0,0 +1,15 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Literal + +from ..custom_base_model import CustomBaseModel + + +class FeedbackLoop(CustomBaseModel): + """Configuration for a custom feedback loop on a message.""" + + type: Literal["custom", "default"] = "default" + """The type of feedback loop. Use `custom` to show a task module dialog""" diff --git a/packages/api/tests/unit/test_activity.py b/packages/api/tests/unit/test_activity.py index 0d643f0ac..675a33a7d 100644 --- a/packages/api/tests/unit/test_activity.py +++ b/packages/api/tests/unit/test_activity.py @@ -124,7 +124,17 @@ def test_should_add_feedback_label(self, test_activity: ConcreteTestActivity) -> activity = test_activity.add_feedback() assert activity.type == "test" - assert activity.channel_data and activity.channel_data.feedback_loop_enabled is True + assert activity.channel_data and activity.channel_data.feedback_loop is not None + assert activity.channel_data.feedback_loop.type == "default" + assert activity.channel_data.feedback_loop_enabled is None + + def test_should_add_custom_feedback_label(self, test_activity: ConcreteTestActivity) -> None: + activity = test_activity.add_feedback(mode="custom") + + assert activity.type == "test" + assert activity.channel_data and activity.channel_data.feedback_loop is not None + assert activity.channel_data.feedback_loop.type == "custom" + assert activity.channel_data.feedback_loop_enabled is None def test_should_add_citation(self, test_activity: ConcreteTestActivity) -> None: activity = test_activity.add_citation(0, CitationAppearance(abstract="test", name="test")) diff --git a/packages/api/tests/unit/test_message_activities.py b/packages/api/tests/unit/test_message_activities.py index cf09f16a4..5ee803f05 100644 --- a/packages/api/tests/unit/test_message_activities.py +++ b/packages/api/tests/unit/test_message_activities.py @@ -7,6 +7,7 @@ from datetime import datetime from typing import cast +from microsoft_teams.api.activities import ActivityTypeAdapter from microsoft_teams.api.activities.message import ( MessageActivity, MessageActivityInput, @@ -259,7 +260,7 @@ def test_is_recipient_mentioned_false(self): activity.recipient = recipient # Mention someone else - other_account = Account(id="user-456", name="User", role="user") + other_account = Account(id="user-456", name="User") mention = MentionEntity(mentioned=other_account, text="User") activity.entities = [mention] @@ -511,6 +512,40 @@ def test_message_update_activity_creation_undelete(self): assert activity.type == "messageUpdate" assert activity.channel_data.event_type == "undeleteMessage" + def test_message_update_activity_text_defaults_to_empty_string(self): + """Test that text field defaults to empty string when absent (e.g. attachment-only update)""" + from_account = Account(id="bot-123", name="Test Bot") + recipient = Account(id="user-456", name="Test User") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + channel_data = MessageUpdateChannelData(event_type="editMessage") + + activity = MessageUpdateActivity( + id="update-no-text", + channel_data=channel_data, + from_=from_account, + conversation=conversation, + recipient=recipient, + ) + + assert activity.text == "" + + def test_inbound_message_update_without_text_does_not_throw(self): + """Test that an inbound messageUpdate payload with no text (attachment-only) parses without error""" + + payload = { + "type": "messageUpdate", + "id": "msg-123", + "from": {"id": "user-123", "name": "Test User"}, + "conversation": {"id": "conv-456", "conversationType": "personal"}, + "recipient": {"id": "bot-789", "name": "Test Bot"}, + "channelData": {"eventType": "editMessage"}, + } + + activity = ActivityTypeAdapter.validate_python(payload) + + assert isinstance(activity, MessageUpdateActivity) + assert activity.text == "" + def test_message_update_activity_optional_fields(self): """Test message update activity with optional fields""" from_account = Account(id="bot-123", name="Test Bot") diff --git a/packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py b/packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py index fe13c3c49..cb9f93629 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py +++ b/packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py @@ -37,6 +37,7 @@ MessageExtensionSelectItemInvokeActivity, MessageExtensionSettingInvokeActivity, MessageExtensionSubmitActionInvokeActivity, + MessageFetchTaskInvokeActivity, MessageReactionActivity, MessageSubmitActionInvokeActivity, MessageUpdateActivity, @@ -49,6 +50,7 @@ TabFetchInvokeActivity, TabInvokeResponse, TabSubmitInvokeActivity, + TaskModuleInvokeResponse, TraceActivity, TypingActivity, UninstalledActivity, @@ -451,6 +453,16 @@ class ActivityConfig: output_type_name="TabInvokeResponse", is_invoke=True, ), + "message.fetch-task": ActivityConfig( + name="message.fetch-task", + method_name="on_message_fetch_task", + input_model=MessageFetchTaskInvokeActivity, + selector=lambda activity: activity.type == "invoke" + and cast(InvokeActivity, activity).name == "message/fetchTask", + output_model=TaskModuleInvokeResponse, + output_type_name="TaskModuleInvokeResponse", + is_invoke=True, + ), "message.submit": ActivityConfig( name="message.submit", method_name="on_message_submit", diff --git a/packages/apps/src/microsoft_teams/apps/routing/generated_handlers.py b/packages/apps/src/microsoft_teams/apps/routing/generated_handlers.py index 803fc8aa8..b6b179770 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/generated_handlers.py +++ b/packages/apps/src/microsoft_teams/apps/routing/generated_handlers.py @@ -43,6 +43,7 @@ MessageExtensionSelectItemInvokeActivity, MessageExtensionSettingInvokeActivity, MessageExtensionSubmitActionInvokeActivity, + MessageFetchTaskInvokeActivity, MessageReactionActivity, MessageSubmitActionInvokeActivity, MessageUpdateActivity, @@ -62,6 +63,7 @@ MessagingExtensionActionInvokeResponse, MessagingExtensionInvokeResponse, TabInvokeResponse, + TaskModuleInvokeResponse, TokenExchangeInvokeResponseType, ) @@ -1251,6 +1253,38 @@ def decorator( return decorator(handler) return decorator + @overload + def on_message_fetch_task( + self, handler: InvokeHandler[MessageFetchTaskInvokeActivity, TaskModuleInvokeResponse] + ) -> InvokeHandler[MessageFetchTaskInvokeActivity, TaskModuleInvokeResponse]: ... + + @overload + def on_message_fetch_task( + self, + ) -> Callable[ + [InvokeHandler[MessageFetchTaskInvokeActivity, TaskModuleInvokeResponse]], + InvokeHandler[MessageFetchTaskInvokeActivity, TaskModuleInvokeResponse], + ]: ... + + def on_message_fetch_task( + self, handler: Optional[InvokeHandler[MessageFetchTaskInvokeActivity, TaskModuleInvokeResponse]] = None + ) -> InvokeHandlerUnion[MessageFetchTaskInvokeActivity, TaskModuleInvokeResponse]: + """Register a message.fetch-task activity handler.""" + + def decorator( + func: InvokeHandler[MessageFetchTaskInvokeActivity, TaskModuleInvokeResponse], + ) -> InvokeHandler[MessageFetchTaskInvokeActivity, TaskModuleInvokeResponse]: + validate_handler_type( + func, MessageFetchTaskInvokeActivity, "on_message_fetch_task", "MessageFetchTaskInvokeActivity" + ) + config = ACTIVITY_ROUTES["message.fetch-task"] + self.router.add_handler(config.selector, func) + return func + + if handler is not None: + return decorator(handler) + return decorator + @overload def on_message_submit( self, handler: VoidInvokeHandler[MessageSubmitActionInvokeActivity] diff --git a/packages/apps/tests/test_feedback_routing.py b/packages/apps/tests/test_feedback_routing.py new file mode 100644 index 000000000..c691f7630 --- /dev/null +++ b/packages/apps/tests/test_feedback_routing.py @@ -0,0 +1,98 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +# pyright: basic + +from unittest.mock import MagicMock + +import pytest +from microsoft_teams.api import ( + Account, + ConversationAccount, + MessageFetchTaskActionValue, + MessageFetchTaskData, + MessageFetchTaskInvokeActivity, + MessageFetchTaskInvokeValue, + TaskFetchInvokeActivity, + TaskModuleInvokeResponse, + TaskModuleMessageResponse, + TaskModuleRequest, +) +from microsoft_teams.apps import ActivityContext, App + + +class TestFeedbackRouting: + """Test cases for custom feedback routing functionality.""" + + @pytest.fixture + def app(self): + return App(storage=MagicMock(), client_id="test-client-id", client_secret="test-secret") + + @pytest.fixture + def fetch_task_activity(self): + return MessageFetchTaskInvokeActivity( + id="activity-1", + type="invoke", + name="message/fetchTask", + from_=Account(id="user-1", name="User"), + recipient=Account(id="bot-1", name="Bot"), + conversation=ConversationAccount(id="conv-1", conversation_type="personal"), + channel_id="msteams", + value=MessageFetchTaskInvokeValue( + data=MessageFetchTaskData(action_value=MessageFetchTaskActionValue(reaction="like")) + ), + ) + + def test_on_message_fetch_task_registers_handler( + self, app: App, fetch_task_activity: MessageFetchTaskInvokeActivity + ) -> None: + @app.on_message_fetch_task + async def handler(ctx: ActivityContext[MessageFetchTaskInvokeActivity]) -> TaskModuleInvokeResponse: + return TaskModuleInvokeResponse(task=TaskModuleMessageResponse(value="feedback form")) + + handlers = app.router.select_handlers(fetch_task_activity) + assert len(handlers) == 1 + assert handlers[0] == handler + + def test_on_message_fetch_task_does_not_match_other_invokes(self, app: App) -> None: + @app.on_message_fetch_task + async def handler(ctx: ActivityContext[MessageFetchTaskInvokeActivity]) -> TaskModuleInvokeResponse: + return TaskModuleInvokeResponse(task=TaskModuleMessageResponse(value="feedback form")) + + other_activity = TaskFetchInvokeActivity( + id="activity-2", + type="invoke", + name="task/fetch", + from_=Account(id="user-1", name="User"), + recipient=Account(id="bot-1", name="Bot"), + conversation=ConversationAccount(id="conv-1", conversation_type="personal"), + channel_id="msteams", + value=TaskModuleRequest(data={}), + ) + + handlers = app.router.select_handlers(other_activity) + assert len(handlers) == 0 + + def test_on_message_fetch_task_reaction_dislike(self, app: App) -> None: + @app.on_message_fetch_task + async def handler(ctx: ActivityContext[MessageFetchTaskInvokeActivity]) -> TaskModuleInvokeResponse: + return TaskModuleInvokeResponse(task=TaskModuleMessageResponse(value="feedback form")) + + dislike_activity = MessageFetchTaskInvokeActivity( + id="activity-3", + type="invoke", + name="message/fetchTask", + from_=Account(id="user-1", name="User"), + recipient=Account(id="bot-1", name="Bot"), + conversation=ConversationAccount(id="conv-1", conversation_type="personal"), + channel_id="msteams", + value=MessageFetchTaskInvokeValue( + data=MessageFetchTaskData(action_value=MessageFetchTaskActionValue(reaction="dislike")) + ), + ) + + handlers = app.router.select_handlers(dislike_activity) + assert len(handlers) == 1 + assert handlers[0] == handler From 493ce1f4055da2b12b280532f038557903d28d3b Mon Sep 17 00:00:00 2001 From: Rajan Date: Mon, 6 Apr 2026 16:54:56 -0400 Subject: [PATCH 03/17] fix: update invalid inbound/outbound activities (#340) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #338. removed: - `ConversationUpdateActivityInput` - invalid outbound activity that fails service validation - inbound and outbound `HandoffActivity` - not recognized by backend - BF has a similar event but it uses `type: "event"` + `name: "handoff.initiate"` - similiarly exists for copilot but it is only inbound as `HandoffActionInvokeActivity` which we have - inbound and outbound `TraceActivity` - not recognized by backend - is only for the BF Emulator which is now deprecated. [doc ref](https://learn.microsoft.com/en-us/azure/bot-service/using-trace-activities?view=azure-bot-service-4.0&tabs=csharp) - `MessageDeleteActivityInput` - invalid outbound activity that fails service validation - will be provided in a new API endpoint instead - `MessageUpdateActivityInput` - invalid outbound activity that fails service validation - `CommandSendActivityInput` - invalid outbound activity that fails service validation - not a Teams concept. it's a BF activity type used in the skills protocol — where one bot sends a command to a skill bot and gets a commandResult back. - `CommandResultActivityInput` - not recognized by backend - not a Teams concept, it's a BF skill that sends back to the parent bot after completing a command. --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: lilydu --- .../api/activities/__init__.py | 6 -- .../api/activities/activity_params.py | 18 +----- .../api/activities/command/__init__.py | 6 +- .../api/activities/command/command_result.py | 8 +-- .../api/activities/command/command_send.py | 8 +-- .../api/activities/conversation/__init__.py | 2 - .../conversation/conversation_update.py | 8 +-- .../microsoft_teams/api/activities/handoff.py | 22 ------- .../api/activities/message/__init__.py | 5 +- .../api/activities/message/message_delete.py | 6 +- .../api/activities/message/message_update.py | 60 +------------------ .../microsoft_teams/api/activities/trace.py | 52 ---------------- .../apps/routing/activity_route_configs.py | 16 ----- .../apps/routing/generated_handlers.py | 40 ------------- 14 files changed, 10 insertions(+), 247 deletions(-) delete mode 100644 packages/api/src/microsoft_teams/api/activities/handoff.py delete mode 100644 packages/api/src/microsoft_teams/api/activities/trace.py diff --git a/packages/api/src/microsoft_teams/api/activities/__init__.py b/packages/api/src/microsoft_teams/api/activities/__init__.py index be3f19c38..1050c8181 100644 --- a/packages/api/src/microsoft_teams/api/activities/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/__init__.py @@ -18,7 +18,6 @@ ) from .event import * # noqa: F403 from .event import EventActivity -from .handoff import HandoffActivity from .install_update import * # noqa: F403 from .install_update import InstallUpdateActivity from .invoke import * # noqa: F403 @@ -26,13 +25,10 @@ from .message import * # noqa: F403 from .message import MessageActivities from .sent_activity import SentActivity -from .trace import TraceActivity from .typing import TypingActivity, TypingActivityInput Activity = Annotated[ Union[ - HandoffActivity, - TraceActivity, TypingActivity, CommandActivity, ConversationActivity, @@ -61,13 +57,11 @@ "ConversationUpdateActivity", "ConversationChannelData", "EventActivity", - "HandoffActivity", "InstallUpdateActivity", "TypingActivity", "TypingActivityInput", "ConversationEventType", "InvokeActivity", - "TraceActivity", "ActivityParams", "SentActivity", ] diff --git a/packages/api/src/microsoft_teams/api/activities/activity_params.py b/packages/api/src/microsoft_teams/api/activities/activity_params.py index a566f2635..bed4be58f 100644 --- a/packages/api/src/microsoft_teams/api/activities/activity_params.py +++ b/packages/api/src/microsoft_teams/api/activities/activity_params.py @@ -8,33 +8,17 @@ from pydantic import Field -from .command import CommandResultActivityInput, CommandSendActivityInput -from .conversation import ConversationUpdateActivityInput -from .handoff import HandoffActivityInput from .message import ( MessageActivityInput, - MessageDeleteActivityInput, MessageReactionActivityInput, - MessageUpdateActivityInput, ) -from .trace import TraceActivityInput from .typing import TypingActivityInput ActivityParams = Annotated[ Union[ - # Simple activities - ConversationUpdateActivityInput, - HandoffActivityInput, - TraceActivityInput, - TypingActivityInput, - # Message activities MessageActivityInput, - MessageDeleteActivityInput, MessageReactionActivityInput, - MessageUpdateActivityInput, - # Command activities - CommandSendActivityInput, - CommandResultActivityInput, + TypingActivityInput, ], Field(discriminator="type"), ] diff --git a/packages/api/src/microsoft_teams/api/activities/command/__init__.py b/packages/api/src/microsoft_teams/api/activities/command/__init__.py index 1d8eea470..2b73f0b2d 100644 --- a/packages/api/src/microsoft_teams/api/activities/command/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/command/__init__.py @@ -7,17 +7,15 @@ from pydantic import Field -from .command_result import CommandResultActivity, CommandResultActivityInput, CommandResultValue -from .command_send import CommandSendActivity, CommandSendActivityInput, CommandSendValue +from .command_result import CommandResultActivity, CommandResultValue +from .command_send import CommandSendActivity, CommandSendValue CommandActivity = Annotated[Union[CommandSendActivity, CommandResultActivity], Field(discriminator="type")] __all__ = [ "CommandResultValue", "CommandResultActivity", - "CommandResultActivityInput", "CommandSendValue", "CommandSendActivity", - "CommandSendActivityInput", "CommandActivity", ] diff --git a/packages/api/src/microsoft_teams/api/activities/command/command_result.py b/packages/api/src/microsoft_teams/api/activities/command/command_result.py index 5d32ceeba..1cb62d578 100644 --- a/packages/api/src/microsoft_teams/api/activities/command/command_result.py +++ b/packages/api/src/microsoft_teams/api/activities/command/command_result.py @@ -5,7 +5,7 @@ from typing import Any, Literal, Optional -from ...models import ActivityBase, ActivityInputBase, CustomBaseModel +from ...models import ActivityBase, CustomBaseModel class CommandResultValue(CustomBaseModel): @@ -45,9 +45,3 @@ class CommandResultActivity(_CommandResultBase, ActivityBase): name: str # pyright: ignore [reportGeneralTypeIssues, reportIncompatibleVariableOverride] """The name of the event.""" - - -class CommandResultActivityInput(_CommandResultBase, ActivityInputBase): - """Input model for creating command result activities with builder methods.""" - - pass diff --git a/packages/api/src/microsoft_teams/api/activities/command/command_send.py b/packages/api/src/microsoft_teams/api/activities/command/command_send.py index 0f5a17fd8..81ef8f664 100644 --- a/packages/api/src/microsoft_teams/api/activities/command/command_send.py +++ b/packages/api/src/microsoft_teams/api/activities/command/command_send.py @@ -5,7 +5,7 @@ from typing import Any, Literal, Optional -from ...models import ActivityBase, ActivityInputBase, CustomBaseModel +from ...models import ActivityBase, CustomBaseModel class CommandSendValue(CustomBaseModel): @@ -41,9 +41,3 @@ class CommandSendActivity(_CommandSendBase, ActivityBase): name: str # pyright: ignore [reportGeneralTypeIssues, reportIncompatibleVariableOverride] """The name of the event.""" - - -class CommandSendActivityInput(_CommandSendBase, ActivityInputBase): - """Input model for creating command send activities with builder methods.""" - - pass diff --git a/packages/api/src/microsoft_teams/api/activities/conversation/__init__.py b/packages/api/src/microsoft_teams/api/activities/conversation/__init__.py index d6aa67bec..51b90e6ad 100644 --- a/packages/api/src/microsoft_teams/api/activities/conversation/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/conversation/__init__.py @@ -7,7 +7,6 @@ ConversationChannelData, ConversationEventType, ConversationUpdateActivity, - ConversationUpdateActivityInput, ) ConversationActivity = ConversationUpdateActivity @@ -16,6 +15,5 @@ "ConversationEventType", "ConversationChannelData", "ConversationUpdateActivity", - "ConversationUpdateActivityInput", "ConversationActivity", ] diff --git a/packages/api/src/microsoft_teams/api/activities/conversation/conversation_update.py b/packages/api/src/microsoft_teams/api/activities/conversation/conversation_update.py index c13aa3382..ba0b69388 100644 --- a/packages/api/src/microsoft_teams/api/activities/conversation/conversation_update.py +++ b/packages/api/src/microsoft_teams/api/activities/conversation/conversation_update.py @@ -5,7 +5,7 @@ from typing import List, Literal, Optional -from ...models import Account, ActivityBase, ActivityInputBase, ChannelData, CustomBaseModel +from ...models import Account, ActivityBase, ChannelData, CustomBaseModel ConversationEventType = Literal[ "channelCreated", @@ -53,9 +53,3 @@ class ConversationUpdateActivity(_ConversationUpdateBase, ActivityBase): channel_data: ConversationChannelData # pyright: ignore [reportGeneralTypeIssues, reportIncompatibleVariableOverride] """Channel data with event type information.""" - - -class ConversationUpdateActivityInput(_ConversationUpdateBase, ActivityInputBase): - """Input model for creating conversation update activities with builder methods.""" - - pass diff --git a/packages/api/src/microsoft_teams/api/activities/handoff.py b/packages/api/src/microsoft_teams/api/activities/handoff.py deleted file mode 100644 index 065b538af..000000000 --- a/packages/api/src/microsoft_teams/api/activities/handoff.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -from typing import Literal - -from ..models import ActivityBase, ActivityInputBase, CustomBaseModel - - -class _HandoffBase(CustomBaseModel): - """Base class containing shared handoff activity fields (all Optional except type).""" - - type: Literal["handoff"] = "handoff" - - -class HandoffActivity(_HandoffBase, ActivityBase): - """Output model for received handoff activities with required fields and read-only properties.""" - - -class HandoffActivityInput(_HandoffBase, ActivityInputBase): - """Input model for creating handoff activities with builder methods.""" diff --git a/packages/api/src/microsoft_teams/api/activities/message/__init__.py b/packages/api/src/microsoft_teams/api/activities/message/__init__.py index a6ad3a6c9..2bbfffa1b 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/message/__init__.py @@ -8,12 +8,11 @@ from pydantic import Field from .message import MessageActivity, MessageActivityInput -from .message_delete import MessageDeleteActivity, MessageDeleteActivityInput, MessageDeleteChannelData +from .message_delete import MessageDeleteActivity, MessageDeleteChannelData from .message_reaction import MessageReactionActivity, MessageReactionActivityInput from .message_update import ( MessageEventType, MessageUpdateActivity, - MessageUpdateActivityInput, MessageUpdateChannelData, ) @@ -32,12 +31,10 @@ "MessageActivity", "MessageActivityInput", "MessageDeleteActivity", - "MessageDeleteActivityInput", "MessageDeleteChannelData", "MessageReactionActivity", "MessageReactionActivityInput", "MessageUpdateActivity", - "MessageUpdateActivityInput", "MessageUpdateChannelData", "MessageEventType", ] diff --git a/packages/api/src/microsoft_teams/api/activities/message/message_delete.py b/packages/api/src/microsoft_teams/api/activities/message/message_delete.py index 7f7272f02..ee203ccc1 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/message_delete.py +++ b/packages/api/src/microsoft_teams/api/activities/message/message_delete.py @@ -5,7 +5,7 @@ from typing import Literal, Optional -from ...models import ActivityBase, ActivityInputBase, ChannelData +from ...models import ActivityBase, ChannelData from ...models.custom_base_model import CustomBaseModel @@ -30,7 +30,3 @@ class MessageDeleteActivity(_MessageDeleteBase, ActivityBase): channel_data: MessageDeleteChannelData # pyright: ignore [reportGeneralTypeIssues] """Channel-specific data for message delete events.""" - - -class MessageDeleteActivityInput(_MessageDeleteBase, ActivityInputBase): - """Input model for creating message delete activities with builder methods.""" diff --git a/packages/api/src/microsoft_teams/api/activities/message/message_update.py b/packages/api/src/microsoft_teams/api/activities/message/message_update.py index cb342e9d0..138bad857 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/message_update.py +++ b/packages/api/src/microsoft_teams/api/activities/message/message_update.py @@ -4,9 +4,9 @@ """ from datetime import datetime -from typing import Any, Literal, Optional, Self +from typing import Any, Literal, Optional -from ...models import ActivityBase, ActivityInputBase, ChannelData +from ...models import ActivityBase, ChannelData from ...models.custom_base_model import CustomBaseModel MessageEventType = Literal["undeleteMessage", "editMessage"] @@ -54,59 +54,3 @@ class MessageUpdateActivity(_MessageUpdateBase, ActivityBase): channel_data: MessageUpdateChannelData # pyright: ignore [reportGeneralTypeIssues] """Channel-specific data for message update events.""" - - -class MessageUpdateActivityInput(_MessageUpdateBase, ActivityInputBase): - """Input model for creating message update activities with builder methods.""" - - def with_text(self, text: str) -> Self: - """ - Set the text content of the message. - - Args: - text: The text content to set - - Returns: - Self for method chaining - """ - self.text = text - return self - - def with_speak(self, speak: str) -> Self: - """ - Set the text to speak. - - Args: - speak: The text to speak - - Returns: - Self for method chaining - """ - self.speak = speak - return self - - def with_summary(self, summary: str) -> Self: - """ - Set the summary text. - - Args: - summary: The summary text to set - - Returns: - Self for method chaining - """ - self.summary = summary - return self - - def with_expiration(self, expiration: datetime) -> Self: - """ - Set the expiration time for the activity. - - Args: - expiration: The expiration datetime to set - - Returns: - Self for method chaining - """ - self.expiration = expiration - return self diff --git a/packages/api/src/microsoft_teams/api/activities/trace.py b/packages/api/src/microsoft_teams/api/activities/trace.py deleted file mode 100644 index ff12876ac..000000000 --- a/packages/api/src/microsoft_teams/api/activities/trace.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -from typing import Any, Literal, Optional - -from ..models import ActivityBase, ActivityInputBase, CustomBaseModel - - -class _TraceBase(CustomBaseModel): - """Base class containing shared trace activity fields (all Optional except type).""" - - type: Literal["trace"] = "trace" - - name: Optional[str] = None - """" - The name of the operation associated with an invoke or event activity. - """ - - label: Optional[str] = None - """ - A descriptive label for the activity. - """ - - value_type: Optional[str] = None - """ - The type of the activity's value object. - """ - - value: Optional[Any] = None - """ - A value that is associated with the activity. - """ - - -class TraceActivity(_TraceBase, ActivityBase): - """Output model for received trace activities with required fields and read-only properties.""" - - label: str # pyright: ignore [reportGeneralTypeIssues] - """ - A descriptive label for the activity. - """ - - value_type: str # pyright: ignore [reportGeneralTypeIssues] - """ - The type of the activity's value object. - """ - - -class TraceActivityInput(_TraceBase, ActivityInputBase): - """Input model for creating trace activities with builder methods.""" diff --git a/packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py b/packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py index cb9f93629..8f5436373 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py +++ b/packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py @@ -19,7 +19,6 @@ ExecuteActionInvokeActivity, FileConsentInvokeActivity, HandoffActionInvokeActivity, - HandoffActivity, InstalledActivity, InvokeActivity, MeetingEndEventActivity, @@ -51,7 +50,6 @@ TabInvokeResponse, TabSubmitInvokeActivity, TaskModuleInvokeResponse, - TraceActivity, TypingActivity, UninstalledActivity, ) @@ -562,20 +560,6 @@ class ActivityConfig: selector=lambda activity: isinstance(activity, TypingActivity), output_model=None, ), - "trace": ActivityConfig( - name="trace", - method_name="on_trace", - input_model=TraceActivity, - selector=lambda activity: isinstance(activity, TraceActivity), - output_model=None, - ), - "handoff": ActivityConfig( - name="handoff", - method_name="on_handoff", - input_model=HandoffActivity, - selector=lambda activity: isinstance(activity, HandoffActivity), - output_model=None, - ), # Generic Activity Handler (catch-all) "activity": ActivityConfig( name="activity", diff --git a/packages/apps/src/microsoft_teams/apps/routing/generated_handlers.py b/packages/apps/src/microsoft_teams/apps/routing/generated_handlers.py index b6b179770..de4715ef3 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/generated_handlers.py +++ b/packages/apps/src/microsoft_teams/apps/routing/generated_handlers.py @@ -24,7 +24,6 @@ ExecuteActionInvokeActivity, FileConsentInvokeActivity, HandoffActionInvokeActivity, - HandoffActivity, InstalledActivity, InstallUpdateActivity, InvokeActivity, @@ -53,7 +52,6 @@ SignInVerifyStateInvokeActivity, TabFetchInvokeActivity, TabSubmitInvokeActivity, - TraceActivity, TypingActivity, UninstalledActivity, ) @@ -1603,44 +1601,6 @@ def decorator(func: BasicHandler[TypingActivity]) -> BasicHandler[TypingActivity return decorator(handler) return decorator - @overload - def on_trace(self, handler: BasicHandler[TraceActivity]) -> BasicHandler[TraceActivity]: ... - - @overload - def on_trace(self) -> Callable[[BasicHandler[TraceActivity]], BasicHandler[TraceActivity]]: ... - - def on_trace(self, handler: Optional[BasicHandler[TraceActivity]] = None) -> BasicHandlerUnion[TraceActivity]: - """Register a trace activity handler.""" - - def decorator(func: BasicHandler[TraceActivity]) -> BasicHandler[TraceActivity]: - validate_handler_type(func, TraceActivity, "on_trace", "TraceActivity") - config = ACTIVITY_ROUTES["trace"] - self.router.add_handler(config.selector, func) - return func - - if handler is not None: - return decorator(handler) - return decorator - - @overload - def on_handoff(self, handler: BasicHandler[HandoffActivity]) -> BasicHandler[HandoffActivity]: ... - - @overload - def on_handoff(self) -> Callable[[BasicHandler[HandoffActivity]], BasicHandler[HandoffActivity]]: ... - - def on_handoff(self, handler: Optional[BasicHandler[HandoffActivity]] = None) -> BasicHandlerUnion[HandoffActivity]: - """Register a handoff activity handler.""" - - def decorator(func: BasicHandler[HandoffActivity]) -> BasicHandler[HandoffActivity]: - validate_handler_type(func, HandoffActivity, "on_handoff", "HandoffActivity") - config = ACTIVITY_ROUTES["handoff"] - self.router.add_handler(config.selector, func) - return func - - if handler is not None: - return decorator(handler) - return decorator - @overload def on_activity(self, handler: BasicHandler[Activity]) -> BasicHandler[Activity]: ... From f0c2721bae5453c2ffcde64e675684c8e206c7ed Mon Sep 17 00:00:00 2001 From: Lily Du Date: Mon, 6 Apr 2026 16:08:52 -0700 Subject: [PATCH 04/17] Move builders from ActivityInput to MessageActivityInput (#363) - only outbound activities we have are `MessageActivityInput` and `TypingActivityInput` - basically same as #231, moved citations, feedback, AI label --------- Co-authored-by: lilydu --- .../api/activities/message/message.py | 90 ++++++++++++++++++ .../microsoft_teams/api/models/activity.py | 92 +------------------ packages/api/tests/unit/test_activity.py | 50 ---------- .../api/tests/unit/test_message_activities.py | 49 ++++++++++ 4 files changed, 141 insertions(+), 140 deletions(-) diff --git a/packages/api/src/microsoft_teams/api/activities/message/message.py b/packages/api/src/microsoft_teams/api/activities/message/message.py index 41cac9705..7f125c95d 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/message.py +++ b/packages/api/src/microsoft_teams/api/activities/message/message.py @@ -22,6 +22,17 @@ SuggestedActions, TextFormat, ) +from ...models.channel_data import FeedbackLoop +from ...models.entity import ( + AIMessageEntity, + Appearance, + CitationAppearance, + CitationEntity, + Claim, + Entity, + Image, + MessageEntity, +) from ..utils import StripMentionsTextOptions, strip_mentions_text @@ -324,6 +335,85 @@ def add_stream_final(self) -> Self: return self.add_entity(stream_entity) + def add_ai_generated(self) -> Self: + """Add the 'Generated By AI' label.""" + message_entity = self._ensure_single_root_level_message_entity() + ai_entity = AIMessageEntity(**message_entity.model_dump()) + if ai_entity.additional_type and "AIGeneratedContent" in ai_entity.additional_type: + return self + + if not ai_entity.additional_type: + ai_entity.additional_type = [] + + ai_entity.additional_type.append("AIGeneratedContent") + + self._update_entity(message_entity, ai_entity) + + return self + + def add_citation(self, position: int, appearance: CitationAppearance) -> Self: + """Add citations.""" + message_entity = self._ensure_single_root_level_message_entity() + citation_entity = CitationEntity(**message_entity.model_dump()) + if citation_entity.citation is None: + citation_entity.citation = [] + + citation_entity.citation.append( + Claim( + position=position, + appearance=Appearance( + abstract=appearance.abstract, + name=appearance.name, + image=Image(name=appearance.icon) if appearance.icon else None, + keywords=appearance.keywords, + text=appearance.text, + url=appearance.url, + usage_info=appearance.usage_info, + ), + ) + ) + + self._update_entity(message_entity, citation_entity) + + return self + + def _ensure_single_root_level_message_entity(self) -> MessageEntity: + """ + Get or create the base message entity. + There should only be one root level message entity. + """ + message_entity = next( + (e for e in (self.entities or []) if e.type == "https://schema.org/Message" and e.at_type == "Message"), + None, + ) + + if not message_entity: + message_entity = MessageEntity() + self.add_entity(message_entity) + + return message_entity + + def _update_entity(self, old_entity: Entity, new_entity: Entity) -> None: + if self.entities is not None: + index = self.entities.index(old_entity) + self.entities.pop(index) + self.entities.insert(index, new_entity) + + def add_feedback(self, mode: Literal["default", "custom"] = "default") -> Self: + """ + Enable message feedback. + + Args: + mode: "default" shows Teams' built-in thumbs up/down UI. + "custom" triggers a message/fetchTask invoke so the bot + can return its own task module dialog. + """ + if not self.channel_data: + self.channel_data = ChannelData() + self.channel_data.feedback_loop = FeedbackLoop(type=mode) + self.channel_data.feedback_loop_enabled = None + return self + def with_recipient(self, value: Account, is_targeted: Optional[bool] = None) -> Self: """ Set the recipient. diff --git a/packages/api/src/microsoft_teams/api/models/activity.py b/packages/api/src/microsoft_teams/api/models/activity.py index cf957a01c..464152363 100644 --- a/packages/api/src/microsoft_teams/api/models/activity.py +++ b/packages/api/src/microsoft_teams/api/models/activity.py @@ -5,24 +5,15 @@ import warnings from datetime import datetime -from typing import Any, List, Literal, Optional, Self +from typing import Any, List, Optional, Self from microsoft_teams.api.models.account import Account, ConversationAccount -from microsoft_teams.api.models.channel_data.channel_data import ChannelData, FeedbackLoop +from microsoft_teams.api.models.channel_data.channel_data import ChannelData from microsoft_teams.api.models.channel_data.channel_info import ChannelInfo from microsoft_teams.api.models.channel_data.notification_info import NotificationInfo from microsoft_teams.api.models.channel_data.team_info import TeamInfo from microsoft_teams.api.models.channel_id import ChannelID -from microsoft_teams.api.models.entity.ai_message_entity import AIMessageEntity -from microsoft_teams.api.models.entity.citation_entity import ( - Appearance, - CitationAppearance, - CitationEntity, - Claim, - Image, -) from microsoft_teams.api.models.entity.entity import Entity -from microsoft_teams.api.models.entity.message_entity import MessageEntity from microsoft_teams.api.models.meetings.meeting_info import MeetingInfo from microsoft_teams.common.experimental import ExperimentalWarning @@ -213,89 +204,10 @@ def add_entities(self, *values: Entity) -> Self: self.entities.extend(values) return self - def add_ai_generated(self) -> Self: - """Add the 'Generated By AI' label.""" - message_entity = self.ensure_single_root_level_message_entity() - ai_entity = AIMessageEntity(**message_entity.model_dump()) - if ai_entity.additional_type and "AIGeneratedContent" in ai_entity.additional_type: - return self - - if not ai_entity.additional_type: - ai_entity.additional_type = [] - - ai_entity.additional_type.append("AIGeneratedContent") - - self._update_entity(message_entity, ai_entity) - - return self - - def add_feedback(self, mode: Literal["default", "custom"] = "default") -> Self: - """ - Enable message feedback. - - Args: - mode: "default" shows Teams' built-in thumbs up/down UI. - "custom" triggers a message/fetchTask invoke so the bot - can return its own task module dialog. - """ - if not self.channel_data: - self.channel_data = ChannelData() - self.channel_data.feedback_loop = FeedbackLoop(type=mode) - self.channel_data.feedback_loop_enabled = None - return self - - def add_citation(self, position: int, appearance: CitationAppearance) -> Self: - """Add citations.""" - message_entity = self.ensure_single_root_level_message_entity() - citation_entity = CitationEntity(**message_entity.model_dump()) - if citation_entity.citation is None: - citation_entity.citation = [] - - citation_entity.citation.append( - Claim( - position=position, - appearance=Appearance( - abstract=appearance.abstract, - name=appearance.name, - image=Image(name=appearance.icon) if appearance.icon else None, - keywords=appearance.keywords, - text=appearance.text, - url=appearance.url, - usage_info=appearance.usage_info, - ), - ) - ) - - self._update_entity(message_entity, citation_entity) - - return self - def is_streaming(self) -> bool: """Check if this is a streaming activity.""" return bool(self.entities and any(e.type == "streaminfo" for e in self.entities or [])) - def ensure_single_root_level_message_entity(self) -> MessageEntity: - """ - Get or create the base message entity. - There should only be one root level message entity. - """ - message_entity = next( - (e for e in (self.entities or []) if e.type == "https://schema.org/Message" and e.at_type == "Message"), - None, - ) - - if not message_entity: - message_entity = MessageEntity() - self.add_entity(message_entity) - - return message_entity - - def _update_entity(self, old_entity: Entity, new_entity: Entity) -> None: - if self.entities is not None: - index = self.entities.index(old_entity) - self.entities.pop(index) - self.entities.insert(index, new_entity) - class Activity(_ActivityBase): """Output model for received activities with required fields and read-only properties.""" diff --git a/packages/api/tests/unit/test_activity.py b/packages/api/tests/unit/test_activity.py index 675a33a7d..c2989acec 100644 --- a/packages/api/tests/unit/test_activity.py +++ b/packages/api/tests/unit/test_activity.py @@ -5,7 +5,6 @@ # pyright: basic from datetime import datetime -from typing import cast import pytest from microsoft_teams.api.models import ( @@ -13,14 +12,12 @@ ActivityInputBase, ChannelData, ChannelInfo, - CitationIconName, ConversationAccount, MeetingInfo, NotificationInfo, TeamInfo, TenantInfo, ) -from microsoft_teams.api.models.entity import CitationAppearance, CitationEntity, MessageEntity @pytest.fixture @@ -111,50 +108,3 @@ def test_should_have_channel_data_accessors( assert activity.team and activity.team.id == "team-id" assert activity.meeting and activity.meeting.id == "meeting-id" assert activity.notification and activity.notification.alert is True - - def test_should_add_ai_label(self, test_activity: ConcreteTestActivity) -> None: - activity = test_activity.add_ai_generated() - - assert activity.type == "test" - assert activity.entities and len(activity.entities) == 1 - message_entity = cast(MessageEntity, activity.entities[0]) - assert message_entity.additional_type and message_entity.additional_type[0] == "AIGeneratedContent" - - def test_should_add_feedback_label(self, test_activity: ConcreteTestActivity) -> None: - activity = test_activity.add_feedback() - - assert activity.type == "test" - assert activity.channel_data and activity.channel_data.feedback_loop is not None - assert activity.channel_data.feedback_loop.type == "default" - assert activity.channel_data.feedback_loop_enabled is None - - def test_should_add_custom_feedback_label(self, test_activity: ConcreteTestActivity) -> None: - activity = test_activity.add_feedback(mode="custom") - - assert activity.type == "test" - assert activity.channel_data and activity.channel_data.feedback_loop is not None - assert activity.channel_data.feedback_loop.type == "custom" - assert activity.channel_data.feedback_loop_enabled is None - - def test_should_add_citation(self, test_activity: ConcreteTestActivity) -> None: - activity = test_activity.add_citation(0, CitationAppearance(abstract="test", name="test")) - - assert activity.type == "test" - assert activity.entities and len(activity.entities) == 1 - citation_entity = cast(CitationEntity, activity.entities[0]) - assert citation_entity.citation and len(citation_entity.citation) == 1 - - def test_should_add_citation_with_icon(self, test_activity: ConcreteTestActivity) -> None: - activity = test_activity.add_citation( - 0, CitationAppearance(abstract="test", name="test", icon=CitationIconName.GIF) - ) - - assert activity.type == "test" - assert activity.entities and len(activity.entities) == 1 - citation_entity = cast(CitationEntity, activity.entities[0]) - assert citation_entity.citation and len(citation_entity.citation) == 1 - assert citation_entity.citation[0].appearance.abstract == "test" - assert citation_entity.citation[0].appearance.name == "test" - assert ( - citation_entity.citation[0].appearance.image and citation_entity.citation[0].appearance.image.name == "GIF" - ) diff --git a/packages/api/tests/unit/test_message_activities.py b/packages/api/tests/unit/test_message_activities.py index 5ee803f05..8711dd158 100644 --- a/packages/api/tests/unit/test_message_activities.py +++ b/packages/api/tests/unit/test_message_activities.py @@ -23,12 +23,14 @@ Account, Attachment, ChannelData, + CitationIconName, ConversationAccount, MentionEntity, MessageReaction, MessageUser, StreamInfoEntity, ) +from microsoft_teams.api.models.entity import CitationAppearance, CitationEntity, MessageEntity from microsoft_teams.cards import AdaptiveCard @@ -303,6 +305,53 @@ def test_get_account_mention_not_found(self): result = activity.get_account_mention("nonexistent-id") assert result is None + def test_should_add_ai_label(self) -> None: + activity = self.create_message_activity().add_ai_generated() + + assert activity.type == "message" + assert activity.entities and len(activity.entities) == 1 + message_entity = cast(MessageEntity, activity.entities[0]) + assert message_entity.additional_type and message_entity.additional_type[0] == "AIGeneratedContent" + + def test_should_add_feedback_label(self) -> None: + activity = self.create_message_activity().add_feedback() + + assert activity.type == "message" + assert activity.channel_data and activity.channel_data.feedback_loop is not None + assert activity.channel_data.feedback_loop.type == "default" + assert activity.channel_data.feedback_loop_enabled is None + + def test_should_add_custom_feedback_label(self) -> None: + activity = self.create_message_activity().add_feedback(mode="custom") + + assert activity.type == "message" + assert activity.channel_data and activity.channel_data.feedback_loop is not None + assert activity.channel_data.feedback_loop.type == "custom" + assert activity.channel_data.feedback_loop_enabled is None + + def test_should_add_citation(self) -> None: + activity = self.create_message_activity().add_citation(0, CitationAppearance(abstract="test", name="test")) + + assert activity.type == "message" + assert activity.entities and len(activity.entities) == 1 + citation_entity = cast(CitationEntity, activity.entities[0]) + assert citation_entity.citation and len(citation_entity.citation) == 1 + + def test_should_add_citation_with_icon(self) -> None: + activity = self.create_message_activity().add_citation( + 0, CitationAppearance(abstract="test", name="test", icon=CitationIconName.GIF) + ) + + assert activity.type == "message" + assert activity.entities and len(activity.entities) == 1 + citation_entity = cast(CitationEntity, activity.entities[0]) + assert citation_entity.citation and len(citation_entity.citation) == 1 + assert citation_entity.citation[0].appearance.abstract == "test" + assert citation_entity.citation[0].appearance.name == "test" + assert ( + citation_entity.citation[0].appearance.image and citation_entity.citation[0].appearance.image.name == "GIF" + ) + def test_add_stream_final(self): """Test adding stream final functionality""" activity = self.create_message_activity() From 99bb1e73460a83989ee621939195ba710bc634e2 Mon Sep 17 00:00:00 2001 From: Corina <14900841+corinagum@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:10:00 -0700 Subject: [PATCH 05/17] Update generated Adaptive Cards core from latest codegen (#352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Regenerated core.py from latest AC schema codegen output - New types: CollabStageInvokeDataValue, OpenUrlDialogAction, PopoverAction, ProgressBar, ProgressRing, Resources, StringResource, TabInfo ## Test plan - [x] 9/9 cards tests pass on updated main - [x] Lint passes (ruff format + ruff check) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 (1M context) --- .../cards/src/microsoft_teams/cards/core.py | 783 ++++++++++++------ 1 file changed, 550 insertions(+), 233 deletions(-) diff --git a/packages/cards/src/microsoft_teams/cards/core.py b/packages/cards/src/microsoft_teams/cards/core.py index 1ec829bf6..c5e7e03c5 100644 --- a/packages/cards/src/microsoft_teams/cards/core.py +++ b/packages/cards/src/microsoft_teams/cards/core.py @@ -1,7 +1,8 @@ -# This file was automatically generated by a tool on 03/31/2026, 12:11 AM UTC. DO NOT UPDATE MANUALLY. +# This file was automatically generated by a tool on 04/02/2026, 8:47 PM UTC. DO NOT UPDATE MANUALLY. # It includes declarations for Adaptive Card features available in Teams, Copilot, Outlook, Word, Excel, PowerPoint. -from typing import Dict, List, Any, Optional, Union, Literal, Self +from typing import Any, Dict, List, Literal, Optional, Self, Union + from pydantic import AliasGenerator, BaseModel, ConfigDict, Field, SerializeAsAny from pydantic.alias_generators import to_camel @@ -12,46 +13,46 @@ class SerializableObject(BaseModel): @staticmethod def validation_alias_generator(field: str) -> str: "Handles deserialization aliasing" - + # Handle parameters that start with "@" if field.startswith("at_"): return f"@{field[3:]}" - + # Handles from field which is a duplicate reserved internal name if field == "from_": return "from" - + # Handle ms_teams field which should deserialize from msteams if field == "ms_teams": return "msteams" - + # Handle choices_data field which should deserialize from choices.data if field == "choices_data": return "choices.data" - + # All other fields are converted to camelCase return to_camel(field) - + @staticmethod def serialization_alias_generator(field: str) -> str: "Handles serialization aliasing and casing" - + # Handle parameters that start with "@" if field.startswith("at_"): return f"@{field[3:]}" - + # Handles from field which is a duplicate reserved internal name if field == "from_": return "from" - + # Handle ms_teams field which should serialize to msteams if field == "ms_teams": return "msteams" - + # Handle choices_data field which should serialize to choices.data if field == "choices_data": return "choices.data" - + # All other fields are converted to camelCase return to_camel(field) @@ -64,19 +65,24 @@ def serialization_alias_generator(field: str) -> str: alias_generator=AliasGenerator( validation_alias=validation_alias_generator, serialization_alias=serialization_alias_generator ), - ) + ) class CardElement(SerializableObject): """Base class for CardElement.""" + pass + class Action(SerializableObject): """Base class for Action.""" + pass + class ContainerLayout(SerializableObject): """Base class for ContainerLayout.""" + pass @@ -92,7 +98,20 @@ class ContainerLayout(SerializableObject): Spacing = Literal["None", "ExtraSmall", "Small", "Default", "Medium", "Large", "ExtraLarge", "Padding"] -TargetWidth = Literal["VeryNarrow", "Narrow", "Standard", "Wide", "atLeast:VeryNarrow", "atMost:VeryNarrow", "atLeast:Narrow", "atMost:Narrow", "atLeast:Standard", "atMost:Standard", "atLeast:Wide", "atMost:Wide"] +TargetWidth = Literal[ + "VeryNarrow", + "Narrow", + "Standard", + "Wide", + "atLeast:VeryNarrow", + "atMost:VeryNarrow", + "atLeast:Narrow", + "atMost:Narrow", + "atLeast:Standard", + "atMost:Standard", + "atLeast:Wide", + "atMost:Wide", +] ContainerStyle = Literal["default", "emphasis", "accent", "good", "attention", "warning"] @@ -152,9 +171,67 @@ class ContainerLayout(SerializableObject): ProgressBarColor = Literal["Accent", "Good", "Warning", "Attention"] -ChartColorSet = Literal["categorical", "sequential", "sequentialred", "sequentialgreen", "sequentialyellow", "diverging"] - -ChartColor = Literal["good", "warning", "attention", "neutral", "categoricalRed", "categoricalPurple", "categoricalLavender", "categoricalBlue", "categoricalLightBlue", "categoricalTeal", "categoricalGreen", "categoricalLime", "categoricalMarigold", "sequential1", "sequential2", "sequential3", "sequential4", "sequential5", "sequential6", "sequential7", "sequential8", "divergingBlue", "divergingLightBlue", "divergingCyan", "divergingTeal", "divergingYellow", "divergingPeach", "divergingLightRed", "divergingRed", "divergingMaroon", "divergingGray", "sequentialRed1", "sequentialRed2", "sequentialRed3", "sequentialRed4", "sequentialRed5", "sequentialRed6", "sequentialRed7", "sequentialRed8", "sequentialGreen1", "sequentialGreen2", "sequentialGreen3", "sequentialGreen4", "sequentialGreen5", "sequentialGreen6", "sequentialGreen7", "sequentialGreen8", "sequentialYellow1", "sequentialYellow2", "sequentialYellow3", "sequentialYellow4", "sequentialYellow5", "sequentialYellow6", "sequentialYellow7", "sequentialYellow8"] +ChartColorSet = Literal[ + "categorical", "sequential", "sequentialred", "sequentialgreen", "sequentialyellow", "diverging" +] + +ChartColor = Literal[ + "good", + "warning", + "attention", + "neutral", + "categoricalRed", + "categoricalPurple", + "categoricalLavender", + "categoricalBlue", + "categoricalLightBlue", + "categoricalTeal", + "categoricalGreen", + "categoricalLime", + "categoricalMarigold", + "sequential1", + "sequential2", + "sequential3", + "sequential4", + "sequential5", + "sequential6", + "sequential7", + "sequential8", + "divergingBlue", + "divergingLightBlue", + "divergingCyan", + "divergingTeal", + "divergingYellow", + "divergingPeach", + "divergingLightRed", + "divergingRed", + "divergingMaroon", + "divergingGray", + "sequentialRed1", + "sequentialRed2", + "sequentialRed3", + "sequentialRed4", + "sequentialRed5", + "sequentialRed6", + "sequentialRed7", + "sequentialRed8", + "sequentialGreen1", + "sequentialGreen2", + "sequentialGreen3", + "sequentialGreen4", + "sequentialGreen5", + "sequentialGreen6", + "sequentialGreen7", + "sequentialGreen8", + "sequentialYellow1", + "sequentialYellow2", + "sequentialYellow3", + "sequentialYellow4", + "sequentialYellow5", + "sequentialYellow6", + "sequentialYellow7", + "sequentialYellow8", +] DonutThickness = Literal["Thin", "Thick"] @@ -162,7 +239,32 @@ class ContainerLayout(SerializableObject): GaugeChartValueFormat = Literal["Percentage", "Fraction"] -CodeLanguage = Literal["Bash", "C", "Cpp", "CSharp", "Css", "Dos", "Go", "Graphql", "Html", "Java", "JavaScript", "Json", "ObjectiveC", "Perl", "Php", "PlainText", "PowerShell", "Python", "Sql", "TypeScript", "VbNet", "Verilog", "Vhdl", "Xml"] +CodeLanguage = Literal[ + "Bash", + "C", + "Cpp", + "CSharp", + "Css", + "Dos", + "Go", + "Graphql", + "Html", + "Java", + "JavaScript", + "Json", + "ObjectiveC", + "Perl", + "Php", + "PlainText", + "PowerShell", + "Python", + "Sql", + "TypeScript", + "VbNet", + "Verilog", + "Vhdl", + "Xml", +] PersonaIconStyle = Literal["profilePicture", "contactCard", "none"] @@ -189,6 +291,7 @@ class ContainerLayout(SerializableObject): class HostCapabilities(SerializableObject): """Represents a list of versioned capabilities a host application must support.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ @@ -196,8 +299,10 @@ def with_key(self, value: str) -> Self: self.key = value return self + class ThemedUrl(SerializableObject): """Defines a theme-specific URL.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ theme: Optional[ThemeName] = "Light" @@ -217,8 +322,10 @@ def with_url(self, value: str) -> Self: self.url = value return self + class BackgroundImage(SerializableObject): """Defines a container's background image and the way it should be rendered.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ url: Optional[str] = None @@ -256,8 +363,10 @@ def with_themed_urls(self, value: List[ThemedUrl]) -> Self: self.themed_urls = value return self + class SubmitActionData(SerializableObject): """Represents the data of an Action.Submit.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ msteams: Optional[Dict[str, Any]] = None @@ -271,11 +380,13 @@ def with_msteams(self, value: Dict[str, Any]) -> Self: self.msteams = value return self + class ExecuteAction(Action): """Gathers input values, merges them with the data property if specified, and sends them to the Bot via an Invoke activity. The Bot can respond synchronously and return an updated Adaptive Card to be displayed by the client. Action.Execute works in all Adaptive Card hosts.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Action.Execute'] = 'Action.Execute' + type: Literal["Action.Execute"] = "Action.Execute" """ Must be **Action.Execute**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -303,7 +414,7 @@ class ExecuteAction(Action): """ The data to send to the Bot when the action is executed. When expressed as an object, `data` is sent back to the Bot when the action is executed, adorned with the values of the inputs expressed as key/value pairs, where the key is the Id of the input. If `data` is expressed as a string, input values are not sent to the Bot. """ associated_inputs: Optional[AssociatedInputs] = None """ The Ids of the inputs associated with the Action.Submit. When the action is executed, the values of the associated inputs are sent to the Bot. See [Input validation](topic:input-validation) for more details. """ - conditionally_enabled: Optional[bool] = None + conditionally_enabled: Optional[bool] = False """ Controls if the action is enabled only if at least one required input has been filled by the user. """ verb: Optional[str] = None """ The verb of the action. """ @@ -374,8 +485,10 @@ def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: self.fallback = value return self + class RefreshDefinition(SerializableObject): """Defines how a card can be refreshed by making a request to the target Bot.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ action: Optional[ExecuteAction] = None @@ -395,8 +508,10 @@ def with_user_ids(self, value: List[str]) -> Self: self.user_ids = value return self + class AuthCardButton(SerializableObject): """Defines a button as displayed when prompting a user to authenticate. For more information, refer to the [Bot Framework CardAction type](https://docs.microsoft.com/dotnet/api/microsoft.bot.schema.cardaction).""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ type: Optional[str] = None @@ -428,8 +543,10 @@ def with_value(self, value: str) -> Self: self.value = value return self + class TokenExchangeResource(SerializableObject): """Defines information required to enable on-behalf-of single sign-on user authentication. For more information, refer to the [Bot Framework TokenExchangeResource type](https://docs.microsoft.com/dotnet/api/microsoft.bot.schema.tokenexchangeresource)""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ id: Optional[str] = None @@ -455,8 +572,10 @@ def with_provider_id(self, value: str) -> Self: self.provider_id = value return self + class Authentication(SerializableObject): """Defines authentication information associated with a card. For more information, refer to the [Bot Framework OAuthCard type](https://docs.microsoft.com/dotnet/api/microsoft.bot.schema.oauthcard)""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ text: Optional[str] = None @@ -488,8 +607,10 @@ def with_token_exchange_resource(self, value: TokenExchangeResource) -> Self: self.token_exchange_resource = value return self + class MentionedEntity(SerializableObject): """Represents a mentioned person or tag.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ id: Optional[str] = None @@ -515,11 +636,13 @@ def with_mention_type(self, value: MentionType) -> Self: self.mention_type = value return self + class Mention(SerializableObject): """Represents a mention to a person.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['mention'] = 'mention' + type: Literal["mention"] = "mention" """ Must be **mention**. """ text: Optional[str] = None """ The text that will be substituted with the mention. """ @@ -538,8 +661,10 @@ def with_mentioned(self, value: MentionedEntity) -> Self: self.mentioned = value return self + class TeamsCardProperties(SerializableObject): """Represents a set of Teams-specific properties on a card.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ width: Optional[TeamsCardWidth] = None @@ -561,8 +686,10 @@ def with_entities(self, value: List[Mention]) -> Self: self.entities = value return self + class CardMetadata(SerializableObject): """Card-level metadata.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ web_url: Optional[str] = None @@ -576,8 +703,10 @@ def with_web_url(self, value: str) -> Self: self.web_url = value return self + class StringResource(SerializableObject): """Defines the replacement string values.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ default_value: Optional[str] = None @@ -597,8 +726,10 @@ def with_localized_values(self, value: Dict[str, str]) -> Self: self.localized_values = value return self + class Resources(SerializableObject): """The resources that can be used in the body of the card.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ strings: Optional[Dict[str, StringResource]] = None @@ -612,11 +743,13 @@ def with_strings(self, value: Dict[str, StringResource]) -> Self: self.strings = value return self + class AdaptiveCard(CardElement): """An Adaptive Card, containing a free-form body of card elements, and an optional set of actions.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['AdaptiveCard'] = 'AdaptiveCard' + type: Literal["AdaptiveCard"] = "AdaptiveCard" """ Must be **AdaptiveCard**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -624,7 +757,7 @@ class AdaptiveCard(CardElement): """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ lang: Optional[str] = None """ The locale associated with the element. """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the element is tapped or clicked. Action.ShowCard is not supported. """ @@ -767,11 +900,13 @@ def with_actions(self, value: List[Action]) -> Self: self.actions = value return self + class InsertImageAction(Action): """Inserts an image into the host application's canvas.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Action.InsertImage'] = 'Action.InsertImage' + type: Literal["Action.InsertImage"] = "Action.InsertImage" """ Must be **Action.InsertImage**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -864,11 +999,13 @@ def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: self.fallback = value return self + class OpenUrlAction(Action): """Opens the provided URL in either a separate browser tab or within the host application.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Action.OpenUrl'] = 'Action.OpenUrl' + type: Literal["Action.OpenUrl"] = "Action.OpenUrl" """ Must be **Action.OpenUrl**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -949,11 +1086,13 @@ def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: self.fallback = value return self + class OpenUrlDialogAction(Action): """Opens a task module in a modal dialog hosting the content at a provided URL.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Action.OpenUrlDialog'] = 'Action.OpenUrlDialog' + type: Literal["Action.OpenUrlDialog"] = "Action.OpenUrlDialog" """ Must be **Action.OpenUrlDialog**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -1052,11 +1191,13 @@ def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: self.fallback = value return self + class ResetInputsAction(Action): """Resets the values of the inputs in the card.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Action.ResetInputs'] = 'Action.ResetInputs' + type: Literal["Action.ResetInputs"] = "Action.ResetInputs" """ Must be **Action.ResetInputs**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -1137,8 +1278,10 @@ def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: self.fallback = value return self + class TeamsSubmitActionFeedback(SerializableObject): """Represents feedback options for an [Action.Submit](https://adaptivecards.microsoft.com/?topic=Action.Submit).""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ hide: Optional[bool] = None @@ -1152,8 +1295,10 @@ def with_hide(self, value: bool) -> Self: self.hide = value return self + class TeamsSubmitActionProperties(SerializableObject): """Teams-specific properties associated with the action.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ feedback: Optional[TeamsSubmitActionFeedback] = None @@ -1167,11 +1312,13 @@ def with_feedback(self, value: TeamsSubmitActionFeedback) -> Self: self.feedback = value return self + class SubmitAction(Action): """Gathers input values, merges them with the data property if specified, and sends them to the Bot via an Invoke activity. The Bot can only acknowledge is has received the request.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Action.Submit'] = 'Action.Submit' + type: Literal["Action.Submit"] = "Action.Submit" """ Must be **Action.Submit**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -1199,7 +1346,7 @@ class SubmitAction(Action): """ The data to send to the Bot when the action is executed. When expressed as an object, `data` is sent back to the Bot when the action is executed, adorned with the values of the inputs expressed as key/value pairs, where the key is the Id of the input. If `data` is expressed as a string, input values are not sent to the Bot. """ associated_inputs: Optional[AssociatedInputs] = None """ The Ids of the inputs associated with the Action.Submit. When the action is executed, the values of the associated inputs are sent to the Bot. See [Input validation](topic:input-validation) for more details. """ - conditionally_enabled: Optional[bool] = None + conditionally_enabled: Optional[bool] = False """ Controls if the action is enabled only if at least one required input has been filled by the user. """ msteams: Optional[TeamsSubmitActionProperties] = None """ Teams-specific metadata associated with the action. """ @@ -1270,8 +1417,10 @@ def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: self.fallback = value return self + class TargetElement(SerializableObject): """Defines a target element in an Action.ToggleVisibility.""" + element_id: Optional[str] = None """ The Id of the element to change the visibility of. """ is_visible: Optional[bool] = None @@ -1285,11 +1434,13 @@ def with_is_visible(self, value: bool) -> Self: self.is_visible = value return self + class ToggleVisibilityAction(Action): """Toggles the visibility of a set of elements. Action.ToggleVisibility is useful for creating "Show more" type UI patterns.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Action.ToggleVisibility'] = 'Action.ToggleVisibility' + type: Literal["Action.ToggleVisibility"] = "Action.ToggleVisibility" """ Must be **Action.ToggleVisibility**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -1370,11 +1521,13 @@ def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: self.fallback = value return self + class ShowCardAction(Action): """Expands or collapses an embedded card within the main card.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Action.ShowCard'] = 'Action.ShowCard' + type: Literal["Action.ShowCard"] = "Action.ShowCard" """ Must be **Action.ShowCard**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -1455,11 +1608,13 @@ def with_card(self, value: AdaptiveCard) -> Self: self.card = value return self + class PopoverAction(Action): """Shows a popover to display more information to the user.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Action.Popover'] = 'Action.Popover' + type: Literal["Action.Popover"] = "Action.Popover" """ Must be **Action.Popover**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -1552,11 +1707,13 @@ def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: self.fallback = value return self + class ActionSet(CardElement): """Displays a set of action, which can be placed anywhere in the card.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['ActionSet'] = 'ActionSet' + type: Literal["ActionSet"] = "ActionSet" """ Must be **ActionSet**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -1566,7 +1723,7 @@ class ActionSet(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -1576,7 +1733,7 @@ class ActionSet(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ @@ -1641,11 +1798,13 @@ def with_actions(self, value: List[Action]) -> Self: self.actions = value return self + class Container(CardElement): """A container for other elements. Use containers for styling purposes and/or to logically group a set of elements together, which can be especially useful when used with Action.ToggleVisibility.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Container'] = 'Container' + type: Literal["Container"] = "Container" """ Must be **Container**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -1655,7 +1814,7 @@ class Container(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -1665,19 +1824,19 @@ class Container(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the element is tapped or clicked. Action.ShowCard is not supported. """ style: Optional[ContainerStyle] = None """ The style of the container. Container styles control the colors of the background, border and text inside the container, in such a way that contrast requirements are always met. """ - show_border: Optional[bool] = None + show_border: Optional[bool] = False """ Controls if a border should be displayed around the container. """ - rounded_corners: Optional[bool] = None + rounded_corners: Optional[bool] = False """ Controls if the container should have rounded corners. """ layouts: Optional[List[SerializeAsAny[ContainerLayout]]] = None """ The layouts associated with the container. The container can dynamically switch from one layout to another as the card's width changes. See [Container layouts](topic:container-layouts) for more details. """ - bleed: Optional[bool] = None + bleed: Optional[bool] = False """ Controls if the container should bleed into its parent. A bleeding container extends into its parent's padding. """ min_height: Optional[str] = None """ The minimum height, in pixels, of the container, in the `px` format. """ @@ -1796,11 +1955,13 @@ def with_items(self, value: List[CardElement]) -> Self: self.items = value return self + class StackLayout(ContainerLayout): """A layout that stacks elements on top of each other. Layout.Stack is the default layout used by AdaptiveCard and all containers.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Layout.Stack'] = 'Layout.Stack' + type: Literal["Layout.Stack"] = "Layout.Stack" """ Must be **Layout.Stack**. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the layout should be used. """ @@ -1813,11 +1974,13 @@ def with_target_width(self, value: TargetWidth) -> Self: self.target_width = value return self + class FlowLayout(ContainerLayout): """A layout that spreads elements horizontally and wraps them across multiple rows, as needed.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Layout.Flow'] = 'Layout.Flow' + type: Literal["Layout.Flow"] = "Layout.Flow" """ Must be **Layout.Flow**. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the layout should be used. """ @@ -1878,8 +2041,10 @@ def with_row_spacing(self, value: Spacing) -> Self: self.row_spacing = value return self + class GridArea(SerializableObject): """Defines an area in a Layout.AreaGrid layout.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ name: Optional[str] = None @@ -1917,11 +2082,13 @@ def with_row_span(self, value: float) -> Self: self.row_span = value return self + class AreaGridLayout(ContainerLayout): """A layout that divides a container into named areas into which elements can be placed.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Layout.AreaGrid'] = 'Layout.AreaGrid' + type: Literal["Layout.AreaGrid"] = "Layout.AreaGrid" """ Must be **Layout.AreaGrid**. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the layout should be used. """ @@ -1958,11 +2125,13 @@ def with_row_spacing(self, value: Spacing) -> Self: self.row_spacing = value return self + class Column(CardElement): """A column in a ColumnSet element.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Column'] = 'Column' + type: Literal["Column"] = "Column" """ Optional. If specified, must be **Column**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -1972,7 +2141,7 @@ class Column(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -1982,19 +2151,19 @@ class Column(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the element is tapped or clicked. Action.ShowCard is not supported. """ style: Optional[ContainerStyle] = None """ The style of the container. Container styles control the colors of the background, border and text inside the container, in such a way that contrast requirements are always met. """ - show_border: Optional[bool] = None + show_border: Optional[bool] = False """ Controls if a border should be displayed around the container. """ - rounded_corners: Optional[bool] = None + rounded_corners: Optional[bool] = False """ Controls if the container should have rounded corners. """ layouts: Optional[List[SerializeAsAny[ContainerLayout]]] = None """ The layouts associated with the container. The container can dynamically switch from one layout to another as the card's width changes. See [Container layouts](topic:container-layouts) for more details. """ - bleed: Optional[bool] = None + bleed: Optional[bool] = False """ Controls if the container should bleed into its parent. A bleeding container extends into its parent's padding. """ min_height: Optional[str] = None """ The minimum height, in pixels, of the container, in the `px` format. """ @@ -2119,11 +2288,13 @@ def with_items(self, value: List[CardElement]) -> Self: self.items = value return self + class ColumnSet(CardElement): """Splits the available horizontal space into separate columns, so elements can be organized in a row.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['ColumnSet'] = 'ColumnSet' + type: Literal["ColumnSet"] = "ColumnSet" """ Must be **ColumnSet**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -2133,7 +2304,7 @@ class ColumnSet(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -2143,17 +2314,17 @@ class ColumnSet(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the element is tapped or clicked. Action.ShowCard is not supported. """ style: Optional[ContainerStyle] = None """ The style of the container. Container styles control the colors of the background, border and text inside the container, in such a way that contrast requirements are always met. """ - show_border: Optional[bool] = None + show_border: Optional[bool] = False """ Controls if a border should be displayed around the container. """ - rounded_corners: Optional[bool] = None + rounded_corners: Optional[bool] = False """ Controls if the container should have rounded corners. """ - bleed: Optional[bool] = None + bleed: Optional[bool] = False """ Controls if the container should bleed into its parent. A bleeding container extends into its parent's padding. """ min_height: Optional[str] = None """ The minimum height, in pixels, of the container, in the `px` format. """ @@ -2250,8 +2421,10 @@ def with_columns(self, value: List[Column]) -> Self: self.columns = value return self + class MediaSource(SerializableObject): """Defines the source URL of a media stream. YouTube, Dailymotion, Vimeo and Microsoft Stream URLs are supported.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ mime_type: Optional[str] = None @@ -2271,8 +2444,10 @@ def with_url(self, value: str) -> Self: self.url = value return self + class CaptionSource(SerializableObject): """Defines a source URL for a video captions.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ mime_type: Optional[str] = None @@ -2298,11 +2473,13 @@ def with_label(self, value: str) -> Self: self.label = value return self + class Media(CardElement): """A media element, that makes it possible to embed videos inside a card.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Media'] = 'Media' + type: Literal["Media"] = "Media" """ Must be **Media**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -2312,7 +2489,7 @@ class Media(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -2320,7 +2497,7 @@ class Media(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ sources: Optional[List[MediaSource]] = None """ The sources for the media. For YouTube, Dailymotion and Vimeo, only one source can be specified. """ @@ -2399,11 +2576,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class RichTextBlock(CardElement): """A rich text block that displays formatted text.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['RichTextBlock'] = 'RichTextBlock' + type: Literal["RichTextBlock"] = "RichTextBlock" """ Must be **RichTextBlock**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -2413,7 +2592,7 @@ class RichTextBlock(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -2423,7 +2602,7 @@ class RichTextBlock(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ label_for: Optional[str] = None """ The Id of the input the RichTextBlock should act as the label of. """ @@ -2494,8 +2673,10 @@ def with_inlines(self, value: Union[List[CardElement], List[str]]) -> Self: self.inlines = value return self + class ColumnDefinition(SerializableObject): """Defines a column in a Table element.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ horizontal_cell_content_alignment: Optional[HorizontalAlignment] = None @@ -2521,11 +2702,13 @@ def with_width(self, value: Union[str, float]) -> Self: self.width = value return self + class TableCell(CardElement): """Represents a cell in a table row.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['TableCell'] = 'TableCell' + type: Literal["TableCell"] = "TableCell" """ Must be **TableCell**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -2535,7 +2718,7 @@ class TableCell(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -2543,7 +2726,7 @@ class TableCell(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the element is tapped or clicked. Action.ShowCard is not supported. """ @@ -2551,7 +2734,7 @@ class TableCell(CardElement): """ The style of the container. Container styles control the colors of the background, border and text inside the container, in such a way that contrast requirements are always met. """ layouts: Optional[List[SerializeAsAny[ContainerLayout]]] = None """ The layouts associated with the container. The container can dynamically switch from one layout to another as the card's width changes. See [Container layouts](topic:container-layouts) for more details. """ - bleed: Optional[bool] = None + bleed: Optional[bool] = False """ Controls if the container should bleed into its parent. A bleeding container extends into its parent's padding. """ min_height: Optional[str] = None """ The minimum height, in pixels, of the container, in the `px` format. """ @@ -2658,11 +2841,13 @@ def with_items(self, value: List[CardElement]) -> Self: self.items = value return self + class TableRow(CardElement): """Represents a row of cells in a table.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['TableRow'] = 'TableRow' + type: Literal["TableRow"] = "TableRow" """ Must be **TableRow**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -2672,7 +2857,7 @@ class TableRow(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -2682,11 +2867,11 @@ class TableRow(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - show_border: Optional[bool] = None + show_border: Optional[bool] = False """ Controls if a border should be displayed around the container. """ - rounded_corners: Optional[bool] = None + rounded_corners: Optional[bool] = False """ Controls if the container should have rounded corners. """ style: Optional[ContainerStyle] = None """ The style of the container. Container styles control the colors of the background, border and text inside the container, in such a way that contrast requirements are always met. """ @@ -2777,11 +2962,13 @@ def with_cells(self, value: List[TableCell]) -> Self: self.cells = value return self + class Table(CardElement): """Use tables to display data in a tabular way, with rows, columns and cells.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Table'] = 'Table' + type: Literal["Table"] = "Table" """ Must be **Table**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -2791,7 +2978,7 @@ class Table(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -2801,13 +2988,13 @@ class Table(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ style: Optional[ContainerStyle] = None """ The style of the container. Container styles control the colors of the background, border and text inside the container, in such a way that contrast requirements are always met. """ - show_border: Optional[bool] = None + show_border: Optional[bool] = False """ Controls if a border should be displayed around the container. """ - rounded_corners: Optional[bool] = None + rounded_corners: Optional[bool] = False """ Controls if the container should have rounded corners. """ columns: Optional[List[ColumnDefinition]] = None """ The columns in the table. """ @@ -2926,11 +3113,13 @@ def with_rows(self, value: List[TableRow]) -> Self: self.rows = value return self + class TextBlock(CardElement): """A block of text, optionally formatted using Markdown.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['TextBlock'] = 'TextBlock' + type: Literal["TextBlock"] = "TextBlock" """ Must be **TextBlock**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -2940,7 +3129,7 @@ class TextBlock(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -2950,7 +3139,7 @@ class TextBlock(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ text: Optional[str] = None """ The text to display. A subset of markdown is supported. """ @@ -2964,7 +3153,7 @@ class TextBlock(CardElement): """ Controls whether the text should be renderer using a subtler variant of the select color. """ font_type: Optional[FontType] = None """ The type of font to use for rendering. """ - wrap: Optional[bool] = None + wrap: Optional[bool] = False """ Controls if the text should wrap. """ max_lines: Optional[float] = None """ The maximum number of lines to display. """ @@ -3069,8 +3258,10 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class Fact(SerializableObject): """A fact in a FactSet element.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ title: Optional[str] = None @@ -3090,11 +3281,13 @@ def with_value(self, value: str) -> Self: self.value = value return self + class FactSet(CardElement): """A set of facts, displayed as a table or a vertical list when horizontal space is constrained.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['FactSet'] = 'FactSet' + type: Literal["FactSet"] = "FactSet" """ Must be **FactSet**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -3104,7 +3297,7 @@ class FactSet(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -3112,7 +3305,7 @@ class FactSet(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ facts: Optional[List[Fact]] = None """ The facts in the set. """ @@ -3173,8 +3366,10 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class TeamsImageProperties(SerializableObject): """Represents a set of Teams-specific properties on an image.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ allow_expand: Optional[bool] = None @@ -3188,11 +3383,13 @@ def with_allow_expand(self, value: bool) -> Self: self.allow_expand = value return self + class Image(CardElement): """A standalone image element.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Image'] = 'Image' + type: Literal["Image"] = "Image" """ Must be **Image**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -3202,7 +3399,7 @@ class Image(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ @@ -3210,7 +3407,7 @@ class Image(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ url: Optional[str] = None """ The URL (or Base64-encoded Data URI) of the image. Acceptable formats are PNG, JPEG, GIF and SVG. """ @@ -3226,7 +3423,7 @@ class Image(CardElement): """ The width of the image. """ select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the image is tapped or clicked. Action.ShowCard is not supported. """ - allow_expand: Optional[bool] = None + allow_expand: Optional[bool] = False """ Controls if the image can be expanded to full screen. """ msteams: Optional[TeamsImageProperties] = None """ Teams-specific metadata associated with the image. """ @@ -3349,11 +3546,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class ImageSet(CardElement): """A set of images, displayed side-by-side and wrapped across multiple rows as needed.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['ImageSet'] = 'ImageSet' + type: Literal["ImageSet"] = "ImageSet" """ Must be **ImageSet**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -3363,7 +3562,7 @@ class ImageSet(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -3373,7 +3572,7 @@ class ImageSet(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ images: Optional[List[Image]] = None """ The images in the set. """ @@ -3444,11 +3643,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class TextInput(CardElement): """An input to allow the user to enter text.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Input.Text'] = 'Input.Text' + type: Literal["Input.Text"] = "Input.Text" """ Must be **Input.Text**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -3458,7 +3659,7 @@ class TextInput(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -3466,13 +3667,13 @@ class TextInput(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ label: Optional[str] = None """ The label of the input. A label should **always** be provided to ensure the best user experience especially for users of assistive technology. """ - is_required: Optional[bool] = None + is_required: Optional[bool] = False """ Controls whether the input is required. See [Input validation](topic:input-validation) for more details. """ error_message: Optional[str] = None """ The error message to display when the input fails validation. See [Input validation](topic:input-validation) for more details. """ @@ -3482,7 +3683,7 @@ class TextInput(CardElement): """ The default value of the input. """ max_length: Optional[float] = None """ The maximum length of the text in the input. """ - is_multiline: Optional[bool] = None + is_multiline: Optional[bool] = False """ Controls if the input should allow multiple lines of text. """ placeholder: Optional[str] = None """ The text to display as a placeholder when the user hasn't entered a value. """ @@ -3589,11 +3790,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class DateInput(CardElement): """An input to allow the user to select a date.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Input.Date'] = 'Input.Date' + type: Literal["Input.Date"] = "Input.Date" """ Must be **Input.Date**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -3603,7 +3806,7 @@ class DateInput(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -3611,13 +3814,13 @@ class DateInput(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ label: Optional[str] = None """ The label of the input. A label should **always** be provided to ensure the best user experience especially for users of assistive technology. """ - is_required: Optional[bool] = None + is_required: Optional[bool] = False """ Controls whether the input is required. See [Input validation](topic:input-validation) for more details. """ error_message: Optional[str] = None """ The error message to display when the input fails validation. See [Input validation](topic:input-validation) for more details. """ @@ -3716,11 +3919,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class TimeInput(CardElement): """An input to allow the user to select a time.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Input.Time'] = 'Input.Time' + type: Literal["Input.Time"] = "Input.Time" """ Must be **Input.Time**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -3730,7 +3935,7 @@ class TimeInput(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -3738,13 +3943,13 @@ class TimeInput(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ label: Optional[str] = None """ The label of the input. A label should **always** be provided to ensure the best user experience especially for users of assistive technology. """ - is_required: Optional[bool] = None + is_required: Optional[bool] = False """ Controls whether the input is required. See [Input validation](topic:input-validation) for more details. """ error_message: Optional[str] = None """ The error message to display when the input fails validation. See [Input validation](topic:input-validation) for more details. """ @@ -3843,11 +4048,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class NumberInput(CardElement): """An input to allow the user to enter a number.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Input.Number'] = 'Input.Number' + type: Literal["Input.Number"] = "Input.Number" """ Must be **Input.Number**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -3857,7 +4064,7 @@ class NumberInput(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -3865,13 +4072,13 @@ class NumberInput(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ label: Optional[str] = None """ The label of the input. A label should **always** be provided to ensure the best user experience especially for users of assistive technology. """ - is_required: Optional[bool] = None + is_required: Optional[bool] = False """ Controls whether the input is required. See [Input validation](topic:input-validation) for more details. """ error_message: Optional[str] = None """ The error message to display when the input fails validation. See [Input validation](topic:input-validation) for more details. """ @@ -3970,11 +4177,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class ToggleInput(CardElement): """An input to allow the user to select between on/off states.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Input.Toggle'] = 'Input.Toggle' + type: Literal["Input.Toggle"] = "Input.Toggle" """ Must be **Input.Toggle**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -3984,7 +4193,7 @@ class ToggleInput(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -3992,13 +4201,13 @@ class ToggleInput(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ label: Optional[str] = None """ The label of the input. A label should **always** be provided to ensure the best user experience especially for users of assistive technology. """ - is_required: Optional[bool] = None + is_required: Optional[bool] = False """ Controls whether the input is required. See [Input validation](topic:input-validation) for more details. """ error_message: Optional[str] = None """ The error message to display when the input fails validation. See [Input validation](topic:input-validation) for more details. """ @@ -4109,8 +4318,10 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class Choice(SerializableObject): """A choice as used by the Input.ChoiceSet input.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ title: Optional[str] = None @@ -4130,11 +4341,13 @@ def with_value(self, value: str) -> Self: self.value = value return self + class QueryData(SerializableObject): """Defines a query to dynamically fetch data from a Bot.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Data.Query'] = 'Data.Query' + type: Literal["Data.Query"] = "Data.Query" """ Must be **Data.Query**. """ dataset: Optional[str] = None """ The dataset from which to fetch the data. """ @@ -4165,11 +4378,13 @@ def with_skip(self, value: float) -> Self: self.skip = value return self + class ChoiceSetInput(CardElement): """An input to allow the user to select one or more values.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Input.ChoiceSet'] = 'Input.ChoiceSet' + type: Literal["Input.ChoiceSet"] = "Input.ChoiceSet" """ Must be **Input.ChoiceSet**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -4179,7 +4394,7 @@ class ChoiceSetInput(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -4187,13 +4402,13 @@ class ChoiceSetInput(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ label: Optional[str] = None """ The label of the input. A label should **always** be provided to ensure the best user experience especially for users of assistive technology. """ - is_required: Optional[bool] = None + is_required: Optional[bool] = False """ Controls whether the input is required. See [Input validation](topic:input-validation) for more details. """ error_message: Optional[str] = None """ The error message to display when the input fails validation. See [Input validation](topic:input-validation) for more details. """ @@ -4207,13 +4422,13 @@ class ChoiceSetInput(CardElement): """ A Data.Query object that defines the dataset from which to dynamically fetch the choices for the input. """ style: Optional[ChoiceSetInputStyle] = "compact" """ Controls whether the input should be displayed as a dropdown (compact) or a list of radio buttons or checkboxes (expanded). """ - is_multi_select: Optional[bool] = None + is_multi_select: Optional[bool] = False """ Controls whether multiple choices can be selected. """ placeholder: Optional[str] = None """ The text to display as a placeholder when the user has not entered any value. """ wrap: Optional[bool] = True """ Controls if choice titles should wrap. """ - use_multiple_columns: Optional[bool] = None + use_multiple_columns: Optional[bool] = False """ Controls whether choice items are arranged in multiple columns in expanded mode, or in a single column. Default is false. """ min_column_width: Optional[str] = None """ The minimum width, in pixels, for each column when using a multi-column layout. This ensures that choice items remain readable even when horizontal space is limited. Default is 100 pixels. """ @@ -4322,11 +4537,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class RatingInput(CardElement): """An input to allow the user to rate something using stars.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Input.Rating'] = 'Input.Rating' + type: Literal["Input.Rating"] = "Input.Rating" """ Must be **Input.Rating**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -4336,7 +4553,7 @@ class RatingInput(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -4344,13 +4561,13 @@ class RatingInput(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ label: Optional[str] = None """ The label of the input. A label should **always** be provided to ensure the best user experience especially for users of assistive technology. """ - is_required: Optional[bool] = None + is_required: Optional[bool] = False """ Controls whether the input is required. See [Input validation](topic:input-validation) for more details. """ error_message: Optional[str] = None """ The error message to display when the input fails validation. See [Input validation](topic:input-validation) for more details. """ @@ -4360,7 +4577,7 @@ class RatingInput(CardElement): """ The default value of the input. """ max: Optional[float] = 5 """ The number of stars to display. """ - allow_half_steps: Optional[bool] = None + allow_half_steps: Optional[bool] = False """ Controls if the user can select half stars. """ size: Optional[RatingSize] = "Large" """ The size of the stars. """ @@ -4455,11 +4672,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class Rating(CardElement): """A read-only star rating element, to display the rating of something.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Rating'] = 'Rating' + type: Literal["Rating"] = "Rating" """ Must be **Rating**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -4469,7 +4688,7 @@ class Rating(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -4479,7 +4698,7 @@ class Rating(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ value: Optional[float] = None """ The value of the rating. Must be between 0 and max. """ @@ -4574,8 +4793,10 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class IconInfo(SerializableObject): """Defines information about a Fluent icon and how it should be rendered.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ name: Optional[str] = None @@ -4607,11 +4828,13 @@ def with_color(self, value: TextColor) -> Self: self.color = value return self + class CompoundButton(CardElement): """A special type of button with an icon, title and description.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['CompoundButton'] = 'CompoundButton' + type: Literal["CompoundButton"] = "CompoundButton" """ Must be **CompoundButton**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -4621,7 +4844,7 @@ class CompoundButton(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -4631,7 +4854,7 @@ class CompoundButton(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ icon: Optional[IconInfo] = None """ The icon to show on the button. """ @@ -4720,11 +4943,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class Icon(CardElement): """A standalone icon element. Icons can be picked from the vast [Adaptive Card icon catalog](https://adaptivecards.microsoft.com/?topic=icon-catalog).""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Icon'] = 'Icon' + type: Literal["Icon"] = "Icon" """ Must be **Icon**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -4734,7 +4959,7 @@ class Icon(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ @@ -4742,7 +4967,7 @@ class Icon(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ name: Optional[str] = None """ The name of the icon to display. """ @@ -4827,11 +5052,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class CarouselPage(CardElement): """A page inside a Carousel element.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['CarouselPage'] = 'CarouselPage' + type: Literal["CarouselPage"] = "CarouselPage" """ Must be **CarouselPage**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -4845,15 +5072,15 @@ class CarouselPage(CardElement): """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the element is tapped or clicked. Action.ShowCard is not supported. """ style: Optional[ContainerStyle] = None """ The style of the container. Container styles control the colors of the background, border and text inside the container, in such a way that contrast requirements are always met. """ - show_border: Optional[bool] = None + show_border: Optional[bool] = False """ Controls if a border should be displayed around the container. """ - rounded_corners: Optional[bool] = None + rounded_corners: Optional[bool] = False """ Controls if the container should have rounded corners. """ layouts: Optional[List[SerializeAsAny[ContainerLayout]]] = None """ The layouts associated with the container. The container can dynamically switch from one layout to another as the card's width changes. See [Container layouts](topic:container-layouts) for more details. """ @@ -4958,11 +5185,13 @@ def with_items(self, value: List[CardElement]) -> Self: self.items = value return self + class Carousel(CardElement): """A carousel with sliding pages.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Carousel'] = 'Carousel' + type: Literal["Carousel"] = "Carousel" """ Must be **Carousel**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -4972,7 +5201,7 @@ class Carousel(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -4980,9 +5209,9 @@ class Carousel(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - bleed: Optional[bool] = None + bleed: Optional[bool] = False """ Controls if the container should bleed into its parent. A bleeding container extends into its parent's padding. """ min_height: Optional[str] = None """ The minimum height, in pixels, of the container, in the `px` format. """ @@ -5059,11 +5288,13 @@ def with_pages(self, value: List[CarouselPage]) -> Self: self.pages = value return self + class Badge(CardElement): """A badge element to show an icon and/or text in a compact form over a colored background.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Badge'] = 'Badge' + type: Literal["Badge"] = "Badge" """ Must be **Badge**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -5073,7 +5304,7 @@ class Badge(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -5083,7 +5314,7 @@ class Badge(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ text: Optional[str] = None """ The text to display. """ @@ -5190,11 +5421,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class ProgressRing(CardElement): """A spinning ring element, to indicate progress.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['ProgressRing'] = 'ProgressRing' + type: Literal["ProgressRing"] = "ProgressRing" """ Must be **ProgressRing**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -5204,7 +5437,7 @@ class ProgressRing(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -5214,7 +5447,7 @@ class ProgressRing(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ label: Optional[str] = None """ The label of the progress ring. """ @@ -5291,11 +5524,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class ProgressBar(CardElement): """A progress bar element, to represent a value within a range.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['ProgressBar'] = 'ProgressBar' + type: Literal["ProgressBar"] = "ProgressBar" """ Must be **ProgressBar**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -5305,7 +5540,7 @@ class ProgressBar(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -5315,7 +5550,7 @@ class ProgressBar(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ value: Optional[float] = None """ The value of the progress bar. Must be between 0 and max. """ @@ -5392,13 +5627,15 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class DonutChartData(SerializableObject): """A data point in a Donut chart.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ legend: Optional[str] = None """ The legend of the chart. """ - value: Optional[float] = None + value: Optional[float] = 0 """ The value associated with the data point. """ color: Optional[ChartColor] = None """ The color to use for the data point. See [Chart colors reference](topic:chart-colors-reference). """ @@ -5419,11 +5656,13 @@ def with_color(self, value: ChartColor) -> Self: self.color = value return self + class DonutChart(CardElement): """A donut chart.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Chart.Donut'] = 'Chart.Donut' + type: Literal["Chart.Donut"] = "Chart.Donut" """ Must be **Chart.Donut**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -5433,7 +5672,7 @@ class DonutChart(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -5443,11 +5682,11 @@ class DonutChart(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ title: Optional[str] = None """ The title of the chart. """ - show_title: Optional[bool] = None + show_title: Optional[bool] = False """ Controls whether the chart's title should be displayed. Defaults to `false`. """ color_set: Optional[ChartColorSet] = None """ The name of the set of colors to use to render the chart. See [Chart colors reference](topic:chart-colors-reference). """ @@ -5562,11 +5801,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class PieChart(CardElement): """A pie chart.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Chart.Pie'] = 'Chart.Pie' + type: Literal["Chart.Pie"] = "Chart.Pie" """ Must be **Chart.Pie**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -5576,7 +5817,7 @@ class PieChart(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -5586,11 +5827,11 @@ class PieChart(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ title: Optional[str] = None """ The title of the chart. """ - show_title: Optional[bool] = None + show_title: Optional[bool] = False """ Controls whether the chart's title should be displayed. Defaults to `false`. """ color_set: Optional[ChartColorSet] = None """ The name of the set of colors to use to render the chart. See [Chart colors reference](topic:chart-colors-reference). """ @@ -5705,13 +5946,15 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class BarChartDataValue(SerializableObject): """A single data point in a bar chart.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ x: Optional[str] = None """ The x axis value of the data point. """ - y: Optional[float] = None + y: Optional[float] = 0 """ The y axis value of the data point. """ def with_key(self, value: str) -> Self: @@ -5726,8 +5969,10 @@ def with_y(self, value: float) -> Self: self.y = value return self + class GroupedVerticalBarChartData(SerializableObject): """Represents a series of data points.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ legend: Optional[str] = None @@ -5753,11 +5998,13 @@ def with_color(self, value: ChartColor) -> Self: self.color = value return self + class GroupedVerticalBarChart(CardElement): """A grouped vertical bar chart.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Chart.VerticalBar.Grouped'] = 'Chart.VerticalBar.Grouped' + type: Literal["Chart.VerticalBar.Grouped"] = "Chart.VerticalBar.Grouped" """ Must be **Chart.VerticalBar.Grouped**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -5767,7 +6014,7 @@ class GroupedVerticalBarChart(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -5777,11 +6024,11 @@ class GroupedVerticalBarChart(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ title: Optional[str] = None """ The title of the chart. """ - show_title: Optional[bool] = None + show_title: Optional[bool] = False """ Controls whether the chart's title should be displayed. Defaults to `false`. """ color_set: Optional[ChartColorSet] = None """ The name of the set of colors to use to render the chart. See [Chart colors reference](topic:chart-colors-reference). """ @@ -5795,13 +6042,13 @@ class GroupedVerticalBarChart(CardElement): """ The title of the y axis. """ color: Optional[ChartColor] = None """ The color to use for all data points. See [Chart colors reference](topic:chart-colors-reference). """ - stacked: Optional[bool] = None + stacked: Optional[bool] = False """ Controls if bars in the chart should be displayed as stacks instead of groups. **Note:** stacked vertical bar charts do not support custom Y ranges nor negative Y values. """ data: Optional[List[GroupedVerticalBarChartData]] = None """ The data points in a series. """ - show_bar_values: Optional[bool] = None + show_bar_values: Optional[bool] = False """ Controls if values should be displayed on each bar. """ y_min: Optional[float] = None """ The requested minimum for the Y axis range. The value used at runtime may be different to optimize visual presentation. @@ -5920,13 +6167,15 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class VerticalBarChartDataValue(SerializableObject): """Represents a data point in a vertical bar chart.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ x: Optional[Union[str, float]] = None """ The x axis value of the data point. """ - y: Optional[float] = None + y: Optional[float] = 0 """ The y axis value of the data point. """ color: Optional[ChartColor] = None """ The color to use for the bar associated with the data point. See [Chart colors reference](topic:chart-colors-reference). """ @@ -5947,11 +6196,13 @@ def with_color(self, value: ChartColor) -> Self: self.color = value return self + class VerticalBarChart(CardElement): """A vertical bar chart.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Chart.VerticalBar'] = 'Chart.VerticalBar' + type: Literal["Chart.VerticalBar"] = "Chart.VerticalBar" """ Must be **Chart.VerticalBar**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -5961,7 +6212,7 @@ class VerticalBarChart(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -5971,11 +6222,11 @@ class VerticalBarChart(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ title: Optional[str] = None """ The title of the chart. """ - show_title: Optional[bool] = None + show_title: Optional[bool] = False """ Controls whether the chart's title should be displayed. Defaults to `false`. """ color_set: Optional[ChartColorSet] = None """ The name of the set of colors to use to render the chart. See [Chart colors reference](topic:chart-colors-reference). """ @@ -5991,7 +6242,7 @@ class VerticalBarChart(CardElement): """ The data to display in the chart. """ color: Optional[ChartColor] = None """ The color to use for all data points. See [Chart colors reference](topic:chart-colors-reference). """ - show_bar_values: Optional[bool] = None + show_bar_values: Optional[bool] = False """ Controls if the bar values should be displayed. """ y_min: Optional[float] = None """ The requested minimum for the Y axis range. The value used at runtime may be different to optimize visual presentation. """ @@ -6102,13 +6353,15 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class HorizontalBarChartDataValue(SerializableObject): """Represents a single data point in a horizontal bar chart.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ x: Optional[str] = None """ The x axis value of the data point. """ - y: Optional[float] = None + y: Optional[float] = 0 """ The y axis value of the data point. """ color: Optional[ChartColor] = None """ The color of the bar associated with the data point. See [Chart colors reference](topic:chart-colors-reference). """ @@ -6129,11 +6382,13 @@ def with_color(self, value: ChartColor) -> Self: self.color = value return self + class HorizontalBarChart(CardElement): """A horizontal bar chart.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Chart.HorizontalBar'] = 'Chart.HorizontalBar' + type: Literal["Chart.HorizontalBar"] = "Chart.HorizontalBar" """ Must be **Chart.HorizontalBar**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -6143,7 +6398,7 @@ class HorizontalBarChart(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -6153,11 +6408,11 @@ class HorizontalBarChart(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ title: Optional[str] = None """ The title of the chart. """ - show_title: Optional[bool] = None + show_title: Optional[bool] = False """ Controls whether the chart's title should be displayed. Defaults to `false`. """ color_set: Optional[ChartColorSet] = None """ The name of the set of colors to use to render the chart. See [Chart colors reference](topic:chart-colors-reference). """ @@ -6272,13 +6527,15 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class StackedHorizontalBarChartDataPoint(SerializableObject): """A data point in a series.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ legend: Optional[str] = None """ The legend associated with the data point. """ - value: Optional[float] = None + value: Optional[float] = 0 """ The value of the data point. """ color: Optional[ChartColor] = None """ The color to use to render the bar associated with the data point. See [Chart colors reference](topic:chart-colors-reference). """ @@ -6299,8 +6556,10 @@ def with_color(self, value: ChartColor) -> Self: self.color = value return self + class StackedHorizontalBarChartData(SerializableObject): """Defines the collection of data series to display in as a stacked horizontal bar chart.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ title: Optional[str] = None @@ -6320,11 +6579,13 @@ def with_data(self, value: List[StackedHorizontalBarChartDataPoint]) -> Self: self.data = value return self + class StackedHorizontalBarChart(CardElement): """A stacked horizontal bar chart.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Chart.HorizontalBar.Stacked'] = 'Chart.HorizontalBar.Stacked' + type: Literal["Chart.HorizontalBar.Stacked"] = "Chart.HorizontalBar.Stacked" """ Must be **Chart.HorizontalBar.Stacked**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -6334,7 +6595,7 @@ class StackedHorizontalBarChart(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -6344,11 +6605,11 @@ class StackedHorizontalBarChart(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ title: Optional[str] = None """ The title of the chart. """ - show_title: Optional[bool] = None + show_title: Optional[bool] = False """ Controls whether the chart's title should be displayed. Defaults to `false`. """ color_set: Optional[ChartColorSet] = None """ The name of the set of colors to use to render the chart. See [Chart colors reference](topic:chart-colors-reference). """ @@ -6457,8 +6718,10 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class LineChartValue(SerializableObject): """Represents a single data point in a line chart.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ x: Optional[Union[float, str]] = None @@ -6467,7 +6730,7 @@ class LineChartValue(SerializableObject): If all x values of the x [Chart.Line](topic:Chart.Line) are expressed as a number, or if all x values are expressed as a date string in the `YYYY-MM-DD` format, the chart will be rendered as a time series chart, i.e. x axis values will span across the minimum x value to maximum x value range. Otherwise, if x values are represented as a mix of numbers and strings or if at least one x value isn't in the `YYYY-MM-DD` format, the chart will be rendered as a categorical chart, i.e. x axis values will be displayed as categories. """ - y: Optional[float] = None + y: Optional[float] = 0 """ The y axis value of the data point. """ def with_key(self, value: str) -> Self: @@ -6482,8 +6745,10 @@ def with_y(self, value: float) -> Self: self.y = value return self + class LineChartData(SerializableObject): """Represents a collection of data points series in a line chart.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ legend: Optional[str] = None @@ -6509,11 +6774,13 @@ def with_color(self, value: ChartColor) -> Self: self.color = value return self + class LineChart(CardElement): """A line chart.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Chart.Line'] = 'Chart.Line' + type: Literal["Chart.Line"] = "Chart.Line" """ Must be **Chart.Line**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -6523,7 +6790,7 @@ class LineChart(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -6533,11 +6800,11 @@ class LineChart(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ title: Optional[str] = None """ The title of the chart. """ - show_title: Optional[bool] = None + show_title: Optional[bool] = False """ Controls whether the chart's title should be displayed. Defaults to `false`. """ color_set: Optional[ChartColorSet] = None """ The name of the set of colors to use to render the chart. See [Chart colors reference](topic:chart-colors-reference). """ @@ -6658,11 +6925,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class GaugeChartLegend(SerializableObject): """The legend of the chart.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - size: Optional[float] = None + size: Optional[float] = 0 """ The size of the segment. """ legend: Optional[str] = None """ The legend text associated with the segment. """ @@ -6685,11 +6954,13 @@ def with_color(self, value: ChartColor) -> Self: self.color = value return self + class GaugeChart(CardElement): """A gauge chart.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Chart.Gauge'] = 'Chart.Gauge' + type: Literal["Chart.Gauge"] = "Chart.Gauge" """ Must be **Chart.Gauge**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -6699,7 +6970,7 @@ class GaugeChart(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -6709,11 +6980,11 @@ class GaugeChart(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ title: Optional[str] = None """ The title of the chart. """ - show_title: Optional[bool] = None + show_title: Optional[bool] = False """ Controls whether the chart's title should be displayed. Defaults to `false`. """ color_set: Optional[ChartColorSet] = None """ The name of the set of colors to use to render the chart. See [Chart colors reference](topic:chart-colors-reference). """ @@ -6721,7 +6992,7 @@ class GaugeChart(CardElement): """ The maximum width, in pixels, of the chart, in the `px` format. """ show_legend: Optional[bool] = True """ Controls whether the chart's legend should be displayed. """ - min: Optional[float] = None + min: Optional[float] = 0 """ The minimum value of the gauge. """ max: Optional[float] = None """ The maximum value of the gauge. """ @@ -6735,7 +7006,7 @@ class GaugeChart(CardElement): """ Controls whether the outlines of the gauge segments are displayed. """ segments: Optional[List[GaugeChartLegend]] = None """ The segments to display in the gauge. """ - value: Optional[float] = None + value: Optional[float] = 0 """ The value of the gauge. """ value_format: Optional[GaugeChartValueFormat] = "Percentage" """ The format used to display the gauge's value. """ @@ -6852,11 +7123,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class CodeBlock(CardElement): """A formatted and syntax-colored code block.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['CodeBlock'] = 'CodeBlock' + type: Literal["CodeBlock"] = "CodeBlock" """ Must be **CodeBlock**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -6866,7 +7139,7 @@ class CodeBlock(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -6876,7 +7149,7 @@ class CodeBlock(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ code_snippet: Optional[str] = None """ The code snippet to display. """ @@ -6953,8 +7226,10 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class PersonaProperties(SerializableObject): """Represents the properties of a Persona component.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ id: Optional[str] = None @@ -6992,11 +7267,13 @@ def with_style(self, value: PersonaDisplayStyle) -> Self: self.style = value return self + class ComUserMicrosoftGraphComponent(CardElement): """Displays a user's information, including their profile picture.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Component'] = 'Component' + type: Literal["Component"] = "Component" """ Must be **Component**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -7006,7 +7283,7 @@ class ComUserMicrosoftGraphComponent(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -7016,9 +7293,9 @@ class ComUserMicrosoftGraphComponent(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - name: Literal['graph.microsoft.com/user'] = 'graph.microsoft.com/user' + name: Literal["graph.microsoft.com/user"] = "graph.microsoft.com/user" """ Must be **graph.microsoft.com/user**. """ properties: Optional[PersonaProperties] = None """ The properties of the Persona component. """ @@ -7083,8 +7360,10 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class PersonaSetProperties(SerializableObject): """Represents the properties of a PersonaSet component.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ users: Optional[List[PersonaProperties]] = None @@ -7110,11 +7389,13 @@ def with_style(self, value: PersonaDisplayStyle) -> Self: self.style = value return self + class ComUsersMicrosoftGraphComponent(CardElement): """Displays multiple users' information, including their profile pictures.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Component'] = 'Component' + type: Literal["Component"] = "Component" """ Must be **Component**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -7124,7 +7405,7 @@ class ComUsersMicrosoftGraphComponent(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -7134,9 +7415,9 @@ class ComUsersMicrosoftGraphComponent(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - name: Literal['graph.microsoft.com/users'] = 'graph.microsoft.com/users' + name: Literal["graph.microsoft.com/users"] = "graph.microsoft.com/users" """ Must be **graph.microsoft.com/users**. """ properties: Optional[PersonaSetProperties] = None """ The properties of the PersonaSet component. """ @@ -7201,8 +7482,10 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class ResourceVisualization(SerializableObject): """Represents a visualization of a resource.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ media: Optional[str] = None @@ -7216,8 +7499,10 @@ def with_media(self, value: str) -> Self: self.media = value return self + class ResourceProperties(SerializableObject): """Represents the properties of a resource component.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ id: Optional[str] = None @@ -7243,11 +7528,13 @@ def with_resource_visualization(self, value: ResourceVisualization) -> Self: self.resource_visualization = value return self + class ComResourceMicrosoftGraphComponent(CardElement): """Displays information about a generic graph resource.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Component'] = 'Component' + type: Literal["Component"] = "Component" """ Must be **Component**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -7257,7 +7544,7 @@ class ComResourceMicrosoftGraphComponent(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -7267,9 +7554,9 @@ class ComResourceMicrosoftGraphComponent(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - name: Literal['graph.microsoft.com/resource'] = 'graph.microsoft.com/resource' + name: Literal["graph.microsoft.com/resource"] = "graph.microsoft.com/resource" """ Must be **graph.microsoft.com/resource**. """ properties: Optional[ResourceProperties] = None """ The properties of the resource. """ @@ -7334,8 +7621,10 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class FileProperties(SerializableObject): """Represents the properties of a file component.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ name: Optional[str] = None @@ -7361,11 +7650,13 @@ def with_url(self, value: str) -> Self: self.url = value return self + class ComFileMicrosoftGraphComponent(CardElement): """Displays information about a file resource.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Component'] = 'Component' + type: Literal["Component"] = "Component" """ Must be **Component**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -7375,7 +7666,7 @@ class ComFileMicrosoftGraphComponent(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -7385,9 +7676,9 @@ class ComFileMicrosoftGraphComponent(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - name: Literal['graph.microsoft.com/file'] = 'graph.microsoft.com/file' + name: Literal["graph.microsoft.com/file"] = "graph.microsoft.com/file" """ Must be **graph.microsoft.com/file**. """ properties: Optional[FileProperties] = None """ The properties of the file. """ @@ -7452,8 +7743,10 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class CalendarEventAttendee(SerializableObject): """Represents a calendar event attendee.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ name: Optional[str] = None @@ -7491,8 +7784,10 @@ def with_status(self, value: str) -> Self: self.status = value return self + class CalendarEventProperties(SerializableObject): """The properties of a calendar event.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ id: Optional[str] = None @@ -7572,11 +7867,13 @@ def with_organizer(self, value: CalendarEventAttendee) -> Self: self.organizer = value return self + class ComEventMicrosoftGraphComponent(CardElement): """Displays information about a calendar event.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['Component'] = 'Component' + type: Literal["Component"] = "Component" """ Must be **Component**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -7586,7 +7883,7 @@ class ComEventMicrosoftGraphComponent(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None + separator: Optional[bool] = False """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ @@ -7596,9 +7893,9 @@ class ComEventMicrosoftGraphComponent(CardElement): """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - name: Literal['graph.microsoft.com/event'] = 'graph.microsoft.com/event' + name: Literal["graph.microsoft.com/event"] = "graph.microsoft.com/event" """ Must be **graph.microsoft.com/event**. """ properties: Optional[CalendarEventProperties] = None """ The properties of the event. """ @@ -7663,11 +7960,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class TextRun(CardElement): """A block of text inside a RichTextBlock element.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['TextRun'] = 'TextRun' + type: Literal["TextRun"] = "TextRun" """ Must be **TextRun**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -7675,7 +7974,7 @@ class TextRun(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ text: Optional[str] = None """ The text to display. A subset of markdown is supported. """ @@ -7689,13 +7988,13 @@ class TextRun(CardElement): """ Controls whether the text should be renderer using a subtler variant of the select color. """ font_type: Optional[FontType] = None """ The type of font to use for rendering. """ - italic: Optional[bool] = None + italic: Optional[bool] = False """ Controls if the text should be italicized. """ - strikethrough: Optional[bool] = None + strikethrough: Optional[bool] = False """ Controls if the text should be struck through. """ - highlight: Optional[bool] = None + highlight: Optional[bool] = False """ Controls if the text should be highlighted. """ - underline: Optional[bool] = None + underline: Optional[bool] = False """ Controls if the text should be underlined. """ select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the text is tapped or clicked. Action.ShowCard is not supported. """ @@ -7776,11 +8075,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class IconRun(CardElement): """An inline icon inside a RichTextBlock element.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['IconRun'] = 'IconRun' + type: Literal["IconRun"] = "IconRun" """ Must be **IconRun**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -7788,7 +8089,7 @@ class IconRun(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ name: Optional[str] = None """ The name of the inline icon to display. """ @@ -7853,11 +8154,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class ImageRun(CardElement): """An inline image inside a RichTextBlock element.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['ImageRun'] = 'ImageRun' + type: Literal["ImageRun"] = "ImageRun" """ Must be **ImageRun**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ @@ -7865,7 +8168,7 @@ class ImageRun(CardElement): """ The locale associated with the element. """ is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - is_sort_key: Optional[bool] = None + is_sort_key: Optional[bool] = False """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ url: Optional[str] = None """ The URL (or Base64-encoded Data URI) of the image. Acceptable formats are PNG, JPEG, GIF and SVG. """ @@ -7930,11 +8233,13 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + class ImBackSubmitActionData(SerializableObject): """Represents Teams-specific data in an Action.Submit to send an Instant Message back to the Bot.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['imBack'] = 'imBack' + type: Literal["imBack"] = "imBack" """ Must be **imBack**. """ value: Optional[str] = None """ The value that will be sent to the Bot. """ @@ -7947,8 +8252,10 @@ def with_value(self, value: str) -> Self: self.value = value return self + class TabInfo(SerializableObject): """Represents information about the iFrame content, rendered in the collab stage popout window.""" + name: Optional[str] = None """ The name for the content. This will be displayed as the title of the window hosting the iFrame. """ content_url: Optional[str] = None @@ -7974,9 +8281,11 @@ def with_website_url(self, value: str) -> Self: self.website_url = value return self + class CollabStageInvokeDataValue(SerializableObject): """Data for invoking a collaboration stage action.""" - type: Literal['tab/tabInfoAction'] = 'tab/tabInfoAction' + + type: Literal["tab/tabInfoAction"] = "tab/tabInfoAction" """ Must be **tab/tabInfoAction**. """ tab_info: Optional[TabInfo] = None """ Provides information about the iFrame content, rendered in the collab stage popout window. """ @@ -7985,11 +8294,13 @@ def with_tab_info(self, value: TabInfo) -> Self: self.tab_info = value return self + class InvokeSubmitActionData(SerializableObject): """Represents Teams-specific data in an Action.Submit to make an Invoke request to the Bot.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['invoke'] = 'invoke' + type: Literal["invoke"] = "invoke" """ Must be **invoke**. """ value: Optional[Union[Dict[str, Any], CollabStageInvokeDataValue]] = None """ The object to send to the Bot with the Invoke request. Can be strongly typed as one of the below values to trigger a specific action in Teams. """ @@ -8002,11 +8313,13 @@ def with_value(self, value: Union[Dict[str, Any], CollabStageInvokeDataValue]) - self.value = value return self + class MessageBackSubmitActionData(SerializableObject): """Represents Teams-specific data in an Action.Submit to send a message back to the Bot.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['messageBack'] = 'messageBack' + type: Literal["messageBack"] = "messageBack" """ Must be **messageBack**. """ text: Optional[str] = None """ The text that will be sent to the Bot. """ @@ -8031,11 +8344,13 @@ def with_value(self, value: Dict[str, Any]) -> Self: self.value = value return self + class SigninSubmitActionData(SerializableObject): """Represents Teams-specific data in an Action.Submit to sign in a user.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['signin'] = 'signin' + type: Literal["signin"] = "signin" """ Must be **signin**. """ value: Optional[str] = None """ The URL to redirect the end-user for signing in. """ @@ -8048,11 +8363,13 @@ def with_value(self, value: str) -> Self: self.value = value return self + class TaskFetchSubmitActionData(SerializableObject): """Represents Teams-specific data in an Action.Submit to open a task module.""" + key: Optional[str] = None """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ - type: Literal['task/fetch'] = 'task/fetch' + type: Literal["task/fetch"] = "task/fetch" """ Must be **task/fetch**. """ def with_key(self, value: str) -> Self: From 2204b310b5717cd72c8ba982f2ed839245a9a097 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:27:44 -0700 Subject: [PATCH 06/17] Bump vite from 6.4.1 to 6.4.2 in /examples/tab/Web (#364) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.4.1 to 6.4.2.
Release notes

Sourced from vite's releases.

v6.4.2

Please refer to CHANGELOG.md for details.

Changelog

Sourced from vite's changelog.

6.4.2 (2026-04-06)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=6.4.1&new-version=6.4.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/teams.py/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/tab/Web/package-lock.json | 8 ++++---- examples/tab/Web/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/tab/Web/package-lock.json b/examples/tab/Web/package-lock.json index 969cfa175..cab0c455a 100644 --- a/examples/tab/Web/package-lock.json +++ b/examples/tab/Web/package-lock.json @@ -18,7 +18,7 @@ "@vitejs/plugin-react": "^4.3.4", "rimraf": "^6.0.1", "typescript": "^5.4.5", - "vite": "^6.4.1" + "vite": "^6.4.2" } }, "node_modules/@azure/msal-browser": { @@ -2531,9 +2531,9 @@ } }, "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/examples/tab/Web/package.json b/examples/tab/Web/package.json index 5d4906f23..41009c752 100644 --- a/examples/tab/Web/package.json +++ b/examples/tab/Web/package.json @@ -24,6 +24,6 @@ "@vitejs/plugin-react": "^4.3.4", "rimraf": "^6.0.1", "typescript": "^5.4.5", - "vite": "^6.4.1" + "vite": "^6.4.2" } } \ No newline at end of file From 7ec00098b67260f40c575a3b55b22ae72632df9b Mon Sep 17 00:00:00 2001 From: Aamir Jawaid <48929123+heyitsaamir@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:49:47 -0700 Subject: [PATCH 07/17] Remove dead HttpPlugin and jwt_middleware code (#367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two minor corrections surfaced during review of the `HttpPlugin` / JWT middleware removal PR. - **`.github/copilot-instructions.md`**: Corrects validation path `tests/echo` → `examples/echo`; sample apps live under `examples/`, not `tests/` - **`packages/apps/tests/test_function_context.py`**: Renames `mock_http` fixture docstring from `"Create a mock HTTP server."` to `"Create a mock activity sender."` — the fixture only wires up `.send()`, never an HTTP server --------- Co-authored-by: Claude Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 4 +- .../src/microsoft_teams/apps/auth/__init__.py | 3 +- .../apps/auth/jwt_middleware.py | 72 ---- .../src/microsoft_teams/apps/http_plugin.py | 327 ------------------ packages/apps/tests/test_function_context.py | 2 +- packages/apps/tests/test_jwt_middleware.py | 154 --------- 6 files changed, 4 insertions(+), 558 deletions(-) delete mode 100644 packages/apps/src/microsoft_teams/apps/auth/jwt_middleware.py delete mode 100644 packages/apps/src/microsoft_teams/apps/http_plugin.py delete mode 100644 packages/apps/tests/test_jwt_middleware.py diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 77ed2a161..5a6af1040 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -72,11 +72,11 @@ Microsoft Teams SDK for Python is a comprehensive SDK for building Microsoft Tea **ALWAYS manually validate changes by running at least one complete test application scenario:** ### Basic Teams App Validation -1. Navigate to test app: `cd tests/echo` +1. Navigate to test app: `cd examples/echo` 2. Start the app: `python src/main.py` 3. **Expected output**: App starts on ports 3978 and 3979 with logs: ``` - [INFO] @teams/app.HttpPlugin listening on port 3978 🚀 + [INFO] Teams app started successfully [INFO] @teams/app.DevToolsPlugin listening on port 3979 🚀 ``` 4. **Test endpoints**: diff --git a/packages/apps/src/microsoft_teams/apps/auth/__init__.py b/packages/apps/src/microsoft_teams/apps/auth/__init__.py index 46b58cfe9..f64ff6335 100644 --- a/packages/apps/src/microsoft_teams/apps/auth/__init__.py +++ b/packages/apps/src/microsoft_teams/apps/auth/__init__.py @@ -3,8 +3,7 @@ Licensed under the MIT License. """ -from .jwt_middleware import create_jwt_validation_middleware from .remote_function_jwt_middleware import validate_remote_function_request from .token_validator import TokenValidator -__all__ = ["TokenValidator", "create_jwt_validation_middleware", "validate_remote_function_request"] +__all__ = ["TokenValidator", "validate_remote_function_request"] diff --git a/packages/apps/src/microsoft_teams/apps/auth/jwt_middleware.py b/packages/apps/src/microsoft_teams/apps/auth/jwt_middleware.py deleted file mode 100644 index e56a8a145..000000000 --- a/packages/apps/src/microsoft_teams/apps/auth/jwt_middleware.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -import logging -from typing import Awaitable, Callable - -import jwt -from fastapi import HTTPException, Request, Response -from microsoft_teams.api.auth.json_web_token import JsonWebToken - -from .token_validator import TokenValidator - -logger = logging.getLogger(__name__) - - -def create_jwt_validation_middleware( - app_id: str, - paths: list[str], -): - """ - Create JWT validation middleware instance. - - Args: - app_id: Bot's Microsoft App ID for audience validation - paths: List of paths to validate - - Returns: - Middleware function that can be added to FastAPI app - """ - # Create service token validator - token_validator = TokenValidator.for_service(app_id) - - async def middleware(request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response: - """JWT validation middleware function.""" - # Only validate specified paths - if request.url.path not in paths: - return await call_next(request) - - # Extract Bearer token - authorization = request.headers.get("authorization") - if not authorization or not authorization.startswith("Bearer "): - logger.warning("Unauthorized request - missing or invalid authorization header") - raise HTTPException(status_code=401, detail="unauthorized") - - raw_token = authorization.removeprefix("Bearer ") - - try: - # Parse request body to get service URL for validation - body = await request.json() - service_url = body.get("serviceUrl") - - # Validate token - await token_validator.validate_token(raw_token, service_url) - validated_token = JsonWebToken(value=raw_token) - - logger.debug(f"Validated service token for activity {body.get('id', 'unknown')}") - - # Store validated token in request state - request.state.validated_token = validated_token - - return await call_next(request) - - except jwt.InvalidTokenError as e: - logger.warning(f"JWT token validation failed: {e}") - raise HTTPException(status_code=401, detail="unauthorized") from e - except Exception as e: - logger.error(f"Unexpected error during token validation: {e}") - raise HTTPException(status_code=500, detail="internal server error") from e - - return middleware diff --git a/packages/apps/src/microsoft_teams/apps/http_plugin.py b/packages/apps/src/microsoft_teams/apps/http_plugin.py deleted file mode 100644 index 534d68bc2..000000000 --- a/packages/apps/src/microsoft_teams/apps/http_plugin.py +++ /dev/null @@ -1,327 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -import importlib.metadata -import inspect -import logging -from contextlib import AsyncExitStack, asynccontextmanager -from pathlib import Path -from types import SimpleNamespace -from typing import ( - Annotated, - Any, - AsyncGenerator, - Awaitable, - Callable, - Dict, - Optional, - TypedDict, - Union, - Unpack, - cast, -) - -import uvicorn -from fastapi import FastAPI, Request, Response -from fastapi.staticfiles import StaticFiles -from microsoft_teams.api import ( - Credentials, - InvokeResponse, - TokenProtocol, -) -from pydantic import BaseModel -from starlette.applications import Starlette -from starlette.types import Lifespan - -from .auth import create_jwt_validation_middleware -from .events import ActivityEvent, CoreActivity, ErrorEvent -from .plugins import ( - DependencyMetadata, - EventMetadata, - PluginActivityResponseEvent, - PluginBase, - PluginStartEvent, -) -from .plugins.metadata import Plugin - -version = importlib.metadata.version("microsoft-teams-apps") - -logger = logging.getLogger(__name__) - - -class HttpPluginOptions(TypedDict, total=False): - """Options for configuring the HTTP plugin.""" - - skip_auth: bool - server_factory: Callable[[FastAPI], uvicorn.Server] - - -@Plugin(name="http", version=version, description="the default plugin for receiving activities via HTTP") -class HttpPlugin(PluginBase): - """ - Basic HTTP plugin that provides a FastAPI server for Teams activities. - Handles HTTP server setup, routing, and authentication. - """ - - credentials: Annotated[Optional[Credentials], DependencyMetadata(optional=True)] - - on_error_event: Annotated[Callable[[ErrorEvent], None], EventMetadata(name="error")] - on_activity_event: Annotated[Callable[[ActivityEvent], InvokeResponse[Any]], EventMetadata(name="activity")] - - lifespans: list[Lifespan[Starlette]] = [] - - def __init__(self, **options: Unpack[HttpPluginOptions]): - """ - Args: - skip_auth: Whether to skip JWT validation. - server_factory: Optional function that takes an ASGI app - and returns a configured `uvicorn.Server`. - Example: - ```python - def custom_server_factory(app: FastAPI) -> uvicorn.Server: - return uvicorn.Server(config=uvicorn.Config(app, host="0.0.0.0", port=8000)) - - - http_plugin = HttpPlugin(server_factory=custom_server_factory) - ``` - """ - super().__init__() - self._port: Optional[int] = None - self._skip_auth: bool = options.get("skip_auth", False) - self._server: Optional[uvicorn.Server] = None - self._on_ready_callback: Optional[Callable[[], Awaitable[None]]] = None - self._on_stopped_callback: Optional[Callable[[], Awaitable[None]]] = None - - # Setup FastAPI app with lifespan - @asynccontextmanager - async def default_lifespan(_app: Starlette) -> AsyncGenerator[None, None]: - # Startup - logger.info(f"listening on port {self._port} 🚀") - if self._on_ready_callback: - await self._on_ready_callback() - yield - # Shutdown - logger.info("Server shutting down") - if self._on_stopped_callback: - await self._on_stopped_callback() - - @asynccontextmanager - async def combined_lifespan(app: Starlette): - async with AsyncExitStack() as stack: - lifespans = self.lifespans.copy() - lifespans.append(default_lifespan) - for lifespan in lifespans: - await stack.enter_async_context(lifespan(app)) - yield - - self.app = FastAPI(lifespan=combined_lifespan) - - # Create uvicorn server if user provides custom factory method - server_factory = options.get("server_factory") - if server_factory: - self._server = server_factory(self.app) - if self._server.config.app is not self.app: - raise ValueError( - "server_factory must return a uvicorn.Server configured with the provided FastAPI app instance." - ) - - # Expose FastAPI routing methods (like TypeScript exposes Express methods) - self.get = self.app.get - self.post = self.app.post - self.put = self.app.put - self.patch = self.app.patch - self.delete = self.app.delete - self.middleware = self.app.middleware - - # Setup routes and error handlers - self._setup_routes() - - @property - def on_ready_callback(self) -> Optional[Callable[[], Awaitable[None]]]: - """Callback to call when HTTP server is ready.""" - return self._on_ready_callback - - @on_ready_callback.setter - def on_ready_callback(self, callback: Optional[Callable[[], Awaitable[None]]]) -> None: - """Set callback to call when HTTP server is ready.""" - self._on_ready_callback = callback - - @property - def on_stopped_callback(self) -> Optional[Callable[[], Awaitable[None]]]: - """Callback to call when HTTP server is stopped.""" - return self._on_stopped_callback - - @on_stopped_callback.setter - def on_stopped_callback(self, callback: Optional[Callable[[], Awaitable[None]]]) -> None: - """Set callback to call when HTTP server is stopped.""" - self._on_stopped_callback = callback - - async def on_init(self) -> None: - """ - Initialize the HTTP plugin when the app starts. - This adds JWT validation middleware unless `skip_auth` is True. - """ - - # Add JWT validation middleware - app_id = getattr(self.credentials, "client_id", None) - if app_id and not self._skip_auth: - jwt_middleware = create_jwt_validation_middleware(app_id=app_id, paths=["/api/messages"]) - self.app.middleware("http")(jwt_middleware) - - async def on_start(self, event: PluginStartEvent) -> None: - """Start the HTTP server.""" - self._port = event.port - - try: - if self._server and self._server.config.port != self._port: - logger.warning( - "Using port configured by server factory: %d, but plugin start event has port %d.", - self._server.config.port, - self._port, - ) - self._port = self._server.config.port - else: - config = uvicorn.Config(app=self.app, host="0.0.0.0", port=self._port, log_level="info") - self._server = uvicorn.Server(config) - - logger.info("Starting HTTP server on port %d", self._port) - - # The lifespan handler will call the callback when the server is ready - await self._server.serve() - - except OSError as error: - # Handle port in use, permission errors, etc. - logger.error("Server startup failed: %s", error) - raise - except Exception as error: - logger.error("Failed to start server: %s", error) - raise - - async def on_stop(self) -> None: - """Stop the HTTP server.""" - if self._server: - logger.info("Stopping HTTP server") - self._server.should_exit = True - - async def on_activity_response(self, event: PluginActivityResponseEvent) -> None: - """ - Complete a pending activity response. - - This is called when the App finishes processing an activity - and is ready to send the HTTP response back. - - Args: - activity_id: The ID of the activity to respond to - response_data: The response data to send back - plugin: The plugin that sent the response - """ - logger.debug(f"Completing activity response for {event.activity.id}") - - async def _process_activity(self, core_activity: CoreActivity, token: TokenProtocol) -> InvokeResponse[Any]: - """ - Process an activity via the registered handler. - - Args: - core_activity: The core activity payload - token: The authorization token (if any) - """ - result: InvokeResponse[Any] - try: - event = ActivityEvent(body=core_activity, token=token) - if inspect.iscoroutinefunction(self.on_activity_event): - result = await self.on_activity_event(event) - else: - result = self.on_activity_event(event) - except Exception as error: - # Log with full traceback - logger.exception(str(error)) - result = InvokeResponse(status=500) - - return result - - def _handle_activity_response(self, response: Response, result: Any) -> Union[Response, Dict[str, object]]: - """ - Handle the activity response formatting. - - Args: - response: The FastAPI response object - result: The result from activity processing - - Returns: - The formatted response - """ - status_code: Optional[int] = None - body: Optional[Dict[str, Any]] = None - resp_dict: Optional[Dict[str, Any]] = None - if isinstance(result, dict): - resp_dict = cast(Dict[str, Any], result) - elif isinstance(result, BaseModel): - resp_dict = result.model_dump(exclude_none=True) - - # if resp_dict has status set it - if resp_dict and "status" in resp_dict: - status_code = resp_dict.get("status") - - if resp_dict and "body" in resp_dict: - body = resp_dict.get("body", None) - - if status_code is not None: - response.status_code = status_code - - if body is not None: - logger.debug(f"Returning body {body}") - return body - logger.debug("Returning empty body") - return response - - async def on_activity_request(self, core_activity: CoreActivity, request: Request, response: Response) -> Any: - """Handle incoming Teams activity.""" - # Get validated token from middleware (if present - will be missing if skip_auth is True) - if hasattr(request.state, "validated_token") and request.state.validated_token: - token = request.state.validated_token - else: - token = cast( - TokenProtocol, - SimpleNamespace( - app_id="", - app_display_name="", - tenant_id="", - service_url=core_activity.service_url or "", - from_="azure", - from_id="", - is_expired=lambda: False, - ), - ) - - activity_type = core_activity.type or "unknown" - activity_id = core_activity.id or "unknown" - - logger.debug(f"Received activity: {activity_type} (ID: {activity_id})") - logger.debug(f"Processing activity {activity_id} via handler...") - - # Process the activity - result = await self._process_activity(core_activity, token) - return self._handle_activity_response(response, result) - - def _setup_routes(self) -> None: - """Setup FastAPI routes.""" - - self.app.post("/api/messages")(self.on_activity_request) - - async def health_check() -> Dict[str, Any]: - """Basic health check endpoint.""" - return {"status": "healthy", "port": self._port} - - self.app.get("/")(health_check) - - def mount(self, name: str, dir_path: Path | str, page_path: Optional[str] = None) -> None: - """ - Serve a static page at the given path. - - Args: - name: The name of the page (used in URL) - page_path: The path to the static HTML file - """ - self.app.mount(page_path or f"/{name}", StaticFiles(directory=dir_path, check_dir=True, html=True), name=name) diff --git a/packages/apps/tests/test_function_context.py b/packages/apps/tests/test_function_context.py index 8b67d3ce0..66d153ab3 100644 --- a/packages/apps/tests/test_function_context.py +++ b/packages/apps/tests/test_function_context.py @@ -32,7 +32,7 @@ def mock_api(self): @pytest.fixture def mock_http(self): - """Create a mock HttpPlugin.""" + """Create a mock activity sender.""" http = MagicMock() http.send = AsyncMock() http.send.return_value = "sent-activity" diff --git a/packages/apps/tests/test_jwt_middleware.py b/packages/apps/tests/test_jwt_middleware.py deleted file mode 100644 index 1f4b548f7..000000000 --- a/packages/apps/tests/test_jwt_middleware.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -# pyright: basic - -from unittest.mock import AsyncMock, MagicMock, patch - -import jwt -import pytest -from fastapi import HTTPException -from microsoft_teams.apps.auth.jwt_middleware import create_jwt_validation_middleware - - -class TestCreateJwtValidationMiddleware: - """Test suite for create_jwt_validation_middleware.""" - - VALIDATED_PATHS = ["/api/messages"] - APP_ID = "test-app-id" - - @pytest.fixture - def mock_call_next(self): - """Create a mock call_next coroutine.""" - mock_response = MagicMock() - call_next = AsyncMock(return_value=mock_response) - return call_next - - def _make_request(self, path: str, auth_header: str | None = None, body: dict | None = None) -> MagicMock: - """Build a mock FastAPI Request with the given path and auth header.""" - mock_request = MagicMock() - mock_request.url.path = path - mock_request.headers.get = MagicMock(return_value=auth_header) - if body is None: - body = {"serviceUrl": "https://test.service.url", "id": "activity-123"} - mock_request.json = AsyncMock(return_value=body) - mock_request.state = MagicMock() - return mock_request - - @pytest.mark.asyncio - async def test_path_not_in_validated_paths(self, mock_call_next): - """Requests whose path is not in the validated paths list bypass auth and call call_next directly.""" - with patch("microsoft_teams.apps.auth.jwt_middleware.TokenValidator") as mock_validator_class: - mock_validator = AsyncMock() - mock_validator_class.for_service.return_value = mock_validator - - middleware = create_jwt_validation_middleware(self.APP_ID, self.VALIDATED_PATHS) - - mock_request = self._make_request("/health") - response = await middleware(mock_request, mock_call_next) - - mock_call_next.assert_awaited_once_with(mock_request) - assert response is mock_call_next.return_value - - @pytest.mark.asyncio - async def test_missing_authorization_header(self, mock_call_next): - """A request to a validated path with no authorization header raises HTTP 401.""" - with patch("microsoft_teams.apps.auth.jwt_middleware.TokenValidator") as mock_validator_class: - mock_validator = AsyncMock() - mock_validator_class.for_service.return_value = mock_validator - - middleware = create_jwt_validation_middleware(self.APP_ID, self.VALIDATED_PATHS) - - mock_request = self._make_request("/api/messages", auth_header=None) - - with pytest.raises(HTTPException) as exc_info: - await middleware(mock_request, mock_call_next) - - assert exc_info.value.status_code == 401 - mock_call_next.assert_not_awaited() - - @pytest.mark.asyncio - async def test_invalid_authorization_format(self, mock_call_next): - """An authorization header that does not start with 'Bearer ' raises HTTP 401.""" - with patch("microsoft_teams.apps.auth.jwt_middleware.TokenValidator") as mock_validator_class: - mock_validator = AsyncMock() - mock_validator_class.for_service.return_value = mock_validator - - middleware = create_jwt_validation_middleware(self.APP_ID, self.VALIDATED_PATHS) - - mock_request = self._make_request("/api/messages", auth_header="Basic dXNlcjpwYXNz") - - with pytest.raises(HTTPException) as exc_info: - await middleware(mock_request, mock_call_next) - - assert exc_info.value.status_code == 401 - mock_call_next.assert_not_awaited() - - @pytest.mark.asyncio - async def test_valid_token_success(self, mock_call_next): - """A valid Bearer token is validated, stored in request.state, and call_next is called.""" - mock_jwt_token = MagicMock() - - with ( - patch("microsoft_teams.apps.auth.jwt_middleware.TokenValidator") as mock_validator_class, - patch("microsoft_teams.apps.auth.jwt_middleware.JsonWebToken", return_value=mock_jwt_token) as mock_jwt_cls, - ): - mock_validator = AsyncMock() - mock_validator_class.for_service.return_value = mock_validator - mock_validator.validate_token = AsyncMock(return_value=None) - - middleware = create_jwt_validation_middleware(self.APP_ID, self.VALIDATED_PATHS) - - mock_request = self._make_request("/api/messages", auth_header="Bearer my.jwt.token") - response = await middleware(mock_request, mock_call_next) - - # validate_token was called with the raw token and service URL from the body - mock_validator.validate_token.assert_awaited_once_with("my.jwt.token", "https://test.service.url") - - # JsonWebToken was constructed with the raw token - mock_jwt_cls.assert_called_once_with(value="my.jwt.token") - - # The validated token is stored in request state - assert mock_request.state.validated_token is mock_jwt_token - - # call_next was invoked and its response was returned - mock_call_next.assert_awaited_once_with(mock_request) - assert response is mock_call_next.return_value - - @pytest.mark.asyncio - async def test_jwt_invalid_token_error(self, mock_call_next): - """When the validator raises jwt.InvalidTokenError the middleware raises HTTP 401.""" - with patch("microsoft_teams.apps.auth.jwt_middleware.TokenValidator") as mock_validator_class: - mock_validator = AsyncMock() - mock_validator_class.for_service.return_value = mock_validator - mock_validator.validate_token = AsyncMock(side_effect=jwt.InvalidTokenError("bad token")) - - middleware = create_jwt_validation_middleware(self.APP_ID, self.VALIDATED_PATHS) - - mock_request = self._make_request("/api/messages", auth_header="Bearer bad.jwt.token") - - with pytest.raises(HTTPException) as exc_info: - await middleware(mock_request, mock_call_next) - - assert exc_info.value.status_code == 401 - mock_call_next.assert_not_awaited() - - @pytest.mark.asyncio - async def test_unexpected_exception(self, mock_call_next): - """When the validator raises a generic Exception the middleware raises HTTP 500.""" - with patch("microsoft_teams.apps.auth.jwt_middleware.TokenValidator") as mock_validator_class: - mock_validator = AsyncMock() - mock_validator_class.for_service.return_value = mock_validator - mock_validator.validate_token = AsyncMock(side_effect=RuntimeError("network error")) - - middleware = create_jwt_validation_middleware(self.APP_ID, self.VALIDATED_PATHS) - - mock_request = self._make_request("/api/messages", auth_header="Bearer some.jwt.token") - - with pytest.raises(HTTPException) as exc_info: - await middleware(mock_request, mock_call_next) - - assert exc_info.value.status_code == 500 - mock_call_next.assert_not_awaited() From 250d1441f0d14e39b7794edf29bce939b307be92 Mon Sep 17 00:00:00 2001 From: Lily Du Date: Wed, 8 Apr 2026 09:46:58 -0700 Subject: [PATCH 08/17] fixes: cookiecutter README, runtime dependencies, graph README (#369) - fixed cookiecutter statements referencing tests to examples - moved dev tools in packages into dev dependency group instead of runtime - updated broken code format for graph package README --------- Co-authored-by: lilydu --- .github/copilot-instructions.md | 6 +++--- CLAUDE.md | 2 +- README.md | 4 ++-- packages/a2aprotocol/pyproject.toml | 4 ++++ packages/cards/pyproject.toml | 3 ++- packages/common/pyproject.toml | 5 ++--- packages/graph/README.md | 16 ++-------------- 7 files changed, 16 insertions(+), 24 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5a6af1040..81f46a24b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -106,7 +106,7 @@ pyright # Type checking validation - **microsoft-teams-openai**: OpenAI integration - **microsoft-teams-mcpplugin**: MCP protocol integration -### Test Applications (`/tests`) +### Test Applications (`/examples`) Available test apps for development and validation: - **echo**: Basic message echo bot (recommended for quick validation) - **ai-test**: AI functionality testing @@ -118,14 +118,14 @@ Available test apps for development and validation: ### Creating New Components - **New package**: `cookiecutter templates/package -o packages` -- **New test app**: `cookiecutter templates/test -o tests` +- **New test app**: `cookiecutter templates/examples -o examples` ## Common Development Tasks ### Testing Changes 1. **Run commands with UV** (recommended): Use `uv run pytest packages/[package-name]` or **activate virtual environment**: `source .venv/bin/activate` 2. **Run affected tests**: `pytest packages/[package-name]` for specific package (or `uv run pytest packages/[package-name]`) -3. **Validate with test app**: Use `tests/echo` for basic functionality validation (starts a blocking server process) +3. **Validate with test app**: Use `examples/echo` for basic functionality validation (starts a blocking server process) 4. **Check DevTools web app**: Access http://localhost:3979/devtools when app is running ### Debugging and Development diff --git a/CLAUDE.md b/CLAUDE.md index d02b590d0..756ef062c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,7 +75,7 @@ All packages live in `packages/`, each with `src/microsoft_teams//` lay ```bash cookiecutter templates/package -o packages # New package -cookiecutter templates/test -o tests # New test package +cookiecutter templates/examples -o examples # New test app ``` ## Dependencies and Build diff --git a/README.md b/README.md index 80d99c4cf..ec4b69e7c 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,10 @@ Follow the prompts to name the package and the directory. It should create the p ### Create A New Test Package -Similarly, to create a new test package, run: +Similarly, to create a new test app, run: ```bash -cookiecutter templates/test -o tests +cookiecutter templates/examples -o examples ``` ## Test Apps diff --git a/packages/a2aprotocol/pyproject.toml b/packages/a2aprotocol/pyproject.toml index ad439e1cf..621708fdd 100644 --- a/packages/a2aprotocol/pyproject.toml +++ b/packages/a2aprotocol/pyproject.toml @@ -13,6 +13,10 @@ dependencies = [ "microsoft-teams-ai", "microsoft-teams-apps", "microsoft-teams-common", +] + +[dependency-groups] +dev = [ "pytest>=8.4.1", ] diff --git a/packages/cards/pyproject.toml b/packages/cards/pyproject.toml index 530e8d195..0942658e1 100644 --- a/packages/cards/pyproject.toml +++ b/packages/cards/pyproject.toml @@ -10,7 +10,8 @@ authors = [ { name = "Microsoft", email = "TeamsAISDKFeedback@microsoft.com" } ] requires-python = ">=3.12,<3.15" -dependencies = [ +[dependency-groups] +dev = [ "coverage>=7.8.0", "pytest>=8.3.5", "ruff>=0.11.5", diff --git a/packages/common/pyproject.toml b/packages/common/pyproject.toml index a9c2e23fa..9c7dc10ca 100644 --- a/packages/common/pyproject.toml +++ b/packages/common/pyproject.toml @@ -11,16 +11,15 @@ authors = [ ] requires-python = ">=3.12,<3.15" dependencies = [ - "coverage>=7.8.0", "httpx>=0.28.1", - "pytest>=8.3.5", - "ruff>=0.11.5", ] [dependency-groups] dev = [ + "coverage>=7.8.0", "pytest>=8.4.0", "pytest-asyncio>=1.0.0", + "ruff>=0.11.5", ] [project.urls] diff --git a/packages/graph/README.md b/packages/graph/README.md index 924ad13ea..93095b5ec 100644 --- a/packages/graph/README.md +++ b/packages/graph/README.md @@ -74,21 +74,9 @@ def create_token_callable(ctx: ActivityContext) -> Token: # Use with Graph client graph = get_graph_client(create_token_callable(ctx)) -``` - - await ctx.sign_in() - return - - # Use the user token that's already available in the context - graph = get_graph_client(ctx.user_token) - # Make Graph API calls - me = await graph.me.get() - await ctx.send(f"Hello {me.display_name}!") - - # Make Graph API calls - me = await graph.me.get() - await ctx.send(f"Hello {me.display_name}!") +# Or, use the user token that's already available in the context +graph = get_graph_client(ctx.user_token) ```` From 73c4e3f32e6c1eeb5da5a7476a0f2ae13a09dc7a Mon Sep 17 00:00:00 2001 From: jerbob92 Date: Wed, 8 Apr 2026 19:31:19 +0200 Subject: [PATCH 09/17] Implement stream cancelation (#337) Fixes #332 --------- Co-authored-by: Lily Du Co-authored-by: heyitsaamir Co-authored-by: Claude Opus 4.6 (1M context) --- examples/ai-test/src/main.py | 3 + .../src/microsoft_teams/apps/app_process.py | 6 +- .../src/microsoft_teams/apps/http_stream.py | 48 ++++++-- .../microsoft_teams/apps/plugins/__init__.py | 3 +- .../microsoft_teams/apps/plugins/streamer.py | 15 +++ packages/apps/tests/test_http_stream.py | 109 ++++++++++++++++++ 6 files changed, 171 insertions(+), 13 deletions(-) diff --git a/examples/ai-test/src/main.py b/examples/ai-test/src/main.py index 6e6e6039e..7b5d67d7a 100644 --- a/examples/ai-test/src/main.py +++ b/examples/ai-test/src/main.py @@ -4,6 +4,7 @@ """ import asyncio +import logging import re from os import getenv @@ -30,6 +31,8 @@ load_dotenv(find_dotenv(usecwd=True)) +logging.basicConfig(level=getenv("LOG_LEVEL", "WARNING").upper()) + def get_required_env(key: str) -> str: value = getenv(key) diff --git a/packages/apps/src/microsoft_teams/apps/app_process.py b/packages/apps/src/microsoft_teams/apps/app_process.py index 9b048113e..fdea6a844 100644 --- a/packages/apps/src/microsoft_teams/apps/app_process.py +++ b/packages/apps/src/microsoft_teams/apps/app_process.py @@ -27,7 +27,7 @@ from .activity_sender import ActivitySender from .events import ActivityEvent, ActivityResponseEvent, ActivitySentEvent, ErrorEvent -from .plugins import PluginActivityEvent, PluginBase +from .plugins import PluginActivityEvent, PluginBase, StreamCancelledError from .routing.activity_context import ActivityContext from .routing.router import ActivityHandler, ActivityRouter from .token_manager import TokenManager @@ -224,6 +224,10 @@ async def route(ctx: ActivityContext[ActivityBase]) -> Optional[Any]: ), plugins=plugins, ) + except StreamCancelledError: + logger.debug("Activity processing was cancelled (stream stopped)") + await activityCtx.stream.close() + response = InvokeResponse[Any](status=200) except Exception as error: await self.event_manager.on_error(ErrorEvent(error=error, activity=activity), plugins) raise error diff --git a/packages/apps/src/microsoft_teams/apps/http_stream.py b/packages/apps/src/microsoft_teams/apps/http_stream.py index 645025c87..65b2b93c1 100644 --- a/packages/apps/src/microsoft_teams/apps/http_stream.py +++ b/packages/apps/src/microsoft_teams/apps/http_stream.py @@ -8,6 +8,7 @@ from collections import deque from typing import Awaitable, Callable, List, Optional, Union +from httpx import HTTPStatusError from microsoft_teams.api import ( ApiClient, Attachment, @@ -20,7 +21,7 @@ ) from microsoft_teams.common import EventEmitter -from .plugins.streamer import StreamerEvent, StreamerProtocol +from .plugins.streamer import StreamCancelledError, StreamerEvent, StreamerProtocol from .utils import RetryOptions, retry logger = logging.getLogger(__name__) @@ -62,6 +63,7 @@ def __init__(self, client: ApiClient, ref: ConversationReference): self._total_wait_timeout: float = 30.0 self._state_changed = asyncio.Event() + self._canceled = False self._reset_state() def _reset_state(self) -> None: @@ -74,6 +76,14 @@ def _reset_state(self) -> None: self._entities: List[Entity] = [] self._queue: deque[Union[MessageActivityInput, TypingActivityInput, str]] = deque() + @property + def canceled(self) -> bool: + """ + Whether the stream has been canceled. + For example when the user pressed the Stop button or the 2-minute timeout has exceeded. + """ + return self._canceled + @property def closed(self) -> bool: """Whether the final stream message has been sent.""" @@ -103,6 +113,9 @@ def emit(self, activity: Union[MessageActivityInput, TypingActivityInput, str]) activity: The activity to emit. """ + if self._canceled: + raise StreamCancelledError("Stream has been cancelled.") + if isinstance(activity, str): activity = MessageActivityInput(text=activity, type="message") self._queue.append(activity) @@ -124,7 +137,7 @@ async def _wait_for_id_and_queue(self): """Wait until _id is set and the queue is empty, with a total timeout.""" async def _poll(): - while self._queue or not self._id: + while (self._queue or not self._id) and not self._canceled: await self._state_changed.wait() self._state_changed.clear() @@ -140,6 +153,10 @@ async def close(self) -> Optional[SentActivity]: logger.debug("stream already closed with result") return self._result + if self._canceled: + logger.debug("stream was cancelled, nothing to close") + return None + if self._index == 1 and not self._queue and not self._lock.locked(): logger.debug("stream has no content to send, returning None") return None @@ -229,13 +246,11 @@ async def _flush(self) -> None: if self._queue and not self._timeout: self._timeout = asyncio.get_running_loop().call_later(0.5, lambda: asyncio.create_task(self._flush())) - # Notify that queue state has changed - self._state_changed.set() - finally: # Reset flushing flag so future emits can trigger another flush self._pending = None self._lock.release() + self._state_changed.set() async def _send_activity(self, to_send: TypingActivityInput): """ @@ -265,12 +280,23 @@ async def _send(self, to_send: Union[TypingActivityInput, MessageActivityInput]) Args: activity: The activity to send. """ + if self._canceled: + logger.warning("Teams channel stopped the stream.") + raise StreamCancelledError("Teams channel stopped the stream.") + to_send.from_ = self._ref.bot to_send.conversation = self._ref.conversation - if to_send.id and not any(e.type == "streaminfo" for e in (to_send.entities or [])): - res = await self._client.conversations.activities(self._ref.conversation.id).update(to_send.id, to_send) - else: - res = await self._client.conversations.activities(self._ref.conversation.id).create(to_send) - - return SentActivity.merge(to_send, res) + try: + if to_send.id and not any(e.type == "streaminfo" for e in (to_send.entities or [])): + res = await self._client.conversations.activities(self._ref.conversation.id).update(to_send.id, to_send) + else: + res = await self._client.conversations.activities(self._ref.conversation.id).create(to_send) + + return SentActivity.merge(to_send, res) + except HTTPStatusError as e: + if e.response.status_code == 403: + self._canceled = True + logger.warning("Teams channel stopped the stream.") + raise StreamCancelledError("Teams channel stopped the stream.") from e + raise diff --git a/packages/apps/src/microsoft_teams/apps/plugins/__init__.py b/packages/apps/src/microsoft_teams/apps/plugins/__init__.py index e78a2de04..b489ab44b 100644 --- a/packages/apps/src/microsoft_teams/apps/plugins/__init__.py +++ b/packages/apps/src/microsoft_teams/apps/plugins/__init__.py @@ -10,10 +10,11 @@ from .plugin_base import PluginBase from .plugin_error_event import PluginErrorEvent from .plugin_start_event import PluginStartEvent -from .streamer import StreamerProtocol +from .streamer import StreamCancelledError, StreamerProtocol __all__ = [ "PluginBase", + "StreamCancelledError", "StreamerProtocol", "PluginActivityEvent", "PluginActivityResponseEvent", diff --git a/packages/apps/src/microsoft_teams/apps/plugins/streamer.py b/packages/apps/src/microsoft_teams/apps/plugins/streamer.py index c73b6fa98..6a3e7b7cd 100644 --- a/packages/apps/src/microsoft_teams/apps/plugins/streamer.py +++ b/packages/apps/src/microsoft_teams/apps/plugins/streamer.py @@ -3,6 +3,7 @@ Licensed under the MIT License. """ +import asyncio from typing import Awaitable, Callable, Literal, Optional, Protocol, Union from microsoft_teams.api import MessageActivityInput, SentActivity, TypingActivityInput @@ -10,9 +11,23 @@ StreamerEvent = Literal["chunk", "close"] +class StreamCancelledError(asyncio.CancelledError): + """Raised when a stream operation is attempted after the stream has been cancelled.""" + + pass + + class StreamerProtocol(Protocol): """Component that can send streamed chunks of an activity.""" + @property + def canceled(self) -> bool: + """ + Whether the stream has been canceled. + For example when the user pressed the Stop button or the 2-minute timeout has exceeded. + """ + ... + @property def closed(self) -> bool: """Whether the final stream message has been sent.""" diff --git a/packages/apps/tests/test_http_stream.py b/packages/apps/tests/test_http_stream.py index f6c9905a5..50cb4d2da 100644 --- a/packages/apps/tests/test_http_stream.py +++ b/packages/apps/tests/test_http_stream.py @@ -8,6 +8,7 @@ from unittest.mock import MagicMock, patch import pytest +from httpx import HTTPStatusError, Request, Response from microsoft_teams.api import ( Account, ApiClient, @@ -17,6 +18,7 @@ TypingActivityInput, ) from microsoft_teams.apps import HttpStream +from microsoft_teams.apps.plugins import StreamCancelledError class TestHttpStream: @@ -219,3 +221,110 @@ async def emit_task(): await self._run_scheduled_flushes(scheduled) assert max_concurrent_entries == 1 + + @pytest.mark.asyncio + async def test_stream_canceled_on_403(self, mock_api_client, conversation_reference, patch_loop_call_later): + loop = asyncio.get_running_loop() + patcher, scheduled = patch_loop_call_later(loop) + with patcher: + + async def mock_send_403(activity): + raise HTTPStatusError( + "Forbidden", + request=Request("POST", "https://example.com"), + response=Response(403), + ) + + mock_api_client.conversations.activities().create = mock_send_403 + stream = HttpStream(mock_api_client, conversation_reference) + + stream.emit("Test message") + await asyncio.sleep(0) + await self._run_scheduled_flushes(scheduled) + + assert stream.canceled is True + + @pytest.mark.asyncio + async def test_emit_blocked_after_cancel(self, mock_api_client, conversation_reference, patch_loop_call_later): + loop = asyncio.get_running_loop() + patcher, scheduled = patch_loop_call_later(loop) + with patcher: + + async def mock_send_403(activity): + raise HTTPStatusError( + "Forbidden", + request=Request("POST", "https://example.com"), + response=Response(403), + ) + + mock_api_client.conversations.activities().create = mock_send_403 + stream = HttpStream(mock_api_client, conversation_reference) + + stream.emit("First message") + await asyncio.sleep(0) + await self._run_scheduled_flushes(scheduled) + + assert stream.canceled is True + + # Emit after cancel should raise + with pytest.raises(StreamCancelledError, match="Stream has been cancelled."): + stream.emit("Should be ignored") + + @pytest.mark.asyncio + async def test_send_blocked_after_cancel(self, mock_api_client, conversation_reference): + stream = HttpStream(mock_api_client, conversation_reference) + stream._canceled = True + + with pytest.raises(StreamCancelledError, match="Teams channel stopped the stream."): + await stream._send(TypingActivityInput(text="test")) + + @pytest.mark.asyncio + async def test_stream_canceled_after_successful_message( + self, mock_api_client, conversation_reference, patch_loop_call_later + ): + call_count = 0 + loop = asyncio.get_running_loop() + patcher, scheduled = patch_loop_call_later(loop) + with patcher: + + async def mock_send_then_403(activity): + nonlocal call_count + call_count += 1 + if call_count == 1: + return SentActivity(id="activity-1", activity_params=activity) + raise HTTPStatusError( + "Forbidden", + request=Request("POST", "https://example.com"), + response=Response(403), + ) + + mock_api_client.conversations.activities().create = mock_send_then_403 + stream = HttpStream(mock_api_client, conversation_reference) + + # First emit succeeds + stream.emit("First message") + await asyncio.sleep(0) + await self._run_scheduled_flushes(scheduled) + + assert stream.canceled is False + assert call_count == 1 + + # Second emit triggers 403 + stream.emit("Second message") + await asyncio.sleep(0) + await self._run_scheduled_flushes(scheduled) + + assert stream.canceled is True + assert call_count == 2 + + # Further emits raise + with pytest.raises(StreamCancelledError, match="Stream has been cancelled."): + stream.emit("Should be ignored") + + @pytest.mark.asyncio + async def test_close_returns_none_when_canceled(self, mock_api_client, conversation_reference): + stream = HttpStream(mock_api_client, conversation_reference) + stream._canceled = True + + result = await stream.close() + assert result is None From 8212432f93bd10e8f5d5418358394ca2a19ac762 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:49:27 -0700 Subject: [PATCH 10/17] feat: add App.get_app_graph() and App._get_graph_token() (#355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `App` had no way to access a Graph client for app-only operations — only `ActivityContext` did. ## Changes - **`App._get_graph_token(tenant_id=None)`** — new private method wrapping `_token_manager.get_graph_token()`, mirroring the existing `_get_bot_token` pattern. - **`App.get_app_graph(tenant_id=None)`** — new public method returning a `GraphServiceClient` for app-only Graph operations. Not cached (clients own caching if needed). Accepts optional `tenant_id` for multi-tenant support. - **`utils/graph.py`** — new utility module with a public `create_graph_client` helper (replaces the private `_get_graph_client` inline function in `activity_context.py`). Both `ActivityContext` and `App` now import it from `utils`. - **`ActivityContext.app_graph`** — unchanged. - **`ActivityProcessor`** — unchanged. ```python # App-level Graph access, no ActivityContext needed graph = app.get_app_graph() # default tenant graph = app.get_app_graph(tenant_id="some-tenant") # explicit tenant me = await graph.me.get() ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: heyitsaamir <48929123+heyitsaamir@users.noreply.github.com> Co-authored-by: heyitsaamir Co-authored-by: Claude Opus 4.6 (1M context) --- examples/graph/src/main.py | 49 ++++++++++++ packages/apps/src/microsoft_teams/apps/app.py | 26 ++++++- .../apps/routing/activity_context.py | 21 ++---- .../microsoft_teams/apps/utils/__init__.py | 3 +- .../src/microsoft_teams/apps/utils/graph.py | 18 +++++ packages/apps/tests/test_activity_context.py | 16 ++-- .../tests/test_optional_graph_dependencies.py | 75 ++++++++++++++++++- 7 files changed, 180 insertions(+), 28 deletions(-) create mode 100644 packages/apps/src/microsoft_teams/apps/utils/graph.py diff --git a/examples/graph/src/main.py b/examples/graph/src/main.py index 3f77f3643..dad84d1c8 100644 --- a/examples/graph/src/main.py +++ b/examples/graph/src/main.py @@ -152,6 +152,51 @@ async def handle_emails_command(ctx: ActivityContext[MessageActivity]): await ctx.send(f"❌ Failed to get your emails: {str(e)}") +@app.on_message_pattern("app-users ctx") +async def handle_app_users_ctx_command(ctx: ActivityContext[MessageActivity]): + """List users using ctx.app_graph (app-only, no sign-in required).""" + try: + users = await ctx.app_graph.users.get() + + if users and users.value: + user_list = "👥 **Organization Users**\n\n*Fetched using `ctx.app_graph`*\n\n" + for i, user in enumerate(users.value[:5], 1): + user_list += f"**{i}.** {user.display_name or 'N/A'} ({user.user_principal_name or 'N/A'})\n\n" + await ctx.send(user_list) + else: + await ctx.send("No users found.") + + except Exception as e: + await ctx.send( + f"❌ Failed to list users: {e}\n\n" + "Ensure the app has **User.Read.All** application permission granted " + "in Azure Portal > App registrations > API permissions, and that an admin has consented." + ) + + +@app.on_message_pattern("app-users") +async def handle_app_users_command(ctx: ActivityContext[MessageActivity]): + """List users using app.get_app_graph() (app-only, no sign-in required).""" + try: + graph = app.get_app_graph() + users = await graph.users.get() + + if users and users.value: + user_list = "👥 **Organization Users**\n\n*Fetched using `app.get_app_graph()`*\n\n" + for i, user in enumerate(users.value[:5], 1): + user_list += f"**{i}.** {user.display_name or 'N/A'} ({user.user_principal_name or 'N/A'})\n\n" + await ctx.send(user_list) + else: + await ctx.send("No users found.") + + except Exception as e: + await ctx.send( + f"❌ Failed to list users: {e}\n\n" + "Ensure the app has **User.Read.All** application permission granted " + "in Azure Portal > App registrations > API permissions, and that an admin has consented." + ) + + @app.on_message_pattern("help") async def handle_help_command(ctx: ActivityContext[MessageActivity]): """Handle help command.""" @@ -164,6 +209,8 @@ async def handle_help_command(ctx: ActivityContext[MessageActivity]): "• **signout** - Sign out of your account\n\n" "• **profile** - View your Microsoft profile information\n\n" "• **emails** - List your 5 most recent emails\n\n" + "• **app-users** - List org users via app.get_app_graph() (no sign-in needed)\n\n" + "• **app-users ctx** - List org users via ctx.app_graph (no sign-in needed)\n\n" "• **help** - Show this help message\n\n" "**Getting Started:**\n\n" "1. Type `signin` to authenticate\n\n" @@ -189,6 +236,8 @@ async def handle_default_message(ctx: ActivityContext[MessageActivity]): "• **signout** - Sign out\n\n" "• **profile** - Show your profile information\n\n" "• **emails** - List your recent emails\n\n" + "• **app-users** - List org users via app.get_app_graph()\n\n" + "• **app-users ctx** - List org users via ctx.app_graph\n\n" "• **help** - Show detailed help with technical info" ) diff --git a/packages/apps/src/microsoft_teams/apps/app.py b/packages/apps/src/microsoft_teams/apps/app.py index f678910dc..ca3247b4d 100644 --- a/packages/apps/src/microsoft_teams/apps/app.py +++ b/packages/apps/src/microsoft_teams/apps/app.py @@ -7,7 +7,7 @@ import importlib.metadata import logging import os -from typing import Any, Awaitable, Callable, List, Optional, TypeVar, Union, Unpack, cast, overload +from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Optional, TypeVar, Union, Unpack, cast, overload from dependency_injector import providers from dotenv import find_dotenv, load_dotenv @@ -24,10 +24,14 @@ ManagedIdentityCredentials, MessageActivityInput, TokenCredentials, + TokenProtocol, ) from microsoft_teams.cards import AdaptiveCard from microsoft_teams.common import Client, ClientOptions, EventEmitter, LocalStorage +if TYPE_CHECKING: + from msgraph.graph_service_client import GraphServiceClient + from .activity_sender import ActivitySender from .app_events import EventManager from .app_oauth import OauthHandlers @@ -52,6 +56,7 @@ from .routing import ActivityHandlerMixin, ActivityRouter from .routing.activity_context import ActivityContext from .token_manager import TokenManager +from .utils import create_graph_client version = importlib.metadata.version("microsoft-teams-apps") @@ -519,3 +524,22 @@ async def _stop_plugins(self) -> None: async def _get_bot_token(self): return await self._token_manager.get_bot_token() + + async def _get_graph_token(self, tenant_id: Optional[str] = None) -> Optional[TokenProtocol]: + return await self._token_manager.get_graph_token(tenant_id) + + def get_app_graph(self, tenant_id: Optional[str] = None) -> "GraphServiceClient": + """ + Get a Microsoft Graph client configured with the app's token. + + This client can be used for app-only operations that don't require user context. + For multi-tenant apps, pass a tenant_id to get a tenant-specific token. + + Args: + tenant_id: Optional tenant ID. If not provided, uses the app's default tenant. + + Raises: + ImportError: If the graph dependencies are not installed. + + """ + return create_graph_client(lambda: self._get_graph_token(tenant_id)) diff --git a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py index 9f273a190..5dfb92964 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py +++ b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py @@ -37,6 +37,7 @@ from microsoft_teams.common.http.client_token import Token from ..activity_sender import ActivitySender +from ..utils import create_graph_client if TYPE_CHECKING: from msgraph.graph_service_client import GraphServiceClient @@ -45,18 +46,6 @@ logger = logging.getLogger(__name__) -def _get_graph_client(token: Token): - """Lazy import and call get_graph_client when needed.""" - try: - from microsoft_teams.graph import get_graph_client - - return get_graph_client(token) - except ImportError as exc: - raise ImportError( - "Graph functionality not available. Install with 'pip install microsoft-teams-apps[graph]'" - ) from exc - - @dataclass class SignInOptions: """Options for the signin method.""" @@ -134,7 +123,9 @@ def user_graph(self) -> "GraphServiceClient": if self._user_graph is None: try: user_token = JsonWebToken(self.user_token) - self._user_graph = _get_graph_client(user_token) + self._user_graph = create_graph_client(user_token) + except ImportError: + raise except Exception as e: self.logger.error(f"Failed to create user graph client: {e}") raise RuntimeError(f"Failed to create user graph client: {e}") from e @@ -156,7 +147,9 @@ def app_graph(self) -> "GraphServiceClient": """ if self._app_graph is None: try: - self._app_graph = _get_graph_client(self._app_token) + self._app_graph = create_graph_client(self._app_token) + except ImportError: + raise except Exception as e: self.logger.error(f"Failed to create app graph client: {e}") raise RuntimeError(f"Failed to create app graph client: {e}") from e diff --git a/packages/apps/src/microsoft_teams/apps/utils/__init__.py b/packages/apps/src/microsoft_teams/apps/utils/__init__.py index 0daf370b9..64c314f58 100644 --- a/packages/apps/src/microsoft_teams/apps/utils/__init__.py +++ b/packages/apps/src/microsoft_teams/apps/utils/__init__.py @@ -4,6 +4,7 @@ """ from .activity_utils import extract_tenant_id +from .graph import create_graph_client from .retry import RetryOptions, retry -__all__ = ["extract_tenant_id", "retry", "RetryOptions"] +__all__ = ["create_graph_client", "extract_tenant_id", "retry", "RetryOptions"] diff --git a/packages/apps/src/microsoft_teams/apps/utils/graph.py b/packages/apps/src/microsoft_teams/apps/utils/graph.py new file mode 100644 index 000000000..e96c47ab2 --- /dev/null +++ b/packages/apps/src/microsoft_teams/apps/utils/graph.py @@ -0,0 +1,18 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from microsoft_teams.common.http.client_token import Token + + +def create_graph_client(token: Token): + """Lazy import and create a Graph client with the given token.""" + try: + from microsoft_teams.graph import get_graph_client + + return get_graph_client(token) + except ImportError as exc: + raise ImportError( + "Graph functionality not available. Install with 'pip install microsoft-teams-apps[graph]'" + ) from exc diff --git a/packages/apps/tests/test_activity_context.py b/packages/apps/tests/test_activity_context.py index 96fdfbd9b..4e4fa429f 100644 --- a/packages/apps/tests/test_activity_context.py +++ b/packages/apps/tests/test_activity_context.py @@ -302,11 +302,11 @@ def test_user_graph_raises_when_no_user_token(self) -> None: _ = ctx.user_graph def test_user_graph_raises_runtime_error_when_graph_import_fails(self) -> None: - """user_graph raises RuntimeError when _get_graph_client raises ImportError.""" + """user_graph raises RuntimeError when create_graph_client raises ImportError.""" ctx, _ = _create_activity_context(is_signed_in=True, user_token="some.jwt.token") with patch( - "microsoft_teams.apps.routing.activity_context._get_graph_client", + "microsoft_teams.apps.routing.activity_context.create_graph_client", side_effect=ImportError("graph not installed"), ): with pytest.raises(RuntimeError, match="Failed to create user graph client"): @@ -316,15 +316,15 @@ def test_user_graph_raises_runtime_error_when_graph_import_fails(self) -> None: class TestActivityContextAppGraph: """Tests for ActivityContext.app_graph property.""" - def test_app_graph_raises_runtime_error_when_graph_import_fails(self) -> None: - """app_graph raises RuntimeError when _get_graph_client raises ImportError.""" + def test_app_graph_raises_import_error_when_graph_not_installed(self) -> None: + """app_graph raises ImportError when graph dependencies are not installed.""" ctx, _ = _create_activity_context() with patch( - "microsoft_teams.apps.routing.activity_context._get_graph_client", + "microsoft_teams.apps.routing.activity_context.create_graph_client", side_effect=ImportError("graph not installed"), ): - with pytest.raises(RuntimeError, match="Failed to create app graph client"): + with pytest.raises(ImportError, match="graph not installed"): _ = ctx.app_graph def test_app_graph_returns_cached_client_on_second_access(self) -> None: @@ -333,14 +333,14 @@ def test_app_graph_returns_cached_client_on_second_access(self) -> None: ctx, _ = _create_activity_context() with patch( - "microsoft_teams.apps.routing.activity_context._get_graph_client", + "microsoft_teams.apps.routing.activity_context.create_graph_client", return_value=mock_graph_client, ): first = ctx.app_graph second = ctx.app_graph assert first is second - # _get_graph_client should only have been called once (caching) + # create_graph_client should only have been called once (caching) assert ctx._app_graph is mock_graph_client diff --git a/packages/apps/tests/test_optional_graph_dependencies.py b/packages/apps/tests/test_optional_graph_dependencies.py index e646df622..6d085929c 100644 --- a/packages/apps/tests/test_optional_graph_dependencies.py +++ b/packages/apps/tests/test_optional_graph_dependencies.py @@ -5,7 +5,7 @@ from types import SimpleNamespace from typing import Any -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest from microsoft_teams.apps.routing.activity_context import ActivityContext @@ -49,8 +49,8 @@ def mock_import(name: str, *args: Any, **kwargs: Any) -> Any: with patch("builtins.__import__", side_effect=mock_import): activity_context = self._create_activity_context() - # app_graph should raise RuntimeError when graph dependencies are not available - with pytest.raises(RuntimeError, match="Failed to create app graph client"): + # app_graph should raise ImportError when graph dependencies are not available + with pytest.raises(ImportError, match="Graph functionality not available"): _ = activity_context.app_graph def test_app_graph_property_with_graph_available(self) -> None: @@ -123,6 +123,73 @@ def test_app_graph_property_no_token(self) -> None: app_token=None, # No app token ) - # app_graph should raise ValueError when no app token is available + # app_graph should raise RuntimeError when no app token is available with pytest.raises(RuntimeError, match="Token cannot be None"): _ = activity_context.app_graph + + +class TestAppGetAppGraph: + """Test App.get_app_graph method.""" + + def _create_app(self): + from microsoft_teams.apps import App, AppOptions + + return App(**AppOptions(client_id="test-id", client_secret="test-secret")) + + def test_get_app_graph_raises_import_error_when_graph_not_installed(self) -> None: + """get_app_graph raises ImportError when graph dependencies are not available.""" + app = self._create_app() + + with patch( + "microsoft_teams.apps.app.create_graph_client", + side_effect=ImportError("graph not installed"), + ): + with pytest.raises(ImportError): + _ = app.get_app_graph() + + def test_get_app_graph_returns_new_client_each_call(self) -> None: + """get_app_graph returns a new client on every call (no caching).""" + app = self._create_app() + + mock_client_1 = MagicMock() + mock_client_2 = MagicMock() + side_effects = [mock_client_1, mock_client_2] + + with patch( + "microsoft_teams.apps.app.create_graph_client", + side_effect=side_effects, + ): + first = app.get_app_graph() + second = app.get_app_graph() + + assert first is mock_client_1 + assert second is mock_client_2 + assert first is not second + + def test_get_app_graph_passes_tenant_id(self) -> None: + """get_app_graph passes the tenant_id through to the token factory callable.""" + app = self._create_app() + + mock_client = MagicMock() + captured_token_arg = [] + + def capture_token(token): + captured_token_arg.append(token) + return mock_client + + with patch( + "microsoft_teams.apps.app.create_graph_client", + side_effect=capture_token, + ): + app.get_app_graph(tenant_id="my-tenant-id") + + assert len(captured_token_arg) == 1 + # token arg should be a callable (lambda) + assert callable(captured_token_arg[0]) + + # Verify the lambda invokes _get_graph_token with the correct tenant_id + with patch.object(app, "_get_graph_token", new=AsyncMock(return_value=None)) as mock_get_token: + import asyncio + + asyncio.run(captured_token_arg[0]()) + mock_get_token.assert_called_once_with("my-tenant-id") From 49ada148c5fc0dada2e2fdd837cd1c5ecc4b6648 Mon Sep 17 00:00:00 2001 From: Lily Du Date: Wed, 8 Apr 2026 15:47:16 -0700 Subject: [PATCH 11/17] address dependabot alerts (#368) - dependabot isn't working right now due to nbgv, addressing manually until we merge in a workaround - aiohttp: 3.13.3 -> 3.13.5 - cryptography: 46.0.5 -> 46.0.6 - fastmcp: 3.1.1 -> 3.2.0 - pygments: 2.19.2 -> 2.20.0 - requests: 2.32.5 -> 2.33.1 Co-authored-by: lilydu --- packages/mcpplugin/pyproject.toml | 2 +- uv.lock | 252 +++++++++++++++--------------- 2 files changed, 127 insertions(+), 127 deletions(-) diff --git a/packages/mcpplugin/pyproject.toml b/packages/mcpplugin/pyproject.toml index 78cd56aac..87aecdbcd 100644 --- a/packages/mcpplugin/pyproject.toml +++ b/packages/mcpplugin/pyproject.toml @@ -11,7 +11,7 @@ license = "MIT" dependencies = [ "mcp>=1.13.1", "microsoft-teams-common", - "fastmcp>=2.14.2", + "fastmcp>=3.2.0", "microsoft-teams-apps", "microsoft-teams-ai" ] diff --git a/uv.lock b/uv.lock index 31f256ca8..0b20a0a89 100644 --- a/uv.lock +++ b/uv.lock @@ -140,7 +140,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.3" +version = "3.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -151,76 +151,76 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, - { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, - { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, - { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, - { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, - { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, - { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, - { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, - { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, - { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, - { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, - { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, - { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, - { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, - { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, - { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, - { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, - { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, - { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, - { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, - { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, - { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, - { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, - { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, - { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, - { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, - { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, - { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, - { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, - { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, - { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, - { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, - { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, - { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, - { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, - { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, - { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, - { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, ] [[package]] @@ -744,55 +744,55 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.5" +version = "46.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, - { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" }, + { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" }, + { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" }, + { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" }, + { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" }, + { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" }, + { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" }, + { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" }, + { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" }, + { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" }, + { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" }, + { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" }, + { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" }, + { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, ] [[package]] @@ -958,7 +958,7 @@ wheels = [ [[package]] name = "fastmcp" -version = "3.1.1" +version = "3.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, @@ -983,9 +983,9 @@ dependencies = [ { name = "watchfiles" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/83/c95d3bf717698a693eccb43e137a32939d2549876e884e246028bff6ecce/fastmcp-3.1.1.tar.gz", hash = "sha256:db184b5391a31199323766a3abf3a8bfbb8010479f77eca84c0e554f18655c48", size = 17347644, upload-time = "2026-03-14T19:12:20.235Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/32/4f1b2cfd7b50db89114949f90158b1dcc2c92a1917b9f57c0ff24e47a2f4/fastmcp-3.2.0.tar.gz", hash = "sha256:d4830b8ffc3592d3d9c76dc0f398904cf41f04910e41a0de38cc1004e0903bef", size = 26318581, upload-time = "2026-03-30T20:25:37.692Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/ea/570122de7e24f72138d006f799768e14cc1ccf7fcb22b7750b2bd276c711/fastmcp-3.1.1-py3-none-any.whl", hash = "sha256:8132ba069d89f14566b3266919d6d72e2ec23dd45d8944622dca407e9beda7eb", size = 633754, upload-time = "2026-03-14T19:12:22.736Z" }, + { url = "https://files.pythonhosted.org/packages/4f/67/684fa2d2de1e7504549d4ca457b4f854ccec3cd3be03bd86b33b599fbf58/fastmcp-3.2.0-py3-none-any.whl", hash = "sha256:e71aba3df16f86f546a4a9e513261d3233bcc92bef0dfa647bac3fa33623f681", size = 705550, upload-time = "2026-03-30T20:25:35.499Z" }, ] [[package]] @@ -2026,7 +2026,7 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "fastmcp", specifier = ">=2.14.2" }, + { name = "fastmcp", specifier = ">=3.2.0" }, { name = "mcp", specifier = ">=1.13.1" }, { name = "microsoft-teams-ai", editable = "packages/ai" }, { name = "microsoft-teams-apps", editable = "packages/apps" }, @@ -2707,11 +2707,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +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/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { 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]] @@ -2939,7 +2939,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -2947,9 +2947,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] From 80b98eb23ee877b41cd770cb7b23559229810115 Mon Sep 17 00:00:00 2001 From: Aamir Jawaid <48929123+heyitsaamir@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:51:30 -0700 Subject: [PATCH 12/17] feat: add hatch-nbgv plugin for graceful nbgv fallback (#365) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Adds `tools/hatch-nbgv/`, a thin hatchling version-source plugin (`nbgv-fallback`) that wraps `nbgv-python` with graceful fallback - When nbgv CLI is available (CI): delegates to `nbgv-python` for real version resolution + PEP 440 normalization - When nbgv CLI is missing (local dev): falls back to `0.0.0` with a warning instead of failing the build - Plugin named `nbgv-fallback` to avoid collision with `nbgv-python`'s own hatch entry point - `NBGV_REQUIRED=1` env var set in both GitHub Actions and AzDO pipelines to prevent publishing `0.0.0` - All 11 packages updated to use `source = "nbgv-fallback"` in `[tool.hatch.version]` ## Test plan - [x] `uv sync` works without nbgv installed (packages resolve to `0.0.0`) - [x] `uv sync` works with nbgv installed (packages resolve to real versions) - [x] `uv build --all-packages` produces correct versions with nbgv - [x] `uv build --all-packages` falls back to `0.0.0` without nbgv - [x] `NBGV_REQUIRED=1 uv build --all-packages` without nbgv → build fails hard - [x] `pytest` — 577 tests pass - [x] `pyright` — 0 errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude --- .azdo/publish.yml | 1 + .github/workflows/ci.yml | 3 ++ README.md | 1 + RELEASE.md | 5 ++ packages/a2aprotocol/pyproject.toml | 4 +- packages/ai/pyproject.toml | 4 +- packages/api/pyproject.toml | 4 +- packages/apps/pyproject.toml | 4 +- packages/botbuilder/pyproject.toml | 4 +- packages/cards/pyproject.toml | 4 +- packages/common/pyproject.toml | 4 +- packages/devtools/pyproject.toml | 4 +- packages/graph/pyproject.toml | 4 +- packages/mcpplugin/pyproject.toml | 4 +- packages/openai/pyproject.toml | 4 +- pyproject.toml | 3 +- tools/hatch-nbgv/pyproject.toml | 21 ++++++++ tools/hatch-nbgv/src/hatch_nbgv/__init__.py | 8 +++ tools/hatch-nbgv/src/hatch_nbgv/hooks.py | 13 +++++ .../src/hatch_nbgv/version_source.py | 41 +++++++++++++++ uv.lock | 51 ++++++++++++++++++- 21 files changed, 167 insertions(+), 24 deletions(-) create mode 100644 tools/hatch-nbgv/pyproject.toml create mode 100644 tools/hatch-nbgv/src/hatch_nbgv/__init__.py create mode 100644 tools/hatch-nbgv/src/hatch_nbgv/hooks.py create mode 100644 tools/hatch-nbgv/src/hatch_nbgv/version_source.py diff --git a/.azdo/publish.yml b/.azdo/publish.yml index a6105de68..51b0f7289 100644 --- a/.azdo/publish.yml +++ b/.azdo/publish.yml @@ -50,6 +50,7 @@ extends: ob_git_fetchTags: true ob_sdl_binskim_break: true ob_git_fetchDepth: -1 + NBGV_REQUIRED: '1' steps: - checkout: self diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8fc00cfb..70db7cce0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,9 @@ jobs: matrix: python-version: ["3.12", "3.13", "3.14"] + env: + NBGV_REQUIRED: "1" + steps: - name: Harden Runner uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 diff --git a/README.md b/README.md index ec4b69e7c..9bf9eb0f4 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ A comprehensive SDK for building Microsoft Teams applications, bots, and AI agen - UV version is >= 0.8.11. Install and upgrade from [docs.astral.sh/uv](https://docs.astral.sh/uv/getting-started/installation/). - Python version is >= 3.12. Install or upgrade from [python.org/downloads](https://www.python.org/downloads/). +- (Optional) .NET SDK + `nbgv` CLI for real version numbers. Without it, packages build as `0.0.0` which is fine for local development. See [RELEASE.md](RELEASE.md) for details. ### Installation diff --git a/RELEASE.md b/RELEASE.md index edecbdf06..26dc224f6 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -6,7 +6,12 @@ This project uses [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.Gi ## Prerequisites +The .NET SDK and `nbgv` CLI are **required for publishing** but **optional for local development**. Without them, packages fall back to version `0.0.0` so you can still build and test locally. + +CI pipelines set `NBGV_REQUIRED=1` to ensure builds fail if `nbgv` is unavailable. + ```bash +# Optional for local dev, required for releases dotnet tool install -g nbgv ``` diff --git a/packages/a2aprotocol/pyproject.toml b/packages/a2aprotocol/pyproject.toml index 621708fdd..f4fe233b7 100644 --- a/packages/a2aprotocol/pyproject.toml +++ b/packages/a2aprotocol/pyproject.toml @@ -25,11 +25,11 @@ external = true [build-system] -requires = ["hatchling", "nbgv-python"] +requires = ["hatchling", "hatch-nbgv"] build-backend = "hatchling.build" [tool.hatch.version] -source = "nbgv" +source = "nbgv-fallback" [tool.hatch.build.targets.wheel] packages = ["src/microsoft_teams", "src/microsoft"] diff --git a/packages/ai/pyproject.toml b/packages/ai/pyproject.toml index 5f7a1c5ab..efac8f9e5 100644 --- a/packages/ai/pyproject.toml +++ b/packages/ai/pyproject.toml @@ -13,11 +13,11 @@ dependencies = [ ] [build-system] -requires = ["hatchling", "nbgv-python"] +requires = ["hatchling", "hatch-nbgv"] build-backend = "hatchling.build" [tool.hatch.version] -source = "nbgv" +source = "nbgv-fallback" [tool.hatch.build.targets.wheel] packages = ["src/microsoft_teams", "src/microsoft"] diff --git a/packages/api/pyproject.toml b/packages/api/pyproject.toml index b666a1d27..778cc532c 100644 --- a/packages/api/pyproject.toml +++ b/packages/api/pyproject.toml @@ -25,11 +25,11 @@ dependencies = [ Homepage = "https://github.com/microsoft/teams.py/tree/main/packages/api/src/microsoft/teams/api" [build-system] -requires = ["hatchling", "nbgv-python"] +requires = ["hatchling", "hatch-nbgv"] build-backend = "hatchling.build" [tool.hatch.version] -source = "nbgv" +source = "nbgv-fallback" [dependency-groups] dev = [ diff --git a/packages/apps/pyproject.toml b/packages/apps/pyproject.toml index cc145ad5e..431adb040 100644 --- a/packages/apps/pyproject.toml +++ b/packages/apps/pyproject.toml @@ -35,11 +35,11 @@ test = [ ] [build-system] -requires = ["hatchling", "nbgv-python"] +requires = ["hatchling", "hatch-nbgv"] build-backend = "hatchling.build" [tool.hatch.version] -source = "nbgv" +source = "nbgv-fallback" [tool.hatch.build.targets.wheel] packages = ["src/microsoft_teams", "src/microsoft"] diff --git a/packages/botbuilder/pyproject.toml b/packages/botbuilder/pyproject.toml index 5ef4d15e7..f0f6c49f2 100644 --- a/packages/botbuilder/pyproject.toml +++ b/packages/botbuilder/pyproject.toml @@ -21,11 +21,11 @@ Homepage = "https://github.com/microsoft/teams.py/tree/main/packages/botbuilder/ [build-system] -requires = ["hatchling", "nbgv-python"] +requires = ["hatchling", "hatch-nbgv"] build-backend = "hatchling.build" [tool.hatch.version] -source = "nbgv" +source = "nbgv-fallback" [tool.hatch.build.targets.wheel] packages = ["src/microsoft_teams", "src/microsoft"] diff --git a/packages/cards/pyproject.toml b/packages/cards/pyproject.toml index 0942658e1..e38bfd4a0 100644 --- a/packages/cards/pyproject.toml +++ b/packages/cards/pyproject.toml @@ -21,11 +21,11 @@ dev = [ Homepage = "https://github.com/microsoft/teams.py/tree/main/packages/cards/src/microsoft/teams/cards" [build-system] -requires = ["hatchling", "nbgv-python"] +requires = ["hatchling", "hatch-nbgv"] build-backend = "hatchling.build" [tool.hatch.version] -source = "nbgv" +source = "nbgv-fallback" [tool.hatch.build.targets.wheel] packages = ["src/microsoft_teams", "src/microsoft"] diff --git a/packages/common/pyproject.toml b/packages/common/pyproject.toml index 9c7dc10ca..aedee67b9 100644 --- a/packages/common/pyproject.toml +++ b/packages/common/pyproject.toml @@ -27,11 +27,11 @@ Homepage = "https://github.com/microsoft/teams.py/tree/main/packages/common/src/ [build-system] -requires = ["hatchling", "nbgv-python"] +requires = ["hatchling", "hatch-nbgv"] build-backend = "hatchling.build" [tool.hatch.version] -source = "nbgv" +source = "nbgv-fallback" [tool.hatch.build.targets.wheel] packages = ["src/microsoft_teams", "src/microsoft"] diff --git a/packages/devtools/pyproject.toml b/packages/devtools/pyproject.toml index 46ceb3e16..d5fe8d399 100644 --- a/packages/devtools/pyproject.toml +++ b/packages/devtools/pyproject.toml @@ -25,11 +25,11 @@ Homepage = "https://github.com/microsoft/teams.py/tree/main/packages/devtools/sr [build-system] -requires = ["hatchling", "nbgv-python"] +requires = ["hatchling", "hatch-nbgv"] build-backend = "hatchling.build" [tool.hatch.version] -source = "nbgv" +source = "nbgv-fallback" [tool.hatch.build.targets.wheel] packages = ["src/microsoft_teams", "src/microsoft"] diff --git a/packages/graph/pyproject.toml b/packages/graph/pyproject.toml index 2df357345..14477b766 100644 --- a/packages/graph/pyproject.toml +++ b/packages/graph/pyproject.toml @@ -26,11 +26,11 @@ testpaths = ["tests"] python_paths = ["src"] [build-system] -requires = ["hatchling", "nbgv-python"] +requires = ["hatchling", "hatch-nbgv"] build-backend = "hatchling.build" [tool.hatch.version] -source = "nbgv" +source = "nbgv-fallback" [tool.hatch.build.targets.wheel] packages = ["src/microsoft_teams", "src/microsoft"] diff --git a/packages/mcpplugin/pyproject.toml b/packages/mcpplugin/pyproject.toml index 87aecdbcd..8bd48b032 100644 --- a/packages/mcpplugin/pyproject.toml +++ b/packages/mcpplugin/pyproject.toml @@ -20,11 +20,11 @@ dependencies = [ external = true [build-system] -requires = ["hatchling", "nbgv-python"] +requires = ["hatchling", "hatch-nbgv"] build-backend = "hatchling.build" [tool.hatch.version] -source = "nbgv" +source = "nbgv-fallback" [tool.hatch.build.targets.wheel] packages = ["src/microsoft_teams", "src/microsoft"] diff --git a/packages/openai/pyproject.toml b/packages/openai/pyproject.toml index 291fb19b4..a0dee43a3 100644 --- a/packages/openai/pyproject.toml +++ b/packages/openai/pyproject.toml @@ -16,11 +16,11 @@ dependencies = [ ] [build-system] -requires = ["hatchling", "nbgv-python"] +requires = ["hatchling", "hatch-nbgv"] build-backend = "hatchling.build" [tool.hatch.version] -source = "nbgv" +source = "nbgv-fallback" [tool.hatch.build.targets.wheel] packages = ["src/microsoft_teams", "src/microsoft"] diff --git a/pyproject.toml b/pyproject.toml index 033ca6774..3b4d4f0e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ "microsoft-teams-mcpplugin" = { workspace = true } "microsoft-teams-a2a" = { workspace = true } "microsoft-teams-botbuilder" = { workspace = true } +"hatch-nbgv" = { path = "tools/hatch-nbgv" } [tool.uv.workspace] members = ["packages/*", "examples/*"] @@ -30,7 +31,7 @@ dev = [ "pytest-cov>=6.0.0", ] release = [ - "nbgv-python>=0.1.8", + "hatch-nbgv", ] [tool.ruff] diff --git a/tools/hatch-nbgv/pyproject.toml b/tools/hatch-nbgv/pyproject.toml new file mode 100644 index 000000000..a396d1ef8 --- /dev/null +++ b/tools/hatch-nbgv/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "hatch-nbgv" +version = "0.1.0" +description = "Hatchling version source plugin wrapping nbgv-python with graceful fallback" +authors = [{ name = "Microsoft", email = "TeamsAISDKFeedback@microsoft.com" }] +requires-python = ">=3.12" +license = "MIT" +dependencies = ["hatchling>=1.25.0", "nbgv-python>=2.0.0"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project.entry-points.hatch] +hatch-nbgv = "hatch_nbgv.hooks" + +[tool.hatch.build.targets.wheel] +packages = ["src/hatch_nbgv"] + +[tool.hatch.build.targets.sdist] +include = ["src"] diff --git a/tools/hatch-nbgv/src/hatch_nbgv/__init__.py b/tools/hatch-nbgv/src/hatch_nbgv/__init__.py new file mode 100644 index 000000000..488333b3e --- /dev/null +++ b/tools/hatch-nbgv/src/hatch_nbgv/__init__.py @@ -0,0 +1,8 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from hatch_nbgv.version_source import NbgvVersionSource + +__all__ = ["NbgvVersionSource"] diff --git a/tools/hatch-nbgv/src/hatch_nbgv/hooks.py b/tools/hatch-nbgv/src/hatch_nbgv/hooks.py new file mode 100644 index 000000000..5da547375 --- /dev/null +++ b/tools/hatch-nbgv/src/hatch_nbgv/hooks.py @@ -0,0 +1,13 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from hatchling.plugin import hookimpl + +from hatch_nbgv.version_source import NbgvVersionSource + + +@hookimpl +def hatch_register_version_source(): + return NbgvVersionSource diff --git a/tools/hatch-nbgv/src/hatch_nbgv/version_source.py b/tools/hatch-nbgv/src/hatch_nbgv/version_source.py new file mode 100644 index 000000000..4c363e816 --- /dev/null +++ b/tools/hatch-nbgv/src/hatch_nbgv/version_source.py @@ -0,0 +1,41 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import logging +import os +import warnings +from typing import Any + +from hatchling.version.source.plugin.interface import VersionSourceInterface +from nbgv_python.errors import NbgvError # type: ignore[import-untyped] +from nbgv_python.hatch_plugin import NbgvVersionSource as _UpstreamSource # type: ignore[import-untyped] + +logger = logging.getLogger(__name__) + +_FALLBACK_VERSION = "0.0.0" + + +class NbgvVersionSource(VersionSourceInterface): + PLUGIN_NAME = "nbgv-fallback" + + def get_version_data(self) -> dict[str, Any]: + try: + upstream = _UpstreamSource(self.root, self.config) # type: ignore[arg-type] + return upstream.get_version_data() + except NbgvError: + if os.environ.get("NBGV_REQUIRED"): + raise + warnings.warn( + "nbgv CLI not found — using fallback version 0.0.0. " + "Install .NET SDK and nbgv for real versions. " + "Set NBGV_REQUIRED=1 to make this a hard error.", + UserWarning, + stacklevel=1, + ) + logger.warning("nbgv not found, falling back to version %s", _FALLBACK_VERSION) + return {"version": _FALLBACK_VERSION} + + def set_version(self, version: str, version_data: dict[str, Any]) -> None: + raise NotImplementedError("Version is managed by nbgv via version.json — not settable.") diff --git a/uv.lock b/uv.lock index 0b20a0a89..2e2c8768d 100644 --- a/uv.lock +++ b/uv.lock @@ -15,6 +15,7 @@ members = [ "dialogs", "echo", "graph", + "hatch-nbgv", "http-adapters", "mcp-client", "mcp-server", @@ -50,7 +51,7 @@ dev = [ { name = "pytest-cov", specifier = ">=6.0.0" }, { name = "ruff", specifier = ">=0.11.13" }, ] -release = [{ name = "nbgv-python", specifier = ">=0.1.8" }] +release = [{ name = "hatch-nbgv", editable = "packages/hatch-nbgv" }] [[package]] name = "a2a" @@ -1167,6 +1168,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, ] +[[package]] +name = "hatch-nbgv" +version = "0.1.0" +source = { editable = "packages/hatch-nbgv" } +dependencies = [ + { name = "hatchling" }, + { name = "nbgv-python" }, +] + +[package.metadata] +requires-dist = [ + { name = "hatchling", specifier = ">=1.25.0" }, + { name = "nbgv-python", specifier = ">=2.0.0" }, +] + +[[package]] +name = "hatchling" +version = "1.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pathspec" }, + { name = "pluggy" }, + { name = "trove-classifiers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/9c/b4cfe330cd4f49cff17fd771154730555fa4123beb7f292cf0098b4e6c20/hatchling-1.29.0.tar.gz", hash = "sha256:793c31816d952cee405b83488ce001c719f325d9cda69f1fc4cd750527640ea6", size = 55656, upload-time = "2026-02-23T19:42:06.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/8a/44032265776062a89171285ede55a0bdaadc8ac00f27f0512a71a9e3e1c8/hatchling-1.29.0-py3-none-any.whl", hash = "sha256:50af9343281f34785fab12da82e445ed987a6efb34fd8c2fc0f6e6630dbcc1b0", size = 76356, upload-time = "2026-02-23T19:42:05.197Z" }, +] + [[package]] name = "hpack" version = "4.1.0" @@ -2376,6 +2407,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, ] +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + [[package]] name = "platformdirs" version = "4.5.0" @@ -3249,6 +3289,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] +[[package]] +name = "trove-classifiers" +version = "2026.1.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/43/7935f8ea93fcb6680bc10a6fdbf534075c198eeead59150dd5ed68449642/trove_classifiers-2026.1.14.14.tar.gz", hash = "sha256:00492545a1402b09d4858605ba190ea33243d361e2b01c9c296ce06b5c3325f3", size = 16997, upload-time = "2026-01-14T14:54:50.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-py3-none-any.whl", hash = "sha256:1f9553927f18d0513d8e5ff80ab8980b8202ce37ecae0e3274ed2ef11880e74d", size = 14197, upload-time = "2026-01-14T14:54:49.067Z" }, +] + [[package]] name = "types-python-dateutil" version = "2.9.0.20251008" From c5bd359ced67be5ed92cdfb717e3e233c8717311 Mon Sep 17 00:00:00 2001 From: Aamir Jawaid <48929123+heyitsaamir@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:22:21 -0700 Subject: [PATCH 13/17] Fix uv lock (#372) Merge conflict - didn't sync uv lock correctly. --- uv.lock | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/uv.lock b/uv.lock index 2e2c8768d..5b8aebb75 100644 --- a/uv.lock +++ b/uv.lock @@ -15,7 +15,6 @@ members = [ "dialogs", "echo", "graph", - "hatch-nbgv", "http-adapters", "mcp-client", "mcp-server", @@ -51,7 +50,7 @@ dev = [ { name = "pytest-cov", specifier = ">=6.0.0" }, { name = "ruff", specifier = ">=0.11.13" }, ] -release = [{ name = "hatch-nbgv", editable = "packages/hatch-nbgv" }] +release = [{ name = "hatch-nbgv", directory = "tools/hatch-nbgv" }] [[package]] name = "a2a" @@ -1171,7 +1170,7 @@ wheels = [ [[package]] name = "hatch-nbgv" version = "0.1.0" -source = { editable = "packages/hatch-nbgv" } +source = { directory = "tools/hatch-nbgv" } dependencies = [ { name = "hatchling" }, { name = "nbgv-python" }, @@ -1823,6 +1822,10 @@ dependencies = [ { name = "microsoft-teams-ai" }, { name = "microsoft-teams-apps" }, { name = "microsoft-teams-common" }, +] + +[package.dev-dependencies] +dev = [ { name = "pytest" }, ] @@ -1832,9 +1835,11 @@ requires-dist = [ { name = "microsoft-teams-ai", editable = "packages/ai" }, { name = "microsoft-teams-apps", editable = "packages/apps" }, { name = "microsoft-teams-common", editable = "packages/common" }, - { name = "pytest", specifier = ">=8.4.1" }, ] +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.4.1" }] + [[package]] name = "microsoft-teams-ai" source = { editable = "packages/ai" } @@ -1951,14 +1956,18 @@ requires-dist = [ [[package]] name = "microsoft-teams-cards" source = { editable = "packages/cards" } -dependencies = [ + +[package.dev-dependencies] +dev = [ { name = "coverage" }, { name = "pytest" }, { name = "ruff" }, ] [package.metadata] -requires-dist = [ + +[package.metadata.requires-dev] +dev = [ { name = "coverage", specifier = ">=7.8.0" }, { name = "pytest", specifier = ">=8.3.5" }, { name = "ruff", specifier = ">=0.11.5" }, @@ -1968,30 +1977,26 @@ requires-dist = [ name = "microsoft-teams-common" source = { editable = "packages/common" } dependencies = [ - { name = "coverage" }, { name = "httpx" }, - { name = "pytest" }, - { name = "ruff" }, ] [package.dev-dependencies] dev = [ + { name = "coverage" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "ruff" }, ] [package.metadata] -requires-dist = [ - { name = "coverage", specifier = ">=7.8.0" }, - { name = "httpx", specifier = ">=0.28.1" }, - { name = "pytest", specifier = ">=8.3.5" }, - { name = "ruff", specifier = ">=0.11.5" }, -] +requires-dist = [{ name = "httpx", specifier = ">=0.28.1" }] [package.metadata.requires-dev] dev = [ + { name = "coverage", specifier = ">=7.8.0" }, { name = "pytest", specifier = ">=8.4.0" }, { name = "pytest-asyncio", specifier = ">=1.0.0" }, + { name = "ruff", specifier = ">=0.11.5" }, ] [[package]] From 662d715da754c419aab1d06b42bc896b4359e360 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:03:02 -0700 Subject: [PATCH 14/17] Bump cryptography from 46.0.6 to 46.0.7 (#371) Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.6 to 46.0.7.
Changelog

Sourced from cryptography's changelog.

46.0.7 - 2026-04-07


* **SECURITY ISSUE**: Fixed an issue where non-contiguous buffers could
be
  passed to APIs that accept Python buffers, which could lead to buffer
  overflow. **CVE-2026-39892**
* Updated Windows, macOS, and Linux wheels to be compiled with OpenSSL
3.5.6.

.. _v46-0-6:

Commits

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 90 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/uv.lock b/uv.lock index 5b8aebb75..3770ac76e 100644 --- a/uv.lock +++ b/uv.lock @@ -744,55 +744,55 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.6" +version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" }, - { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" }, - { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" }, - { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" }, - { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" }, - { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" }, - { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" }, - { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" }, - { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" }, - { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" }, - { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" }, - { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" }, - { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" }, - { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" }, - { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" }, - { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" }, - { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" }, - { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" }, - { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" }, - { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" }, - { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" }, - { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" }, - { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" }, - { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" }, - { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" }, - { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" }, - { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" }, - { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" }, - { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" }, - { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" }, - { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, ] [[package]] From 3ef08711949873be492fad2c41b4a39853d8868c Mon Sep 17 00:00:00 2001 From: Aamir Jawaid <48929123+heyitsaamir@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:28:53 -0700 Subject: [PATCH 15/17] fix: update graph example README to match actual sample code (#375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Added missing `app-users` and `app-users ctx` commands and app-level Graph access feature - Fixed default port from 3979 to 3978 (matching `main.py`) - Replaced non-existent `run_demo.ps1` and manual PYTHONPATH instructions with `uv run` - Updated example code to show actual patterns used (`ctx.user_token`, `app.get_app_graph()`, `ctx.app_graph`) - Removed outdated sections (manual `GetUserTokenParams` token fetching, verbose Token Lifecycle/Architecture) ## Test plan - [ ] Verify README commands list matches `main.py` handlers - [ ] Confirm `sample.env` port comment matches code default (`3978`) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude --- examples/graph/README.md | 117 +++++++--------------------------- examples/graph/src/sample.env | 9 ++- 2 files changed, 29 insertions(+), 97 deletions(-) diff --git a/examples/graph/README.md b/examples/graph/README.md index 46bd42612..61430b660 100644 --- a/examples/graph/README.md +++ b/examples/graph/README.md @@ -1,37 +1,42 @@ > [!CAUTION] -> This project is in public preview. We’ll do our best to maintain compatibility, but there may be breaking changes in upcoming releases. +> This project is in public preview. We'll do our best to maintain compatibility, but there may be breaking changes in upcoming releases. # Teams Graph Integration Demo -This demo application showcases how to use Microsoft Graph APIs within a Teams bot. +This demo application showcases how to use Microsoft Graph APIs within a Teams bot, including both delegated (user) and app-only access patterns. ## Features - **User Authentication**: Teams OAuth integration with automatic token management -- **Token Implementation**: Uses callable-based tokens for exact expiration handling -- **Profile Information**: Retrieve and display user profile data +- **Profile Information**: Retrieve and display user profile data via delegated access - **Email Access**: List recent emails with Mail.Read scope -- **Automatic Token Refresh**: Intelligent token lifecycle management +- **App-Level Graph Access**: Query organization data using app-only permissions (no user sign-in needed) via `app.get_app_graph()` or `ctx.app_graph` ## Commands - `signin` - Authenticate with Microsoft Graph - `profile` - Display user profile information (requires User.Read) - `emails` - Show recent emails (requires Mail.Read permission) +- `app-users` - List organization users via `app.get_app_graph()` (app-only, no sign-in needed) +- `app-users ctx` - List organization users via `ctx.app_graph` (app-only, no sign-in needed) - `signout` - Sign out of Microsoft Graph - `help` - Show available commands and implementation details ## Setup 1. Configure OAuth connection in Azure Bot registration -2. Set connection name to "graph" (or update `default_connection_name` in app options) +2. Set connection name to "graph" (or update `CONNECTION_NAME` env var) 3. Configure appropriate Microsoft Graph permissions: - `User.Read` (for profile access) - `Mail.Read` (for email access) -4. Create a `.env` file with required environment variables (copy from `sample.env`): + - `User.Read.All` application permission (for app-users commands) +4. Create a `.env` file in `examples/graph/src/` with required environment variables (copy from `sample.env`): ``` + CLIENT_ID= + CLIENT_SECRET= + TENANT_ID= CONNECTION_NAME=graph - # PORT=3979 # Optional: specify custom port (defaults to 3979) + # PORT=3978 # Optional: specify custom port (defaults to 3978) ``` ## Configuring a Regional Bot @@ -53,105 +58,27 @@ app = App( ## Running -### Option 1: Using the PowerShell Script (Recommended) - -From the `examples/graph/` directory: - -```powershell -.\run_demo.ps1 -``` - -### Option 2: Manual PYTHONPATH Setup - -From the `examples/graph/` directory: - -```powershell -# PowerShell -$env:PYTHONPATH="..\..\packages\graph\src;..\..\packages\api\src;..\..\packages\apps\src;..\..\packages\common\src" -python src\main.py -``` - -```bash -# Bash (Linux/macOS) -PYTHONPATH="../../packages/graph/src:../../packages/api/src:../../packages/apps/src:../../packages/common/src" python src/main.py -``` - -### Option 3: Install Packages in Development Mode - -From the repository root: +From the `examples/graph/` directory (so `.env` is discovered automatically): ```bash -# Install the graph package in development mode -pip install -e packages/graph -pip install -e packages/api -pip install -e packages/app -pip install -e packages/common - -# Then run the demo -python examples/graph/src/main.py +cd examples/graph +uv run src/main.py ``` -## Architecture - -The demo uses the `microsoft_teams.graph` package which provides: - -- **Token Integration**: Uses callable tokens for exact expiration handling -- **Automatic Token Resolution**: Seamless integration with Teams OAuth tokens -- **Graph Client Factory**: `get_graph_client()` function for creating authenticated clients - ## Example Usage ```python from microsoft_teams.graph import get_graph_client -# Get user's Graph client using their token +# Delegated access — create Graph client using the user's token graph = get_graph_client(ctx.user_token) - -# Access user profile me = await graph.me.get() - -# Access Teams membership -teams = await graph.me.joined_teams.get() - -# Access emails messages = await graph.me.messages.get() -``` - -## Token Lifecycle -1. User initiates `signin` command -2. Teams OAuth flow completes and stores user token -3. Graph client created with callable token that: - - Fetches fresh token on each call - - Includes exact expiration metadata - - Handles token refresh automatically -4. Graph API calls use current valid token -5. User can `signout` to clear tokens +# App-only access — no user sign-in needed +graph = app.get_app_graph() +users = await graph.users.get() -This approach provides better reliability and eliminates common token expiration issues. - -- **`get_graph_client()`** - Main factory function accepting Token values (strings, callables, etc.) -- **`DirectTokenCredential`** - Azure TokenCredential implementation using the unified Token type - -### Key Implementation Details - -```python -from microsoft_teams.api.clients.user.params import GetUserTokenParams -from microsoft_teams.graph import get_graph_client - -# Get token directly from Teams API -token_params = GetUserTokenParams( - channel_id=ctx.activity.channel_id, - user_id=ctx.activity.from_.id, - connection_name=ctx.connection_name, -) - -# Get user token and create Graph client directly -token_response = await ctx.api.users.token.get(token_params) - -# Create Graph client with string token (simplest approach) -graph = get_graph_client(token_response.token, connection_name=ctx.connection_name) - -# Make Graph API calls -me = await graph.me.get() +# Or via context +users = await ctx.app_graph.users.get() ``` diff --git a/examples/graph/src/sample.env b/examples/graph/src/sample.env index 75f63a0c6..ea9d8e33f 100644 --- a/examples/graph/src/sample.env +++ b/examples/graph/src/sample.env @@ -1,8 +1,13 @@ # Example environment configuration for Teams Graph Demo # Copy this file to .env and update with your values +# Azure Bot / App registration credentials (required) +CLIENT_ID= +CLIENT_SECRET= +TENANT_ID= + # OAuth connection name (should match your Azure Bot registration) CONNECTION_NAME=graph -# Optional: Custom port (defaults to 3979 if not specified) -# PORT=3979 +# Optional: Custom port (defaults to 3978 if not specified) +# PORT=3978 From a723ab91362f6de06682c2b9e1d5f7f2775f0468 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:18:17 -0700 Subject: [PATCH 16/17] Remove workflow_dispatch trigger from issue-analysis workflow (#374) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `workflow_dispatch` triggers can be abused to manually invoke workflows with attacker-controlled inputs, expanding the attack surface unnecessarily. ## Changes - **`.github/workflows/issue-analysis.yml`** - Removed `workflow_dispatch` trigger and its `issue_number` input - Dropped the job-level `if` guard that branched on `workflow_dispatch` vs `issues` event - Collapsed the "Resolve issue details" step — removed the `workflow_dispatch` branch that called the GitHub API with a user-supplied issue number, keeping only the direct event-payload path - Removed `EVENT_NAME` and `INPUT_NUMBER` env vars that existed solely to support the dispatch path --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: heyitsaamir <48929123+heyitsaamir@users.noreply.github.com> --- .github/workflows/issue-analysis.yml | 32 ++++++---------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/.github/workflows/issue-analysis.yml b/.github/workflows/issue-analysis.yml index ca15a46fe..e48d628fd 100644 --- a/.github/workflows/issue-analysis.yml +++ b/.github/workflows/issue-analysis.yml @@ -5,12 +5,6 @@ name: "Issue Analysis → Teams" on: issues: types: [opened] - workflow_dispatch: - inputs: - issue_number: - description: "Issue number to analyze" - required: true - type: number # Declare default permissions as read only. permissions: read-all @@ -19,7 +13,7 @@ jobs: analyze-and-notify: name: Analyze Issue & Notify Teams runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' || github.event.issue.performed_via_github_app == null + if: github.event.issue.performed_via_github_app == null permissions: issues: read @@ -53,8 +47,6 @@ jobs: id: issue env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - EVENT_NAME: ${{ github.event_name }} - INPUT_NUMBER: ${{ inputs.issue_number }} # Pass event data through env vars to avoid shell injection EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} EVENT_ISSUE_TITLE: ${{ github.event.issue.title }} @@ -76,22 +68,12 @@ jobs: } >> "$GITHUB_OUTPUT" } - if [ "$EVENT_NAME" = "workflow_dispatch" ]; then - ISSUE=$(gh api repos/microsoft/teams.py/issues/"$INPUT_NUMBER") - write_output "number" "$(echo "$ISSUE" | jq -r '.number')" - write_output "title" "$(echo "$ISSUE" | jq -r '.title')" - write_output "author" "$(echo "$ISSUE" | jq -r '.user.login')" - write_output "html_url" "$(echo "$ISSUE" | jq -r '.html_url')" - write_output "labels" "$(echo "$ISSUE" | jq -r '[.labels[].name] | join(",")')" - write_output "body" "$(echo "$ISSUE" | jq -r '.body // ""')" - else - write_output "number" "$EVENT_ISSUE_NUMBER" - write_output "title" "$EVENT_ISSUE_TITLE" - write_output "author" "$EVENT_ISSUE_AUTHOR" - write_output "html_url" "$EVENT_ISSUE_URL" - write_output "labels" "$(echo "$EVENT_ISSUE_LABELS" | jq -r '[.[].name] | join(",")')" - write_output "body" "$EVENT_ISSUE_BODY" - fi + write_output "number" "$EVENT_ISSUE_NUMBER" + write_output "title" "$EVENT_ISSUE_TITLE" + write_output "author" "$EVENT_ISSUE_AUTHOR" + write_output "html_url" "$EVENT_ISSUE_URL" + write_output "labels" "$(echo "$EVENT_ISSUE_LABELS" | jq -r '[.[].name] | join(",")')" + write_output "body" "$EVENT_ISSUE_BODY" - name: Analyze issue with Copilot CLI env: From 4e800d2146cd40ae21ff35af0e02d8ca8a39060b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:08:18 -0700 Subject: [PATCH 17/17] Bump axios from 1.13.5 to 1.15.0 in /examples/tab/Web (#376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [axios](https://github.com/axios/axios) from 1.13.5 to 1.15.0.
Release notes

Sourced from axios's releases.

v1.15.0

This release delivers two critical security patches, adds runtime support for Deno and Bun, and includes significant CI hardening, documentation improvements, and routine dependency updates.

⚠️ Important Changes

  • Deprecation: url.parse() usage has been replaced to address Node.js deprecation warnings. If you are on a recent version of Node.js, this resolves console warnings you may have been seeing. (#10625)

🔒 Security Fixes

  • Proxy Handling: Fixed a no_proxy hostname normalisation bypass that could lead to Server-Side Request Forgery (SSRF). (#10661)
  • Header Injection: Fixed an unrestricted cloud metadata exfiltration vulnerability via a header injection chain. (#10660)

🚀 New Features

  • Runtime Support: Added compatibility checks and documentation for Deno and Bun environments. (#10652, #10653)

🔧 Maintenance & Chores

  • CI Security: Hardened workflow permissions to least privilege, added the zizmor security scanner, pinned action versions, and gated npm publishing with OIDC and environment protection. (#10618, #10619, #10627, #10637, #10666)
  • Dependencies: Bumped serialize-javascript, handlebars, picomatch, vite, and denoland/setup-deno to latest versions. Added a 7-day Dependabot cooldown period. (#10574, #10572, #10568, #10663, #10664, #10665, #10669, #10670, #10616)
  • Documentation: Unified docs, improved beforeRedirect credential leakage example, clarified withCredentials/withXSRFToken behaviour, HTTP/2 support notes, async/await timeout error handling, header case preservation, and various typo fixes. (#10649, #10624, #7452, #7471, #10654, #10644, #10589)
  • Housekeeping: Removed stale files, regenerated lockfile, and updated sponsor scripts and blocks. (#10584, #10650, #10582, #10640, #10659, #10668)
  • Tests: Added regression coverage for urlencoded Content-Type casing. (#10573)

🌟 New Contributors

We are thrilled to welcome our new contributors. Thank you for helping improve Axios:

v1.14.0

This release focuses on compatibility fixes, adapter stability improvements, and test/tooling modernisation.

⚠️ Important Changes

  • Breaking Changes: None identified in this release.
  • Action Required: If you rely on env-based proxy behaviour or CJS resolution edge-cases, validate your integration after upgrade (notably proxy-from-env v2 alignment and main entry compatibility fix).

🚀 New Features

  • Runtime Features: No new end-user features were introduced in this release.
  • Test Coverage Expansion: Added broader smoke/module test coverage for CJS and ESM package usage. (#7510)

🐛 Bug Fixes

  • Headers: Trim trailing CRLF in normalised header values. (#7456)
  • HTTP/2: Close detached HTTP/2 sessions on timeout to avoid lingering sessions. (#7457)
  • Fetch Adapter: Cancel ReadableStream created during request-stream capability probing to prevent async resource leaks. (#7515)
  • Proxy Handling: Fixed env proxy behavior with proxy-from-env v2 usage. (#7499)

... (truncated)

Changelog

Sourced from axios's changelog.

Changelog

1.13.3 (2026-01-20)

Bug Fixes

  • http2: Use port 443 for HTTPS connections by default. (#7256) (d7e6065)
  • interceptor: handle the error in the same interceptor (#6269) (5945e40)
  • main field in package.json should correspond to cjs artifacts (#5756) (7373fbf)
  • package.json: add 'bun' package.json 'exports' condition. Load the Node.js build in Bun instead of the browser build (#5754) (b89217e)
  • silentJSONParsing=false should throw on invalid JSON (#7253) (#7257) (7d19335)
  • turn AxiosError into a native error (#5394) (#5558) (1c6a86d)
  • types: add handlers to AxiosInterceptorManager interface (#5551) (8d1271b)
  • types: restore AxiosError.cause type from unknown to Error (#7327) (d8233d9)
  • unclear error message is thrown when specifying an empty proxy authorization (#6314) (6ef867e)

Features

Reverts

  • Revert "fix: silentJSONParsing=false should throw on invalid JSON (#7253) (#7…" (#7298) (a4230f5), closes #7253 #7 #7298
  • deps: bump peter-evans/create-pull-request from 7 to 8 in the github-actions group (#7334) (2d6ad5e)

Contributors to this release

... (truncated)

Commits
  • 772a4e5 chore(release): prepare release 1.15.0 (#10671)
  • 4b07137 chore(deps-dev): bump vite from 8.0.0 to 8.0.5 in /tests/smoke/esm (#10663)
  • 51e57b3 chore(deps-dev): bump vite from 8.0.2 to 8.0.5 (#10664)
  • fba1a77 chore(deps-dev): bump vite from 8.0.2 to 8.0.5 in /tests/module/esm (#10665)
  • 0bf6e28 chore(deps): bump denoland/setup-deno in the github-actions group (#10669)
  • 8107157 chore(deps-dev): bump the development_dependencies group with 4 updates (#10670)
  • e66530e ci: require npm-publish environment for releases (#10666)
  • 49f23cb chore(sponsor): update sponsor block (#10668)
  • 3631854 fix: unrestricted cloud metadata exfiltration via header injection chain (#10...
  • fb3befb fix: no_proxy hostname normalization bypass leads to ssrf (#10661)
  • Additional commits viewable in compare view
Install script changes

This version modifies prepare script that runs during installation. Review the package contents before updating.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=axios&package-manager=npm_and_yarn&previous-version=1.13.5&new-version=1.15.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/teams.py/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/tab/Web/package-lock.json | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/examples/tab/Web/package-lock.json b/examples/tab/Web/package-lock.json index cab0c455a..a64b949ea 100644 --- a/examples/tab/Web/package-lock.json +++ b/examples/tab/Web/package-lock.json @@ -1320,14 +1320,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" } }, "node_modules/balanced-match": { @@ -2126,9 +2126,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/qs": { "version": "6.15.0",