diff --git a/examples/reactions/src/main.py b/examples/reactions/src/main.py index 957473cc..61f967da 100644 --- a/examples/reactions/src/main.py +++ b/examples/reactions/src/main.py @@ -46,19 +46,24 @@ async def handle_message(ctx: ActivityContext[MessageActivity]): return # ============================================ - # Remove Reaction + # Remove Reaction (adds first so there's something to remove) # ============================================ if text.startswith("unreact "): reaction_type = text[8:].strip() + await ctx.api.reactions.add( + conversation_id=conversation_id, + activity_id=activity_id, + reaction_type=reaction_type, + ) + await ctx.reply(f"✅ Added {reaction_type} reaction, removing in 2s...") + await asyncio.sleep(2) await ctx.api.reactions.delete( conversation_id=conversation_id, activity_id=activity_id, reaction_type=reaction_type, ) - - await ctx.reply(f"✅ Removed {reaction_type} reaction from your message!") - print(f"[REACTION] Removed '{reaction_type}' from activity {activity_id}") + print(f"[REACTION] Cycled '{reaction_type}' on activity {activity_id}") return # ============================================ @@ -69,9 +74,9 @@ async def handle_message(ctx: ActivityContext[MessageActivity]): "**Message Reactions Bot**\n\n" "**Commands:**\n" "- `react ` - Add a reaction to your message\n" - "- `unreact ` - Remove a reaction from your message\n\n" + "- `unreact ` - Add then remove a reaction (2s cycle) to demo deletion\n\n" "- `react like` - Adds a 👍 to your message\n" - "- `unreact like` - Removes the 👍 from your message" + "- `unreact like` - Adds a 👍 then removes it after 2 seconds" ) return diff --git a/examples/tab/Web/package-lock.json b/examples/tab/Web/package-lock.json index f95c84cc..a53be8f3 100644 --- a/examples/tab/Web/package-lock.json +++ b/examples/tab/Web/package-lock.json @@ -7,7 +7,7 @@ "name": "tab", "license": "MIT", "dependencies": { - "@microsoft/teams.client": "^2.0.0", + "@microsoft/teams.client": "^2.0.9", "@microsoft/teams.common": "^2.0.0", "@microsoft/teams.graph": "^2.0.0", "@microsoft/teams.graph-endpoints": "^2.0.0", @@ -792,67 +792,67 @@ } }, "node_modules/@microsoft/teams.api": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@microsoft/teams.api/-/teams.api-2.0.1.tgz", - "integrity": "sha512-rdNrIsqJ1tOwvr/97iGsvN7hFyhG1xkxmDBGG2eRrnEjnN9uunVUgxjcMM45pTlbO71CQL10AYU+So2Caj6vFQ==", - "peer": true, + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@microsoft/teams.api/-/teams.api-2.0.9.tgz", + "integrity": "sha512-U8Bv7Ok/zZa4FdwS6xbB2Wts2gYyC3+f2gFPm9chMkYa7O2doFw+7AZXSiUEBY2p5IlvD4RwKEoaXuBeuDqwfQ==", + "license": "MIT", "dependencies": { + "@microsoft/teams.cards": "2.0.9", + "@microsoft/teams.common": "2.0.9", "jwt-decode": "^4.0.0", - "qs": "^6.13.0" + "qs": "^6.14.2" }, "engines": { "node": ">=20" - }, - "peerDependencies": { - "@microsoft/teams.cards": "2.0.1", - "@microsoft/teams.common": "2.0.1" } }, "node_modules/@microsoft/teams.cards": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@microsoft/teams.cards/-/teams.cards-2.0.1.tgz", - "integrity": "sha512-8l2SdfCMIHcgIGOfhvkvocoXRl1csMX/Lg7fgWCJIY3SKgMFs7ifs1tzKYL/lSWXIUPcrWOogOKSezm7CtkhlQ==", - "peer": true, + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@microsoft/teams.cards/-/teams.cards-2.0.9.tgz", + "integrity": "sha512-YYec0ATVI3jG98UMReTUsT+y8BfoB7JF3kTDzX8Co/iJOV9GGQ86jT+hgYUBK3Vc+avvoimWT8poJu7Clwz2Sg==", + "license": "MIT", "engines": { "node": ">=20" } }, "node_modules/@microsoft/teams.client": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@microsoft/teams.client/-/teams.client-2.0.1.tgz", - "integrity": "sha512-qwoRLJw06naWQrupqv7B/ZPxdNGFT5t0Ut85FGx13bSRYH/tGmLSDx1M7vux1GdIJ1O9KHZV7rfiB9UdsiOZBg==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@microsoft/teams.client/-/teams.client-2.0.9.tgz", + "integrity": "sha512-S3vfLtNytjpBkOAZOSxJg5NQ3AjWYNjoiFhREJyspH+LrmnaCP0X6iJj3MLcmz9g3hlrDBHGa1CrxbIoV56lQQ==", + "license": "MIT", "dependencies": { "@azure/msal-browser": "^4.9.1", - "uuid": "^11.0.5" + "@microsoft/teams.api": "2.0.9", + "@microsoft/teams.common": "2.0.9", + "@microsoft/teams.graph": "2.0.9" }, "engines": { "node": ">=20" }, "peerDependencies": { - "@microsoft/teams-js": "^2.35.0", - "@microsoft/teams.api": "2.0.1", - "@microsoft/teams.common": "2.0.1", - "@microsoft/teams.graph": "2.0.1" + "@microsoft/teams-js": "^2.35.0" } }, "node_modules/@microsoft/teams.common": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@microsoft/teams.common/-/teams.common-2.0.1.tgz", - "integrity": "sha512-w3a4Bk1Shww84nOZMZmXnw9DNXS1QyHU6fIVUfP+X3q/zYwa7dsqy2l7LFHT1qVkGDSktM1UmCOl9plBAHvebQ==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@microsoft/teams.common/-/teams.common-2.0.9.tgz", + "integrity": "sha512-vgMgZv9uc1v4f3gUlY/+6tjm+0vWMO8Nxrw9pCCvR7Y4+au075vBTDq0mPiq4uT/S1786hEegdny2c+3U1ECCA==", + "license": "MIT", "dependencies": { - "axios": "^1.8.2" + "axios": "^1.15.2" }, "engines": { "node": ">=20" } }, "node_modules/@microsoft/teams.graph": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@microsoft/teams.graph/-/teams.graph-2.0.1.tgz", - "integrity": "sha512-umO0ST5APY29WAJaP9mLPoM9QcOZ9R3viePLL8Tliq1SxluOuWJLqTEEu8pLc0z7bMX0H0Y3qz48UgPilzEYSA==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@microsoft/teams.graph/-/teams.graph-2.0.9.tgz", + "integrity": "sha512-AZDAfiGdAzA1cNh84z5p7kSqGHakOPgkoNd1sWM7Hg09jzmwh1F5TuMXLS0CKZjOOBtovXw84cqL5Pdmprq6yA==", + "license": "MIT", "dependencies": { - "@microsoft/teams.common": "2.0.1", - "qs": "^6.13.0" + "@microsoft/teams.common": "2.0.9", + "qs": "^6.14.2" }, "engines": { "node": ">=20" @@ -1320,12 +1320,12 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } @@ -1931,7 +1931,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "peer": true, + "license": "MIT", "engines": { "node": ">=18" } @@ -2524,18 +2524,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, "node_modules/vite": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", diff --git a/examples/tab/Web/package.json b/examples/tab/Web/package.json index 41009c75..3cfae025 100644 --- a/examples/tab/Web/package.json +++ b/examples/tab/Web/package.json @@ -13,7 +13,7 @@ "build": "npx vite build" }, "dependencies": { - "@microsoft/teams.client": "^2.0.0", + "@microsoft/teams.client": "^2.0.9", "@microsoft/teams.common": "^2.0.0", "@microsoft/teams.graph": "^2.0.0", "@microsoft/teams.graph-endpoints": "^2.0.0", diff --git a/examples/targeted-messages/src/main.py b/examples/targeted-messages/src/main.py index f2e3de26..c4274cfd 100644 --- a/examples/targeted-messages/src/main.py +++ b/examples/targeted-messages/src/main.py @@ -4,9 +4,9 @@ """ import asyncio +from datetime import datetime, timezone -from microsoft_teams.api import Account, MessageActivity, MessageActivityInput -from microsoft_teams.api.activities.typing import TypingActivityInput +from microsoft_teams.api import MessageActivity, MessageActivityInput from microsoft_teams.apps import ActivityContext, App """ @@ -23,112 +23,93 @@ @app.on_message async def handle_message(ctx: ActivityContext[MessageActivity]): """Handle message activities.""" - await ctx.reply(TypingActivityInput()) + activity = ctx.activity + text = (activity.text or "").lower() - text = (ctx.activity.text or "").lower() + print(f"[MESSAGE] Received: {text}") - # ============================================ - # Test targeted SEND (create) - # ============================================ - if "test send" in text: - members = await ctx.api.conversations.members(ctx.activity.conversation.id).get_all() + if "test update" in text: + # UPDATE: Send a targeted message, then update it after 3 seconds + conversation_id = activity.conversation.id - for member in members: - print(f"Member: {member.name} - {member.id}") + targeted_message = MessageActivityInput( + text="📝 This message will be **updated** in 3 seconds..." + ).with_recipient(activity.from_, is_targeted=True) - targeted_message = MessageActivityInput( - text="🔒 [SEND] This is a targeted message - only YOU can see this!" - ).with_recipient(Account(id=member.id, name=member.name), is_targeted=True) + result = await ctx.send(targeted_message) - result = await ctx.send(targeted_message) - print("[SEND] Sent targeted message") - return - - # ============================================ - # Test targeted REPLY - # ============================================ - if "test reply" in text: - targeted_reply = MessageActivityInput(text="🔒 [REPLY] Targeted reply - only YOU can see this!").with_recipient( - ctx.activity.from_, is_targeted=True - ) + if result.id: + message_id = result.id - result = await ctx.reply(targeted_reply) - print(f"Targeted REPLY result: {result}") - return + async def update_after_delay(): + await asyncio.sleep(3) + try: + timestamp = datetime.now(timezone.utc).strftime("%H:%M:%S") + updated_message = MessageActivityInput( + text=f"✏️ **Updated!** This message was modified at {timestamp}" + ) + updated_message.id = message_id - # ============================================ - # Test targeted UPDATE - # ============================================ - if "test update" in text: - # First send a targeted message - targeted_message = MessageActivityInput(text="🔒 [UPDATE] Original targeted message...").with_recipient( - ctx.activity.from_, is_targeted=True - ) + await ctx.api.conversations.activities(conversation_id).update_targeted(message_id, updated_message) + print("[UPDATE] Updated targeted message") + except Exception as err: + print(f"[UPDATE] Error: {err}") + + asyncio.create_task(update_after_delay()) + + print("[UPDATE] Scheduled update in 3 seconds") + + elif "test delete" in text: + # DELETE: Send a targeted message, then delete it after 3 seconds + conversation_id = activity.conversation.id - result = await ctx.send(targeted_message) - print(f"Initial targeted message ID: {result.id}") - - # Wait then update - async def update_after_delay(): - await asyncio.sleep(3) - try: - # For targeted updates, do not include recipient in the payload. - updated_message = MessageActivityInput( - text="🔒 [UPDATE] ✅ UPDATED targeted message! (only you see this)" - ) - updated_message.id = result.id - - await ctx.api.conversations.activities(ctx.activity.conversation.id).update_targeted( - result.id, updated_message - ) - print("Targeted UPDATE completed") - except Exception as err: - print(f"Targeted UPDATE error: {err}") - - asyncio.create_task(update_after_delay()) - return - - # ============================================ - # Test targeted DELETE - # ============================================ - if "test delete" in text: - # First send a targeted message targeted_message = MessageActivityInput( - text="🔒 [DELETE] This targeted message will be DELETED in 5 seconds..." - ).with_recipient(ctx.activity.from_, is_targeted=True) + text="🗑️ This message will be **deleted** in 3 seconds..." + ).with_recipient(activity.from_, is_targeted=True) result = await ctx.send(targeted_message) - print(f"Targeted message to delete, ID: {result.id}") - - # Wait then delete using the targeted API - async def delete_after_delay(): - await asyncio.sleep(5) - try: - await ctx.api.conversations.activities(ctx.activity.conversation.id).delete_targeted(result.id) - print("Targeted DELETE completed") - except Exception as err: - print(f"Targeted DELETE error: {err}") - - asyncio.create_task(delete_after_delay()) - return - - # ============================================ - # Help / Default - # ============================================ - if "help" in text: - await ctx.reply( - "**Targeted Messages Test Bot**\n\n" + + if result.id: + message_id = result.id + + async def delete_after_delay(): + await asyncio.sleep(3) + try: + await ctx.api.conversations.activities(conversation_id).delete_targeted(message_id) + print("[DELETE] Deleted targeted message") + except Exception as err: + print(f"[DELETE] Error: {err}") + + asyncio.create_task(delete_after_delay()) + + print("[DELETE] Scheduled delete in 3 seconds") + + elif "test public" in text: + # PUBLIC: Send a public message visible to everyone in the chat. + await ctx.send(MessageActivityInput(text="📋 Here is the public result — everyone can see this!")) + print("[PUBLIC] Sent public message") + + elif "test send" in text: + # SEND: Send a targeted message visible only to the sender. + targeted_message = MessageActivityInput( + text="👋 This is a **targeted message** — only YOU can see this!" + ).with_recipient(activity.from_, is_targeted=True) + await ctx.send(targeted_message) + print("[SEND] Sent targeted message") + + elif "help" in text: + await ctx.send( + "**🎯 Targeted Messages Demo**\n\n" "**Commands:**\n" - "- `test send` - Send a targeted message\n" - "- `test reply` - Reply with a targeted message\n" - "- `test update` - Send then update a targeted message\n" - "- `test delete` - Send then delete a targeted message\n\n" - "💡 *Test in a group chat to verify others don't see targeted messages!*" + "- `test send` - Send a targeted message (only visible to you)\n" + "- `test update` - Send a targeted message, then update it after 3 seconds\n" + "- `test delete` - Send a targeted message, then delete it after 3 seconds\n" + "- `test public` - Send a public reply (visible to all)\n\n" + "_Targeted messages are only visible to you, even in group chats!_" ) - return - # Default - await ctx.reply('Say "help" for available commands.') + else: + await ctx.send(f"You said: '{activity.text}'\n\nType `help` to see available commands.") if __name__ == "__main__": 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 7f125c95..3b1a285a 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/message.py +++ b/packages/api/src/microsoft_teams/api/activities/message/message.py @@ -6,6 +6,7 @@ from typing import Any, List, Literal, Optional, Self from microsoft_teams.cards import AdaptiveCard +from microsoft_teams.common.experimental import experimental from ...models import ( Account, @@ -32,6 +33,7 @@ Entity, Image, MessageEntity, + TargetedMessageInfoEntity, ) from ..utils import StripMentionsTextOptions, strip_mentions_text @@ -414,6 +416,36 @@ def add_feedback(self, mode: Literal["default", "custom"] = "default") -> Self: self.channel_data.feedback_loop_enabled = None return self + @experimental("ExperimentalTeamsTargeted") + def add_targeted_message_info(self, message_id: str) -> Self: + """Add a targetedMessageInfo entity for prompt preview. + + If an entity with type ``"targetedMessageInfo"`` already exists, + it is not added again (one prompt preview per message). + + When adding the entity, any ``quotedReply`` entities and matching + ```` placeholder text are removed to avoid + collision with prompt preview. + + Args: + message_id: The message ID of the targeted message. + + Returns: + Self for method chaining + """ + has_entity = any(isinstance(e, TargetedMessageInfoEntity) for e in (self.entities or [])) + + # Always strip quotedReply artifacts to avoid collision with prompt preview, + # if the developer already attached a targetedMessageInfo entity. + if self.entities is not None: + self.entities = [e for e in self.entities if getattr(e, "type", None) != "quotedReply"] + if self.text is not None: + self.text = self.text.replace(f'', "").strip() + + if not has_entity: + self.add_entity(TargetedMessageInfoEntity(message_id=message_id)) + 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/clients/reaction/client.py b/packages/api/src/microsoft_teams/api/clients/reaction/client.py index a1a20a94..ac9d68c4 100644 --- a/packages/api/src/microsoft_teams/api/clients/reaction/client.py +++ b/packages/api/src/microsoft_teams/api/clients/reaction/client.py @@ -5,7 +5,6 @@ from typing import Optional -from microsoft_teams.common.experimental import experimental from microsoft_teams.common.http import Client from ...models.message import MessageReactionType @@ -13,14 +12,9 @@ from ..base_client import BaseClient -@experimental("ExperimentalTeamsReactions") class ReactionClient(BaseClient): """ Client for working with app message reactions for a given conversation/activity. - - .. warning:: Preview - This API is in preview and may change in the future. - Diagnostic: ExperimentalTeamsReactions """ def __init__( diff --git a/packages/api/src/microsoft_teams/api/models/entity/__init__.py b/packages/api/src/microsoft_teams/api/models/entity/__init__.py index 55fd6a10..47bcdf37 100644 --- a/packages/api/src/microsoft_teams/api/models/entity/__init__.py +++ b/packages/api/src/microsoft_teams/api/models/entity/__init__.py @@ -18,8 +18,10 @@ from .mention_entity import MentionEntity from .message_entity import MessageEntity from .product_info_entity import ProductInfoEntity +from .quoted_reply_entity import QuotedReplyData, QuotedReplyEntity from .sensitive_usage_entity import SensitiveUsage, SensitiveUsageEntity, SensitiveUsagePattern from .stream_info_entity import StreamInfoEntity +from .targeted_message_info_entity import TargetedMessageInfoEntity __all__ = [ "AIMessageEntity", @@ -34,9 +36,12 @@ "MentionEntity", "MessageEntity", "ProductInfoEntity", + "QuotedReplyData", + "QuotedReplyEntity", "SensitiveUsageEntity", "SensitiveUsage", "SensitiveUsagePattern", "StreamInfoEntity", + "TargetedMessageInfoEntity", "Entity", ] diff --git a/packages/api/src/microsoft_teams/api/models/entity/entity.py b/packages/api/src/microsoft_teams/api/models/entity/entity.py index 5af75dd2..0488c4aa 100644 --- a/packages/api/src/microsoft_teams/api/models/entity/entity.py +++ b/packages/api/src/microsoft_teams/api/models/entity/entity.py @@ -11,8 +11,10 @@ from .mention_entity import MentionEntity from .message_entity import MessageEntity from .product_info_entity import ProductInfoEntity +from .quoted_reply_entity import QuotedReplyEntity from .sensitive_usage_entity import SensitiveUsageEntity from .stream_info_entity import StreamInfoEntity +from .targeted_message_info_entity import TargetedMessageInfoEntity Entity = Union[ ClientInfoEntity, @@ -23,4 +25,6 @@ CitationEntity, SensitiveUsageEntity, ProductInfoEntity, + QuotedReplyEntity, + TargetedMessageInfoEntity, ] diff --git a/packages/api/src/microsoft_teams/api/models/entity/quoted_reply_entity.py b/packages/api/src/microsoft_teams/api/models/entity/quoted_reply_entity.py new file mode 100644 index 00000000..74ec0525 --- /dev/null +++ b/packages/api/src/microsoft_teams/api/models/entity/quoted_reply_entity.py @@ -0,0 +1,57 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Literal, Optional + +from microsoft_teams.common.experimental import experimental + +from ..custom_base_model import CustomBaseModel + + +@experimental("ExperimentalTeamsQuotedReplies") +class QuotedReplyData(CustomBaseModel): + """Data for a quoted reply entity + + .. warning:: Coming Soon + This API is coming soon and may change in the future. + Diagnostic: ExperimentalTeamsQuotedReplies + """ + + message_id: str + "ID of the message being quoted" + + sender_id: Optional[str] = None + "ID of the sender of the quoted message" + + sender_name: Optional[str] = None + "Name of the sender of the quoted message" + + preview: Optional[str] = None + "Preview text of the quoted message" + + time: Optional[str] = None + "Timestamp of the quoted message (IC3 epoch value, e.g. '1772050244572'). Inbound only." + + is_reply_deleted: Optional[bool] = None + "Whether the quoted reply has been deleted" + + validated_message_reference: Optional[bool] = None + "Whether the message reference has been validated" + + +@experimental("ExperimentalTeamsQuotedReplies") +class QuotedReplyEntity(CustomBaseModel): + """Entity containing quoted reply information + + .. warning:: Coming Soon + This API is coming soon and may change in the future. + Diagnostic: ExperimentalTeamsQuotedReplies + """ + + type: Literal["quotedReply"] = "quotedReply" + "Type identifier for quoted reply" + + quoted_reply: QuotedReplyData + "The quoted reply data" diff --git a/packages/api/src/microsoft_teams/api/models/entity/targeted_message_info_entity.py b/packages/api/src/microsoft_teams/api/models/entity/targeted_message_info_entity.py new file mode 100644 index 00000000..aa97e57e --- /dev/null +++ b/packages/api/src/microsoft_teams/api/models/entity/targeted_message_info_entity.py @@ -0,0 +1,26 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Literal + +from microsoft_teams.common.experimental import experimental + +from ..custom_base_model import CustomBaseModel + + +@experimental("ExperimentalTeamsTargeted") +class TargetedMessageInfoEntity(CustomBaseModel): + """Entity containing targeted message information for prompt preview. + + .. warning:: Preview + This class is in preview and may change in the future. + Diagnostic: ExperimentalTeamsTargeted + """ + + type: Literal["targetedMessageInfo"] = "targetedMessageInfo" + "Type identifier for targeted message info" + + message_id: str + "The ID of the targeted message this activity is replying to" diff --git a/packages/api/tests/unit/test_targeted_message_info_entity.py b/packages/api/tests/unit/test_targeted_message_info_entity.py new file mode 100644 index 00000000..82c35da2 --- /dev/null +++ b/packages/api/tests/unit/test_targeted_message_info_entity.py @@ -0,0 +1,109 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" +# pyright: basic + +import pytest +from microsoft_teams.api import MessageActivityInput +from microsoft_teams.api.models.entity.targeted_message_info_entity import TargetedMessageInfoEntity + + +@pytest.mark.unit +class TestTargetedMessageInfoEntity: + """Unit tests for TargetedMessageInfoEntity.""" + + def test_default_type(self) -> None: + entity = TargetedMessageInfoEntity(message_id="1772129782775") + assert entity.type == "targetedMessageInfo" + + def test_message_id(self) -> None: + entity = TargetedMessageInfoEntity(message_id="1772129782775") + assert entity.message_id == "1772129782775" + + def test_serialization_camel_case(self) -> None: + entity = TargetedMessageInfoEntity(message_id="1772129782775") + data = entity.model_dump(by_alias=True, exclude_none=True) + assert data == { + "type": "targetedMessageInfo", + "messageId": "1772129782775", + } + + def test_deserialization_camel_case(self) -> None: + entity = TargetedMessageInfoEntity.model_validate( + { + "type": "targetedMessageInfo", + "messageId": "1772129782775", + } + ) + assert entity.type == "targetedMessageInfo" + assert entity.message_id == "1772129782775" + + +@pytest.mark.unit +class TestAddTargetedMessageInfo: + """Tests for MessageActivityInput.add_targeted_message_info including QR collision guard.""" + + def test_adds_entity(self) -> None: + activity = MessageActivityInput(text="test") + activity.add_targeted_message_info("12345") + + targeted = [e for e in (activity.entities or []) if isinstance(e, TargetedMessageInfoEntity)] + assert len(targeted) == 1 + assert targeted[0].message_id == "12345" + + def test_does_not_duplicate_when_entity_exists(self) -> None: + activity = MessageActivityInput(text="test") + activity.add_entity(TargetedMessageInfoEntity(message_id="9999")) + activity.add_targeted_message_info("12345") + + targeted = [e for e in (activity.entities or []) if isinstance(e, TargetedMessageInfoEntity)] + assert len(targeted) == 1 + assert targeted[0].message_id == "9999" + + def test_strips_qr_even_when_entity_already_exists(self) -> None: + """When developer pre-attaches targetedMessageInfo and reply() adds quotedReply, + add_targeted_message_info should still strip the quotedReply artifacts.""" + from unittest.mock import MagicMock + + qr_entity = MagicMock() + qr_entity.type = "quotedReply" + + activity = MessageActivityInput(text=' Here is my reply') + activity.add_entity(TargetedMessageInfoEntity(message_id="9999")) + activity.entities.append(qr_entity) # type: ignore[arg-type] + + activity.add_targeted_message_info("12345") + + # Should not duplicate the entity + targeted = [e for e in (activity.entities or []) if isinstance(e, TargetedMessageInfoEntity)] + assert len(targeted) == 1 + assert targeted[0].message_id == "9999" + # Should still strip quotedReply + assert not any(getattr(e, "type", None) == "quotedReply" for e in (activity.entities or [])) + assert activity.text == "Here is my reply" + + def test_strips_quoted_reply_entities(self) -> None: + activity = MessageActivityInput(text="test", entities=[]) + # Simulate a quotedReply entity (generic entity with type="quotedReply") + + # Use a mock-like approach: create a simple object with type="quotedReply" + from unittest.mock import MagicMock + + qr_entity = MagicMock() + qr_entity.type = "quotedReply" + activity.entities = [qr_entity] # type: ignore[list-item] + + activity.add_targeted_message_info("12345") + + assert not any(getattr(e, "type", None) == "quotedReply" for e in (activity.entities or [])) + targeted = [e for e in (activity.entities or []) if isinstance(e, TargetedMessageInfoEntity)] + assert len(targeted) == 1 + + def test_strips_quoted_placeholder_from_text(self) -> None: + activity = MessageActivityInput(text=' Here is my reply') + activity.add_targeted_message_info("12345") + + assert activity.text == "Here is my reply" + targeted = [e for e in (activity.entities or []) if isinstance(e, TargetedMessageInfoEntity)] + assert len(targeted) == 1 diff --git a/packages/apps/src/microsoft_teams/apps/activity_sender.py b/packages/apps/src/microsoft_teams/apps/activity_sender.py index 78d02ae8..1ab97c96 100644 --- a/packages/apps/src/microsoft_teams/apps/activity_sender.py +++ b/packages/apps/src/microsoft_teams/apps/activity_sender.py @@ -47,6 +47,15 @@ async def send(self, activity: ActivityParams, ref: ConversationReference) -> Se Returns: The sent activity with id and other server-populated fields """ + is_targeted = ( + isinstance(activity, MessageActivityInput) + and activity.recipient is not None + and activity.recipient.is_targeted is True + ) + + if is_targeted and ref.conversation.conversation_type == "personal": + raise ValueError("Targeted messages are not supported in 1:1 (personal) chats.") + # Create API client for this conversation's service URL api = ApiClient(service_url=ref.service_url, options=self._client) @@ -54,11 +63,6 @@ async def send(self, activity: ActivityParams, ref: ConversationReference) -> Se activity.from_ = ref.bot activity.conversation = ref.conversation - is_targeted = ( - isinstance(activity, MessageActivityInput) - and activity.recipient is not None - and activity.recipient.is_targeted is True - ) is_update = hasattr(activity, "id") and activity.id activities = api.conversations.activities(ref.conversation.id) diff --git a/packages/apps/src/microsoft_teams/apps/auth/token_validator.py b/packages/apps/src/microsoft_teams/apps/auth/token_validator.py index 085d0481..a4f1df1d 100644 --- a/packages/apps/src/microsoft_teams/apps/auth/token_validator.py +++ b/packages/apps/src/microsoft_teams/apps/auth/token_validator.py @@ -108,7 +108,12 @@ def for_entra( env = cloud or PUBLIC valid_issuers: List[str] = [] if tenant_id: + # Accept both Azure AD v2 (login.microsoftonline.com/.../v2.0) and + # v1 (sts.windows.net/.../) issuer formats. Some valid Entra tokens + # are still issued with the v1 issuer. + # See: https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens valid_issuers.append(f"{env.login_endpoint}/{tenant_id}/v2.0") + valid_issuers.append(f"https://sts.windows.net/{tenant_id}/") else: logger.warning( "No tenant_id provided for Entra token validation. " diff --git a/packages/apps/src/microsoft_teams/apps/http_stream.py b/packages/apps/src/microsoft_teams/apps/http_stream.py index 5aa515ee..48f07fab 100644 --- a/packages/apps/src/microsoft_teams/apps/http_stream.py +++ b/packages/apps/src/microsoft_teams/apps/http_stream.py @@ -131,10 +131,10 @@ def update(self, text: str) -> None: self.emit(TypingActivityInput().with_text(text).with_channel_data(ChannelData(stream_type="informative"))) async def _wait_for_id_and_queue(self): - """Wait until _id is set and the queue is empty, with a total timeout.""" + """Wait until _id is set, the queue is empty, and no flush is in progress, with a total timeout.""" async def _poll(): - while (self._queue or not self._id) and not self._canceled: + while (self._queue or not self._id or self._lock.locked()) and not self._canceled: await self._state_changed.wait() self._state_changed.clear() @@ -158,10 +158,12 @@ async def close(self) -> Optional[SentActivity]: logger.debug("stream has no content to send, returning None") return None - # Wait until _id is set and queue is empty + # Wait until _id is set, queue is empty, and no flush is in progress result = await self._wait_for_id_and_queue() if not result: - logger.warning("Timeout while waiting for _id to be set and queue to be empty, cannot close stream") + logger.warning( + "Timeout while waiting for _id to be set, queue to be empty, and flush to complete, cannot close stream" + ) return None has_content = ( 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 1963c185..e6c8710a 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py +++ b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py @@ -6,6 +6,7 @@ import base64 import json import logging +import warnings from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Awaitable, Callable, Generic, Optional, TypeVar, cast @@ -35,6 +36,7 @@ from microsoft_teams.api.models.oauth import OAuthCard from microsoft_teams.cards import AdaptiveCard from microsoft_teams.common import Storage +from microsoft_teams.common.experimental import ExperimentalWarning from microsoft_teams.common.http.client_token import Token from ..activity_sender import ActivitySender @@ -181,6 +183,8 @@ async def send( else: activity = message + self._add_targeted_message_info_entity(activity) + ref = conversation_ref or self.conversation_ref res = await self._activity_sender.send(activity, ref) return res @@ -209,6 +213,31 @@ def set_next(self, handler: Callable[[], Awaitable[None]]) -> None: """Set the next handler in the middleware chain.""" self._next_handler = handler + def _is_incoming_targeted(self) -> bool: + """Check if the incoming activity is a targeted message.""" + activity = self.activity + return ( + hasattr(activity, "recipient") + and hasattr(activity.recipient, "is_targeted") + and activity.recipient.is_targeted is True + ) + + def _add_targeted_message_info_entity(self, activity_params: ActivityParams) -> None: + """Auto-populate targetedMessageInfo entity when replying to a targeted message. + + In the reactive flow, the SDK reads the incoming targeted message ID + and attaches the entity automatically so the developer doesn't need to. + Skips if the developer already attached a targetedMessageInfo entity. + """ + if not self._is_incoming_targeted(): + return + if not isinstance(activity_params, MessageActivityInput): + return + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", ExperimentalWarning) + activity_params.add_targeted_message_info(self.activity.id) + def _build_block_quote_for_activity(self) -> Optional[str]: if self.activity.type == "message" and hasattr(self.activity, "text"): activity = cast(MessageActivityInput, self.activity) diff --git a/packages/apps/tests/test_activity_context.py b/packages/apps/tests/test_activity_context.py index 83b152da..e89eb0ee 100644 --- a/packages/apps/tests/test_activity_context.py +++ b/packages/apps/tests/test_activity_context.py @@ -9,7 +9,8 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from microsoft_teams.api import Account, MessageActivityInput, SentActivity +from microsoft_teams.api import Account, MessageActivityInput, SentActivity, TargetedMessageInfoEntity +from microsoft_teams.api.activities.typing import TypingActivityInput from microsoft_teams.api.auth.cloud_environment import PUBLIC from microsoft_teams.apps.routing.activity_context import ActivityContext @@ -577,3 +578,96 @@ async def test_sign_out_logs_error_and_does_not_raise_on_failure(self) -> None: mock_log_error.assert_called_once() logged_message = mock_log_error.call_args[0][0] assert "Failed to sign out user" in logged_message + + +class TestActivityContextPromptPreview: + """Tests for reactive auto-population of targetedMessageInfo entity.""" + + def _make_targeted_activity(self, activity_id: str = "1772129782775") -> MagicMock: + mock_activity = MagicMock() + mock_activity.type = "message" + mock_activity.id = activity_id + mock_activity.text = "Hello from slash command" + mock_activity.from_ = Account(id="user-123", name="Test User") + mock_activity.recipient = Account(id="bot-456", name="Bot", is_targeted=True) + return mock_activity + + def _make_non_targeted_activity(self) -> MagicMock: + mock_activity = MagicMock() + mock_activity.type = "message" + mock_activity.id = "normal-msg-id" + mock_activity.text = "Normal message" + mock_activity.from_ = Account(id="user-123", name="Test User") + mock_activity.recipient = Account(id="bot-456", name="Bot") + return mock_activity + + @pytest.mark.asyncio + async def test_send_auto_adds_targeted_message_info_entity(self) -> None: + """When replying to a targeted message, the SDK auto-adds targetedMessageInfo.""" + activity = self._make_targeted_activity("1772129782775") + ctx, mock_sender = _create_activity_context(activity=activity) + + await ctx.send("Here is your agenda") + + sent_activity = mock_sender.send.call_args[0][0] + assert sent_activity.entities is not None + assert len(sent_activity.entities) == 1 + entity = sent_activity.entities[0] + assert isinstance(entity, TargetedMessageInfoEntity) + assert entity.message_id == "1772129782775" + assert entity.type == "targetedMessageInfo" + + @pytest.mark.asyncio + async def test_send_does_not_add_entity_for_non_targeted(self) -> None: + """When replying to a normal message, no targetedMessageInfo is added.""" + activity = self._make_non_targeted_activity() + ctx, mock_sender = _create_activity_context(activity=activity) + + await ctx.send("Normal reply") + + sent_activity = mock_sender.send.call_args[0][0] + assert sent_activity.entities is None + + @pytest.mark.asyncio + async def test_send_does_not_duplicate_entity_if_already_present(self) -> None: + """If the developer already added targetedMessageInfo, the SDK does not duplicate it.""" + activity = self._make_targeted_activity("1772129782775") + ctx, mock_sender = _create_activity_context(activity=activity) + + msg = MessageActivityInput(text="Reply").add_entity(TargetedMessageInfoEntity(message_id="custom-id")) + await ctx.send(msg) + + sent_activity = mock_sender.send.call_args[0][0] + assert sent_activity.entities is not None + assert len(sent_activity.entities) == 1 + assert sent_activity.entities[0].message_id == "custom-id" + + @pytest.mark.asyncio + async def test_reply_auto_adds_targeted_message_info_entity(self) -> None: + """reply() also auto-adds targetedMessageInfo for targeted messages. + The blockquote is added by reply(), then stripped by add_targeted_message_info + in send() to avoid collision with prompt preview.""" + activity = self._make_targeted_activity("1772129782775") + ctx, mock_sender = _create_activity_context(activity=activity) + + await ctx.reply("Reply with prompt preview") + + sent_activity = mock_sender.send.call_args[0][0] + assert sent_activity.entities is not None + targeted_entities = [e for e in sent_activity.entities if isinstance(e, TargetedMessageInfoEntity)] + assert len(targeted_entities) == 1 + assert targeted_entities[0].message_id == "1772129782775" + + # quotedReply entities should be stripped by add_targeted_message_info + assert not any(getattr(e, "type", None) == "quotedReply" for e in sent_activity.entities) + + @pytest.mark.asyncio + async def test_send_does_not_add_entity_for_non_message_activity(self) -> None: + """Non-message activities (e.g. typing) should not get targetedMessageInfo attached.""" + activity = self._make_targeted_activity("1772129782775") + ctx, mock_sender = _create_activity_context(activity=activity) + + await ctx.send(TypingActivityInput()) + + sent_activity = mock_sender.send.call_args[0][0] + assert sent_activity.entities is None diff --git a/packages/apps/tests/test_activity_sender.py b/packages/apps/tests/test_activity_sender.py index 384edbb9..59965934 100644 --- a/packages/apps/tests/test_activity_sender.py +++ b/packages/apps/tests/test_activity_sender.py @@ -36,6 +36,16 @@ def conversation_ref(self): service_url="https://test.service.url", ) + @pytest.fixture + def group_conversation_ref(self): + """Create a group chat conversation reference for testing.""" + return ConversationReference( + bot=Account(id="bot-123", name="Test Bot", role="bot"), + conversation=ConversationAccount(id="conv-789", conversation_type="groupChat"), + channel_id="msteams", + service_url="https://test.service.url", + ) + def _create_sent_activity(self, activity, activity_id="msg-123"): """Helper to create a proper SentActivity mock.""" return SentActivity(id=activity_id, activity_params=activity) @@ -94,7 +104,7 @@ async def test_send_sets_from_and_conversation(self, sender, conversation_ref): assert activity.conversation == conversation_ref.conversation @pytest.mark.asyncio - async def test_send_targeted_message_calls_create_targeted(self, sender, conversation_ref): + async def test_send_targeted_message_calls_create_targeted(self, sender, group_conversation_ref): """Test that targeted messages use the create_targeted method.""" recipient = Account(id="user-123", name="Test User", role="user") activity = MessageActivityInput(text="Hello").with_recipient(recipient, is_targeted=True) @@ -107,7 +117,7 @@ async def test_send_targeted_message_calls_create_targeted(self, sender, convers mock_api.conversations.activities.return_value = mock_activities mock_api_client.return_value = mock_api - await sender.send(activity, conversation_ref) + await sender.send(activity, group_conversation_ref) mock_activities.create_targeted.assert_called_once_with(activity) mock_activities.create.assert_not_called() @@ -131,7 +141,7 @@ async def test_send_non_targeted_message_does_not_call_create_targeted(self, sen mock_activities.create_targeted.assert_not_called() @pytest.mark.asyncio - async def test_update_targeted_message_calls_update_targeted(self, sender, conversation_ref): + async def test_update_targeted_message_calls_update_targeted(self, sender, group_conversation_ref): """Test that targeted message updates use the update_targeted method.""" activity = MessageActivityInput(text="Updated targeted message") activity.recipient = Account(id="user-123", name="Test User", role="user", is_targeted=True) @@ -147,7 +157,7 @@ async def test_update_targeted_message_calls_update_targeted(self, sender, conve mock_api.conversations.activities.return_value = mock_activities mock_api_client.return_value = mock_api - await sender.send(activity, conversation_ref) + await sender.send(activity, group_conversation_ref) mock_activities.update_targeted.assert_called_once_with("existing-msg-id", activity) mock_activities.update.assert_not_called() @@ -170,3 +180,32 @@ async def test_update_non_targeted_message_calls_update(self, sender, conversati mock_activities.update.assert_called_once_with("existing-msg-id", activity) mock_activities.update_targeted.assert_not_called() + + @pytest.mark.asyncio + async def test_send_targeted_in_personal_chat_raises(self, sender, conversation_ref): + """Test that sending a targeted message in a personal (1:1) chat raises ValueError.""" + activity = MessageActivityInput(text="Hello").with_recipient( + Account(id="user-1", name="User"), is_targeted=True + ) + + with pytest.raises(ValueError, match="Targeted messages are not supported in 1:1"): + await sender.send(activity, conversation_ref) + + @pytest.mark.asyncio + async def test_send_targeted_in_group_chat_succeeds(self, sender, group_conversation_ref): + """Test that sending a targeted message in a group chat proceeds normally.""" + activity = MessageActivityInput(text="Hello").with_recipient( + Account(id="user-1", name="User"), is_targeted=True + ) + + mock_activities = MagicMock() + mock_activities.create_targeted = AsyncMock(return_value=self._create_sent_activity(activity)) + + with patch("microsoft_teams.apps.activity_sender.ApiClient") as mock_api_client: + mock_api = MagicMock() + mock_api.conversations.activities.return_value = mock_activities + mock_api_client.return_value = mock_api + + await sender.send(activity, group_conversation_ref) + + mock_activities.create_targeted.assert_called_once() diff --git a/packages/apps/tests/test_http_stream.py b/packages/apps/tests/test_http_stream.py index 64d7b124..53173c4c 100644 --- a/packages/apps/tests/test_http_stream.py +++ b/packages/apps/tests/test_http_stream.py @@ -426,3 +426,30 @@ async def mock_send(activity): assert result.activity_params.suggested_actions is not None assert len(result.activity_params.suggested_actions.actions) == 2 assert result.activity_params.suggested_actions.actions[0].title == "Option A" + + @pytest.mark.asyncio + async def test_close_waits_for_flush_to_complete(self, mock_api_client, conversation_reference): + """close() must not send the final message while a flush is still mid-await.""" + stream = HttpStream(mock_api_client, conversation_reference) + + # Simulate a flush in progress: lock held, _id assigned, text accumulated. + # This mirrors the window after the inner queue drain but before SendActivity awaits resolve. + await stream._lock.acquire() + stream._id = "activity-1" + stream._text = "Response text" + + close_task = asyncio.create_task(stream.close()) + + # Give close() a chance to enter its wait loop, then confirm it has not sent the final message yet. + await asyncio.sleep(0.05) + assert mock_api_client.send_call_count == 0 + assert not close_task.done() + + # Release the lock and signal — close() should now proceed. + stream._lock.release() + stream._state_changed.set() + + result = await close_task + assert result is not None + assert mock_api_client.send_call_count == 1 + assert mock_api_client.sent_activities[0].text == "Response text" diff --git a/packages/apps/tests/test_token_validator.py b/packages/apps/tests/test_token_validator.py index 88d1abe7..eaa1bab6 100644 --- a/packages/apps/tests/test_token_validator.py +++ b/packages/apps/tests/test_token_validator.py @@ -250,7 +250,10 @@ def test_validate_service_url_direct(self, validator): def test_for_entra_initialization(self, validator_entra): """Check Entra-specific initialization.""" options = validator_entra.options - assert options.valid_issuers == ["https://login.microsoftonline.com/test-tenant-id/v2.0"] + assert options.valid_issuers == [ + "https://login.microsoftonline.com/test-tenant-id/v2.0", + "https://sts.windows.net/test-tenant-id/", + ] assert options.valid_audiences == ["test-app-id", "api://test-app-id", "api://botid-test-app-id"] assert options.jwks_uri == "https://login.microsoftonline.com/test-tenant-id/discovery/v2.0/keys" assert options.scope == "user.read" @@ -384,3 +387,23 @@ def test_for_entra_without_tenant_id_logs_warning(self, caplog): assert validator.options.valid_issuers == [] assert "Issuer validation will be skipped" in caplog.text + @pytest.mark.asyncio + async def test_validate_entra_token_v1_sts_issuer(self, mock_jwks_client): + """Validator should accept the Azure AD v1 sts.windows.net issuer.""" + validator = TokenValidator.for_entra(app_id="test-app-id", tenant_id="test-tenant-id", scope="user.read") + validator._jwks_client = mock_jwks_client + payload_v1 = { + "iss": "https://sts.windows.net/test-tenant-id/", + "aud": "test-app-id", + "scp": "user.read", + "appid": "test-app-id", + "tid": "test-tenant-id", + "ver": "1.0", + "exp": 9999999999, + "iat": 1000000000, + } + + with patch("jwt.decode", return_value=payload_v1): + result = await validator.validate_token("v1.entra.token") + assert result["iss"] == "https://sts.windows.net/test-tenant-id/" + assert result["ver"] == "1.0" diff --git a/uv.lock b/uv.lock index d8a0b9b2..53ca4413 100644 --- a/uv.lock +++ b/uv.lock @@ -1512,16 +1512,16 @@ requires-dist = [ [[package]] name = "microsoft-kiota-abstractions" -version = "1.9.7" +version = "1.9.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, { name = "std-uritemplate" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/6c/fd855a03545ae261b28d179b206e5f80a0e7c95fac5a580514c4dabedca0/microsoft_kiota_abstractions-1.9.7.tar.gz", hash = "sha256:731ed60c2df74ca80d1bf36d40a4c390aab353db3a76796c63ea9e9a220ce65c", size = 24447, upload-time = "2025-09-09T13:53:42.631Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/e1/39de28380fc0eddf12f66099469fb7561bc38f577ea06e3a074751ebbcd9/microsoft_kiota_abstractions-1.9.10.tar.gz", hash = "sha256:8eb62d64c35ad0eeb4e8bcdbb143c0b308dc4a494e757f8e44cb959d34f44ecf", size = 24473, upload-time = "2026-03-12T17:27:15.398Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/d8/d699a2cb209c72f1258af5f582a7868d1b006e57cc4394b68b0f996ba370/microsoft_kiota_abstractions-1.9.7-py3-none-any.whl", hash = "sha256:8add66c38d05ab9a496c1c843bb16e04b70edc4651dc290b9629b14009f5c0c0", size = 44404, upload-time = "2025-09-09T13:53:41.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/59/bf0cb26c80fbd3fa882df8474ad87e9dbd742656c376388c427c4e314171/microsoft_kiota_abstractions-1.9.10-py3-none-any.whl", hash = "sha256:cd169067ebe48e6feea1258630807034239e0c61c2abe5fd66896a58177e8f05", size = 44462, upload-time = "2026-03-12T17:27:16.532Z" }, ] [[package]] @@ -1542,7 +1542,7 @@ wheels = [ [[package]] name = "microsoft-kiota-http" -version = "1.9.7" +version = "1.9.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx", extra = ["http2"] }, @@ -1550,9 +1550,9 @@ dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/a9/7efe67311902394a208545ae067dfc7e957383939b0ee6ff43e1955afbe7/microsoft_kiota_http-1.9.7.tar.gz", hash = "sha256:abcacca784649308ab93d8578c2afb581a42deed048b183d7bbdc48c325dd6a1", size = 21249, upload-time = "2025-09-09T13:54:00.45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/3f/fc18eb0d1d845daf6355fd54fd990af7f7e10043ef6a6da39b9e5981cbaf/microsoft_kiota_http-1.9.9.tar.gz", hash = "sha256:ae672b145df71b644f8da0951767a12a4ce47a40576d86eba19b7c22d9e160f9", size = 21493, upload-time = "2026-03-02T21:04:11.662Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/99/1d625b9353cabb3aaddb468c379b1e1fc726795281e94437096846b434b1/microsoft_kiota_http-1.9.7-py3-none-any.whl", hash = "sha256:14ce6b14c4fa93608f535f2c6ae21d35b1d0e2635ab70501fa3a3afc90135261", size = 31577, upload-time = "2025-09-09T13:53:59.616Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6a/cc1b1055b4b6d4dfc1be7a71917c2f0ef19c070c6a18b16d3c1032d20925/microsoft_kiota_http-1.9.9-py3-none-any.whl", hash = "sha256:a5b1b217ac9afeb4054f12515417e3b1d2be12a9385a70a41d18d64379ea2e7e", size = 31945, upload-time = "2026-03-02T21:04:12.328Z" }, ] [[package]] @@ -2487,11 +2487,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.26" +version = "0.0.27" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/9b/f23807317a113dc36e74e75eb265a02dd1a4d9082abc3c1064acd22997c4/python_multipart-0.0.27.tar.gz", hash = "sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602", size = 44043, upload-time = "2026-04-27T10:51:26.649Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, + { url = "https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645", size = 29254, upload-time = "2026-04-27T10:51:24.997Z" }, ] [[package]] @@ -2941,11 +2941,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] diff --git a/version.json b/version.json index 09c855a6..a74040dd 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "2.0.0", + "version": "2.0.11", "versionHeightOffset": 1, "publicReleaseRefSpec": [ "^refs/heads/release$"