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`.
**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
[](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.
[](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",