diff --git a/mypy.ini b/mypy.ini index 57ef3aeb..a2cf5c54 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,6 +2,7 @@ python_version = 3.12 strict = true exclude = tests/ +no_namespace_packages = true # from https://blog.wolt.com/engineering/2021/09/30/professional-grade-mypy-configuration/ disallow_untyped_defs = true diff --git a/packages/api/pyproject.toml b/packages/api/pyproject.toml index 23e86fc0..81de54c9 100644 --- a/packages/api/pyproject.toml +++ b/packages/api/pyproject.toml @@ -10,7 +10,13 @@ authors = [ { name = "Microsoft", email = "teams@microsoft.com" } ] requires-python = ">=3.12" -dependencies = [] +dependencies = [ + "pydantic>=2.0.0", + "microsoft-teams-common", +] + +[tool.uv.sources] +"microsoft-teams-common" = { workspace = true } [project.urls] Homepage = "https://github.com/microsoft/teams.py/tree/main/packages/api/src/microsoft/teams/api" diff --git a/packages/api/src/microsoft/teams/api/__init__.py b/packages/api/src/microsoft/teams/api/__init__.py index c6bd566e..09bb450b 100644 --- a/packages/api/src/microsoft/teams/api/__init__.py +++ b/packages/api/src/microsoft/teams/api/__init__.py @@ -3,6 +3,13 @@ Licensed under the MIT License. """ +from .clients import * # noqa: F403 +from .clients import __all__ as clients_all +from .models import * # noqa: F403 +from .models import __all__ as models_all -def hello() -> str: - return "Hello from api!" +# Combine all exports from submodules +__all__ = [ + *clients_all, + *models_all, +] diff --git a/packages/api/src/microsoft/teams/api/clients/__init__.py b/packages/api/src/microsoft/teams/api/clients/__init__.py new file mode 100644 index 00000000..2dba6e03 --- /dev/null +++ b/packages/api/src/microsoft/teams/api/clients/__init__.py @@ -0,0 +1,11 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from .conversation import * # noqa: F403 +from .conversation import __all__ as conversation_all + +__all__ = [ + *conversation_all, +] diff --git a/packages/api/src/microsoft/teams/api/clients/base_client.py b/packages/api/src/microsoft/teams/api/clients/base_client.py new file mode 100644 index 00000000..0026a5d3 --- /dev/null +++ b/packages/api/src/microsoft/teams/api/clients/base_client.py @@ -0,0 +1,30 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Optional, Union + +from microsoft.teams.common.http import Client, ClientOptions + + +class BaseClient: + """Base client""" + + def __init__(self, options: Optional[Union[Client, ClientOptions]] = None) -> None: + """Initialize the BaseClient. + + Args: + options: Optional Client or ClientOptions instance. If not provided, a default Client will be created. + """ + self._http = Client(options or ClientOptions()) + + @property + def http(self) -> Client: + """Get the HTTP client instance.""" + return self._http + + @http.setter + def http(self, value: Client) -> None: + """Set the HTTP client instance.""" + self._http = value diff --git a/packages/api/src/microsoft/teams/api/clients/conversation/__init__.py b/packages/api/src/microsoft/teams/api/clients/conversation/__init__.py new file mode 100644 index 00000000..dc421bc9 --- /dev/null +++ b/packages/api/src/microsoft/teams/api/clients/conversation/__init__.py @@ -0,0 +1,18 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from .activity import ConversationActivityClient +from .client import ConversationClient +from .member import ConversationMemberClient +from .params import CreateConversationParams, GetConversationsParams, GetConversationsResponse + +__all__ = [ + "ConversationActivityClient", + "ConversationClient", + "ConversationMemberClient", + "CreateConversationParams", + "GetConversationsParams", + "GetConversationsResponse", +] diff --git a/packages/api/src/microsoft/teams/api/clients/conversation/activity.py b/packages/api/src/microsoft/teams/api/clients/conversation/activity.py new file mode 100644 index 00000000..ce2d470a --- /dev/null +++ b/packages/api/src/microsoft/teams/api/clients/conversation/activity.py @@ -0,0 +1,108 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import List, Optional + +from microsoft.teams.common.http import Client + +from ...models import Account, Activity +from ..base_client import BaseClient + + +class ConversationActivityClient(BaseClient): + """ + Client for managing activities in a Teams conversation. + """ + + def __init__(self, service_url: str, http_client: Optional[Client] = None): + """ + Initialize the conversation activity client. + + Args: + service_url: The base URL for the Teams service + http_client: Optional HTTP client to use. If not provided, a new one will be created. + """ + super().__init__(http_client) + self.service_url = service_url + + async def create(self, conversation_id: str, activity: Activity) -> Activity: + """ + Create a new activity in a conversation. + + Args: + conversation_id: The ID of the conversation + activity: The activity to create + + Returns: + The created activity + """ + response = await self.http.post( + f"{self.service_url}/v3/conversations/{conversation_id}/activities", + json=activity.model_dump(by_alias=True), + ) + return Activity.model_validate(response.json()) + + async def update(self, conversation_id: str, activity_id: str, activity: Activity) -> Activity: + """ + Update an existing activity in a conversation. + + Args: + conversation_id: The ID of the conversation + activity_id: The ID of the activity to update + activity: The updated activity data + + Returns: + The updated activity + """ + response = await self.http.put( + f"{self.service_url}/v3/conversations/{conversation_id}/activities/{activity_id}", + json=activity.model_dump(by_alias=True), + ) + return Activity.model_validate(response.json()) + + async def reply(self, conversation_id: str, activity_id: str, activity: Activity) -> Activity: + """ + Reply to an activity in a conversation. + + Args: + conversation_id: The ID of the conversation + activity_id: The ID of the activity to reply to + activity: The reply activity + + Returns: + The created reply activity + """ + activity.reply_to_id = activity_id + response = await self.http.post( + f"{self.service_url}/v3/conversations/{conversation_id}/activities/{activity_id}", + json=activity.model_dump(by_alias=True), + ) + return Activity.model_validate(response.json()) + + async def delete(self, conversation_id: str, activity_id: str) -> None: + """ + Delete an activity from a conversation. + + Args: + conversation_id: The ID of the conversation + activity_id: The ID of the activity to delete + """ + await self.http.delete(f"{self.service_url}/v3/conversations/{conversation_id}/activities/{activity_id}") + + async def get_members(self, conversation_id: str, activity_id: str) -> List[Account]: + """ + Get the members associated with an activity. + + Args: + conversation_id: The ID of the conversation + activity_id: The ID of the activity + + Returns: + List of Account objects representing the activity members + """ + response = await self.http.get( + f"{self.service_url}/v3/conversations/{conversation_id}/activities/{activity_id}/members" + ) + return [Account.model_validate(member) for member in response.json()] diff --git a/packages/api/src/microsoft/teams/api/clients/conversation/client.py b/packages/api/src/microsoft/teams/api/clients/conversation/client.py new file mode 100644 index 00000000..943b3a7f --- /dev/null +++ b/packages/api/src/microsoft/teams/api/clients/conversation/client.py @@ -0,0 +1,131 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Any, Optional, Union + +from microsoft.teams.common.http import Client, ClientOptions + +from ...models import ConversationResource +from ..base_client import BaseClient +from .activity import ConversationActivityClient +from .member import ConversationMemberClient +from .params import ( + CreateConversationParams, + GetConversationsParams, + GetConversationsResponse, +) + + +class ConversationOperations: + """Base class for conversation operations.""" + + def __init__(self, client: "ConversationClient", conversation_id: str) -> None: + self._client = client + self._conversation_id = conversation_id + + +class ActivityOperations(ConversationOperations): + """Operations for managing activities in a conversation.""" + + async def create(self, activity: Any) -> Any: + return await self._client._activities.create(self._conversation_id, activity) + + async def update(self, activity_id: str, activity: Any) -> Any: + return await self._client._activities.update(self._conversation_id, activity_id, activity) + + async def reply(self, activity_id: str, activity: Any) -> Any: + return await self._client._activities.reply(self._conversation_id, activity_id, activity) + + async def delete(self, activity_id: str) -> None: + await self._client._activities.delete(self._conversation_id, activity_id) + + async def get_members(self, activity_id: str) -> Any: + return await self._client._activities.get_members(self._conversation_id, activity_id) + + +class MemberOperations(ConversationOperations): + """Operations for managing members in a conversation.""" + + async def get_all(self) -> Any: + return await self._client._members.get(self._conversation_id) + + async def get(self, member_id: str) -> Any: + return await self._client._members.get_by_id(self._conversation_id, member_id) + + async def delete(self, member_id: str) -> None: + await self._client._members.delete(self._conversation_id, member_id) + + +class ConversationClient(BaseClient): + """Client for managing Teams conversations.""" + + def __init__(self, service_url: str, options: Optional[Union[Client, ClientOptions]] = None) -> None: + """Initialize the client. + + Args: + service_url: The Teams service URL. + options: Either an HTTP client instance or client options. If None, a default client is created. + """ + super().__init__(options) + self.service_url = service_url + + self._activities = ConversationActivityClient(service_url, self.http) + self._members = ConversationMemberClient(service_url, self.http) + + def activities(self, conversation_id: str) -> ActivityOperations: + """Get activity operations for a conversation. + + Args: + conversation_id: The ID of the conversation. + + Returns: + An operations object for managing activities in the conversation. + """ + return ActivityOperations(self, conversation_id) + + def members(self, conversation_id: str) -> MemberOperations: + """Get member operations for a conversation. + + Args: + conversation_id: The ID of the conversation. + + Returns: + An operations object for managing members in the conversation. + """ + return MemberOperations(self, conversation_id) + + async def get(self, params: Optional[GetConversationsParams] = None) -> GetConversationsResponse: + """Get a list of conversations. + + Args: + params: Optional parameters for getting conversations. + + Returns: + A response containing the list of conversations and a continuation token. + """ + query_params = {} + if params and params.continuation_token: + query_params["continuationToken"] = params.continuation_token + + response = await self.http.get( + f"{self.service_url}/v3/conversations", + params=query_params, + ) + return GetConversationsResponse.model_validate(response.json()) + + async def create(self, params: CreateConversationParams) -> ConversationResource: + """Create a new conversation. + + Args: + params: Parameters for creating the conversation. + + Returns: + The created conversation resource. + """ + response = await self.http.post( + f"{self.service_url}/v3/conversations", + json=params.model_dump(by_alias=True), + ) + return ConversationResource.model_validate(response.json()) diff --git a/packages/api/src/microsoft/teams/api/clients/conversation/member.py b/packages/api/src/microsoft/teams/api/clients/conversation/member.py new file mode 100644 index 00000000..e85c7548 --- /dev/null +++ b/packages/api/src/microsoft/teams/api/clients/conversation/member.py @@ -0,0 +1,65 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import List, Optional + +from microsoft.teams.common.http import Client + +from ...models import Account +from ..base_client import BaseClient + + +class ConversationMemberClient(BaseClient): + """ + Client for managing members in a Teams conversation. + """ + + def __init__(self, service_url: str, http_client: Optional[Client] = None): + """ + Initialize the conversation member client. + + Args: + service_url: The base URL for the Teams service + http_client: Optional HTTP client to use. If not provided, a new one will be created. + """ + super().__init__(http_client) + self.service_url = service_url + + async def get(self, conversation_id: str) -> List[Account]: + """ + Get all members in a conversation. + + Args: + conversation_id: The ID of the conversation + + Returns: + List of Account objects representing the conversation members + """ + response = await self.http.get(f"{self.service_url}/v3/conversations/{conversation_id}/members") + return [Account.model_validate(member) for member in response.json()] + + async def get_by_id(self, conversation_id: str, member_id: str) -> Account: + """ + Get a specific member in a conversation. + + Args: + conversation_id: The ID of the conversation + member_id: The ID of the member to get + + Returns: + Account object representing the conversation member + """ + response = await self.http.get(f"{self.service_url}/v3/conversations/{conversation_id}/members/{member_id}") + return Account.model_validate(response.json()) + + async def delete(self, conversation_id: str, member_id: str) -> None: + """ + Remove a member from a conversation. + + Args: + conversation_id: The ID of the conversation + member_id: The ID of the member to remove + """ + await self.http.delete(f"{self.service_url}/v3/conversations/{conversation_id}/members/{member_id}") diff --git a/packages/api/src/microsoft/teams/api/clients/conversation/params.py b/packages/api/src/microsoft/teams/api/clients/conversation/params.py new file mode 100644 index 00000000..6e1087f6 --- /dev/null +++ b/packages/api/src/microsoft/teams/api/clients/conversation/params.py @@ -0,0 +1,72 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel + +from ...models import Account, Activity, Conversation + + +class GetConversationsParams(BaseModel): + """Parameters for getting conversations.""" + + model_config = ConfigDict( + alias_generator=to_camel, + extra="allow", + ) + + continuation_token: Optional[str] = None + + +class CreateConversationParams(BaseModel): + """Parameters for creating a conversation.""" + + model_config = ConfigDict( + alias_generator=to_camel, + extra="allow", + ) + + is_group: bool = False + """ + Whether this is a group conversation. + """ + bot: Optional[Account] = None + """ + The bot account to add to the conversation. + """ + members: Optional[List[Account]] = None + """ + The members to add to the conversation. + """ + topic_name: Optional[str] = None + """ + The topic name for the conversation. + """ + tenant_id: Optional[str] = None + """ + The tenant ID for the conversation. + """ + activity: Optional[Activity] = None + """ + The initial activity to post in the conversation. + """ + channel_data: Optional[Dict[str, Any]] = None + """ + The channel-specific data for the conversation. + """ + + +class GetConversationsResponse(BaseModel): + """Response from getting conversations.""" + + model_config = ConfigDict( + alias_generator=to_camel, + extra="allow", + ) + + continuation_token: Optional[str] = Field(None, description="Token for getting the next page of conversations") + conversations: List[Conversation] = Field([], description="List of conversations") diff --git a/packages/api/src/microsoft/teams/api/models/__init__.py b/packages/api/src/microsoft/teams/api/models/__init__.py new file mode 100644 index 00000000..5c62ffd1 --- /dev/null +++ b/packages/api/src/microsoft/teams/api/models/__init__.py @@ -0,0 +1,16 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from .account import Account, AccountRole +from .activity import Activity +from .conversation import * # noqa: F403 +from .conversation import __all__ as conversation_all + +__all__ = [ + "Account", + "Activity", + "AccountRole", + *conversation_all, +] diff --git a/packages/api/src/microsoft/teams/api/models/account.py b/packages/api/src/microsoft/teams/api/models/account.py new file mode 100644 index 00000000..6184473d --- /dev/null +++ b/packages/api/src/microsoft/teams/api/models/account.py @@ -0,0 +1,41 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Any, Dict, Literal, Optional + +from pydantic import AliasGenerator, BaseModel, ConfigDict +from pydantic.alias_generators import to_camel + +AccountRole = Literal["user", "bot"] + + +class Account(BaseModel): + """ + Represents a Teams account/user. + """ + + model_config = ConfigDict( + alias_generator=AliasGenerator( + serialization_alias=to_camel, + ), + extra="allow", + ) + + id: str + """ + The unique identifier for the account. + """ + aad_object_id: Optional[str] = None + """ + The Azure AD object ID. + """ + role: Optional[AccountRole] = None + """ + The role of the account in the conversation. + """ + properties: Optional[Dict[str, Any]] = None + """ + Additional properties for the account. + """ diff --git a/packages/api/src/microsoft/teams/api/models/activity.py b/packages/api/src/microsoft/teams/api/models/activity.py new file mode 100644 index 00000000..e4c248c3 --- /dev/null +++ b/packages/api/src/microsoft/teams/api/models/activity.py @@ -0,0 +1,35 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Any, Dict, Optional + +from pydantic import AliasGenerator, BaseModel, ConfigDict +from pydantic.alias_generators import to_camel + + +# TODO: This is a barebones model for now. +class Activity(BaseModel): + """Represents a Teams activity.""" + + model_config = ConfigDict( + alias_generator=AliasGenerator( + serialization_alias=to_camel, + ), + extra="allow", + ) + + type: str = "message" + """ + The type of activity (e.g. 'message'). + """ + text: Optional[str] = None + """ + The text content of the activity. + """ + reply_to_id: Optional[str] = None + """ + The ID of the activity this is replying to. + """ + properties: Optional[Dict[str, Any]] = None diff --git a/packages/api/src/microsoft/teams/api/models/conversation/__init__.py b/packages/api/src/microsoft/teams/api/models/conversation/__init__.py new file mode 100644 index 00000000..65539723 --- /dev/null +++ b/packages/api/src/microsoft/teams/api/models/conversation/__init__.py @@ -0,0 +1,9 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from .conversation import Conversation, ConversationType +from .resource import ConversationResource + +__all__ = ["Conversation", "ConversationResource", "ConversationType"] diff --git a/packages/api/src/microsoft/teams/api/models/conversation/conversation.py b/packages/api/src/microsoft/teams/api/models/conversation/conversation.py new file mode 100644 index 00000000..5f6c5521 --- /dev/null +++ b/packages/api/src/microsoft/teams/api/models/conversation/conversation.py @@ -0,0 +1,54 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import List, Literal, Optional + +from pydantic import AliasGenerator, BaseModel, ConfigDict +from pydantic.alias_generators import to_camel + +from ..account import Account + +ConversationType = Literal["personal", "groupChat"] + + +class Conversation(BaseModel): + """Represents a Teams conversation.""" + + model_config = ConfigDict( + alias_generator=AliasGenerator( + serialization_alias=to_camel, + ), + extra="allow", + ) + + id: str + """ + Conversation ID + """ + + tenant_id: Optional[str] = None + """ + Conversation Tenant ID + """ + + type: ConversationType + """ + The Conversations Type + """ + + name: Optional[str] = None + """ + The Conversations Name + """ + + is_group: Optional[bool] = None + """ + If the Conversation supports multiple participants + """ + + members: Optional[List[Account]] = None + """ + List of members in this conversation + """ diff --git a/packages/api/src/microsoft/teams/api/models/conversation/resource.py b/packages/api/src/microsoft/teams/api/models/conversation/resource.py new file mode 100644 index 00000000..f5ad82ad --- /dev/null +++ b/packages/api/src/microsoft/teams/api/models/conversation/resource.py @@ -0,0 +1,33 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from pydantic import AliasGenerator, BaseModel, ConfigDict +from pydantic.alias_generators import to_camel + + +class ConversationResource(BaseModel): + """A response containing a resource.""" + + model_config = ConfigDict( + alias_generator=AliasGenerator( + serialization_alias=to_camel, + ), + extra="allow", + ) + + id: str + """ + Id of the resource. + """ + + activity_id: str + """ + Id of the Activity (if sent). + """ + + service_url: str + """ + Service endpoint where operations concerning the conversation may be performed. + """ diff --git a/packages/common/src/microsoft/teams/common/py.typed b/packages/common/src/microsoft/teams/common/py.typed deleted file mode 100644 index e69de29b..00000000 diff --git a/pyproject.toml b/pyproject.toml index db0ed9e6..f1fa669f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.12" dependencies = [ "microsoft-teams-common", "microsoft-teams-cards", + "microsoft-teams-api", "pytest-asyncio>=1.0.0", ] diff --git a/src/teams_py/api.py b/src/teams_py/api.py new file mode 100644 index 00000000..881a6f3f --- /dev/null +++ b/src/teams_py/api.py @@ -0,0 +1,150 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import asyncio +import os +from typing import Optional + +from microsoft.teams.api import ( + Account, + Activity, + ConversationClient, + CreateConversationParams, + GetConversationsParams, +) +from microsoft.teams.common.http import ClientOptions + + +class TeamsApiTester: + """Test the Teams API clients.""" + + def __init__(self, service_url: str, token: str) -> None: + """Initialize the tester. + + Args: + service_url: The Teams service URL. + token: The authentication token. + """ + options = ClientOptions( + headers={"Authorization": f"Bearer {token}"}, + ) + self.client = ConversationClient(service_url, options) + + async def test_conversation_client(self, conversation_id: Optional[str] = None) -> None: + """Test the conversation client. + + Args: + conversation_id: Optional conversation ID to use for testing. If not provided, + a new conversation will be created. + """ + print("\nTesting Conversation Client...") + + # Get conversations + print("\nGetting conversations...") + try: + conversations = await self.client.get(GetConversationsParams()) + print(f"Found {len(conversations.conversations)} conversations") + if conversations.conversations: + print("First conversation:") + print(f" ID: {conversations.conversations[0].id}") + print(f" Type: {conversations.conversations[0].type}") + print(f" Is Group: {conversations.conversations[0].is_group}") + except Exception as e: + print(f"Error getting conversations: {e}") + + # Create a conversation if no ID provided + if not conversation_id: + print("\nCreating a new conversation...") + try: + conversation = await self.client.create( + CreateConversationParams( + is_group=True, + members=[ + Account(id="user1", name="User 1"), + Account(id="user2", name="User 2"), + ], + topic_name="Test Conversation", + ) + ) + conversation_id = conversation.id + print(f"Created conversation with ID: {conversation_id}") + except Exception as e: + print(f"Error creating conversation: {e}") + return + + if not conversation_id: + print("No conversation ID available for testing") + return + + # Test activities + print("\nTesting activities...") + try: + activities = self.client.activities(conversation_id) + + # Create an activity + activity = await activities.create(Activity(type="message", text="Hello from Python SDK!")) + print(f"Created activity with ID: {activity.id}") + + # Update the activity + updated = await activities.update( + activity.id, + Activity(type="message", text="Updated message from Python SDK!"), + ) + print(f"Updated activity: {updated.text}") + + # Reply to the activity + reply = await activities.reply( + activity.id, + Activity(type="message", text="Reply from Python SDK!"), + ) + print(f"Replied to activity: {reply.text}") + + # Get members for the activity + activity_members = await activities.get_members(activity.id) + print(f"Activity has {len(activity_members)} members") + + # Delete the activity + await activities.delete(activity.id) + print("Deleted activity") + except Exception as e: + print(f"Error testing activities: {e}") + + # Test members + print("\nTesting members...") + try: + members = self.client.members(conversation_id) + + # Get all members + all_members = await members.get_all() + print(f"Conversation has {len(all_members)} members") + for member in all_members: + print(f" Member: {member.name} (ID: {member.id})") + + # Get a specific member + if all_members: + member = await members.get(all_members[0].id) + print(f"Got member: {member.name} (ID: {member.id})") + except Exception as e: + print(f"Error testing members: {e}") + + +async def main() -> None: + """Run the API tests.""" + # Get configuration from environment + service_url = os.getenv("TEAMS_SERVICE_URL") + token = os.getenv("TEAMS_TOKEN") + conversation_id = os.getenv("TEAMS_CONVERSATION_ID") + + if not service_url or not token: + print("Error: TEAMS_SERVICE_URL and TEAMS_TOKEN environment variables must be set") + return + + # Create tester and run tests + tester = TeamsApiTester(service_url, token) + await tester.test_conversation_client(conversation_id) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/uv.lock b/uv.lock index 371893bc..0304bb9b 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,4 @@ version = 1 -revision = 1 requires-python = ">=3.12" [manifest] @@ -10,6 +9,15 @@ members = [ "microsoft-teams-common", ] +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + [[package]] name = "anyio" version = "4.9.0" @@ -180,6 +188,7 @@ name = "microsoft-teams" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "microsoft-teams-api" }, { name = "microsoft-teams-cards" }, { name = "microsoft-teams-common" }, { name = "pytest-asyncio" }, @@ -195,6 +204,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "microsoft-teams-api", editable = "packages/api" }, { name = "microsoft-teams-cards", editable = "packages/cards" }, { name = "microsoft-teams-common", editable = "packages/common" }, { name = "pytest-asyncio", specifier = ">=1.0.0" }, @@ -212,6 +222,16 @@ dev = [ name = "microsoft-teams-api" version = "0.1.0" source = { editable = "packages/api" } +dependencies = [ + { name = "microsoft-teams-common" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "microsoft-teams-common", editable = "packages/common" }, + { name = "pydantic", specifier = ">=2.0.0" }, +] [[package]] name = "microsoft-teams-cards" @@ -379,6 +399,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 }, ] +[[package]] +name = "pydantic" +version = "2.11.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/8f/9af0f46acc943b8c4592d06523f26a150acf6e6e37e8bd5f0ace925e996d/pydantic-2.11.6.tar.gz", hash = "sha256:12b45cfb4af17e555d3c6283d0b55271865fb0b43cc16dd0d52749dc7abf70e7", size = 787868 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/11/7912a9a194ee4ea96520740d1534bc31a03a4a59d62e1d7cac9461d3f379/pydantic-2.11.6-py3-none-any.whl", hash = "sha256:a24478d2be1b91b6d3bc9597439f69ed5e87f68ebd285d86f7c7932a084b72e7", size = 444718 }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 }, +] + [[package]] name = "pygments" version = "2.19.1" @@ -485,6 +562,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839 }, ] +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, +] + [[package]] name = "virtualenv" version = "20.31.2"