From 32b03d7b1b5af7b65c780fd9175f51fdd5ddb6b1 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 13 Jun 2025 16:58:50 -0700 Subject: [PATCH 01/12] Create member client --- packages/api/pyproject.toml | 8 +- .../api/src/microsoft/teams/api/__init__.py | 11 ++- .../microsoft/teams/api/clients/__init__.py | 11 +++ .../api/clients/conversation/__init__.py | 8 ++ .../teams/api/clients/conversation/member.py | 74 +++++++++++++++ .../microsoft/teams/api/models/__init__.py | 8 ++ .../src/microsoft/teams/api/models/account.py | 26 ++++++ pyproject.toml | 1 + uv.lock | 91 ++++++++++++++++++- 9 files changed, 234 insertions(+), 4 deletions(-) create mode 100644 packages/api/src/microsoft/teams/api/clients/__init__.py create mode 100644 packages/api/src/microsoft/teams/api/clients/conversation/__init__.py create mode 100644 packages/api/src/microsoft/teams/api/clients/conversation/member.py create mode 100644 packages/api/src/microsoft/teams/api/models/__init__.py create mode 100644 packages/api/src/microsoft/teams/api/models/account.py 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/conversation/__init__.py b/packages/api/src/microsoft/teams/api/clients/conversation/__init__.py new file mode 100644 index 00000000..91b41e15 --- /dev/null +++ b/packages/api/src/microsoft/teams/api/clients/conversation/__init__.py @@ -0,0 +1,8 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from .member import ConversationMemberClient + +__all__ = ["ConversationMemberClient"] 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..0c117ea8 --- /dev/null +++ b/packages/api/src/microsoft/teams/api/clients/conversation/member.py @@ -0,0 +1,74 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import List, Optional + +from microsoft.teams.common.http import Client, ClientOptions + +from ...models import Account + + +class ConversationMemberClient: + """ + 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. + """ + self.service_url = service_url + self._http = http_client or Client(ClientOptions()) + + @property + def http(self) -> Client: + """Get the HTTP client.""" + return self._http + + @http.setter + def http(self, client: Client) -> None: + """Set the HTTP client.""" + self._http = client + + 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/models/__init__.py b/packages/api/src/microsoft/teams/api/models/__init__.py new file mode 100644 index 00000000..e5b1999b --- /dev/null +++ b/packages/api/src/microsoft/teams/api/models/__init__.py @@ -0,0 +1,8 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from .account import Account + +__all__ = ["Account"] 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..8169a30f --- /dev/null +++ b/packages/api/src/microsoft/teams/api/models/account.py @@ -0,0 +1,26 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Any, Dict, Optional + +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel + + +class Account(BaseModel): + """ + Represents a Teams account/user. + """ + + model_config = ConfigDict( + alias_generator=to_camel, + extra="allow", + ) + + id: str = Field(..., description="The unique identifier for the account") + aad_object_id: Optional[str] = Field(None, description="The Azure AD object ID") + role: Optional[str] = Field(None, description="The role of the account in the conversation") + name: Optional[str] = Field(None, description="The display name of the account") + properties: Optional[Dict[str, Any]] = Field(None, description="Additional properties for the account") 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/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" From fc4d5144bd5c66681f78450791056b339884a83b Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 13 Jun 2025 17:17:27 -0700 Subject: [PATCH 02/12] Create activity client --- .../api/clients/conversation/__init__.py | 3 +- .../api/clients/conversation/activity.py | 117 +++++++++++++ .../microsoft/teams/api/models/__init__.py | 3 +- .../microsoft/teams/api/models/activity.py | 25 +++ src/teams_py/api.py | 162 ++++++++++++++++++ 5 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 packages/api/src/microsoft/teams/api/clients/conversation/activity.py create mode 100644 packages/api/src/microsoft/teams/api/models/activity.py create mode 100644 src/teams_py/api.py diff --git a/packages/api/src/microsoft/teams/api/clients/conversation/__init__.py b/packages/api/src/microsoft/teams/api/clients/conversation/__init__.py index 91b41e15..588939c1 100644 --- a/packages/api/src/microsoft/teams/api/clients/conversation/__init__.py +++ b/packages/api/src/microsoft/teams/api/clients/conversation/__init__.py @@ -3,6 +3,7 @@ Licensed under the MIT License. """ +from .activity import ConversationActivityClient from .member import ConversationMemberClient -__all__ = ["ConversationMemberClient"] +__all__ = ["ConversationActivityClient", "ConversationMemberClient"] 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..272178b4 --- /dev/null +++ b/packages/api/src/microsoft/teams/api/clients/conversation/activity.py @@ -0,0 +1,117 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import List, Optional + +from microsoft.teams.common.http import Client, ClientOptions + +from ...models import Account, Activity + + +class ConversationActivityClient: + """ + 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. + """ + self.service_url = service_url + self._http = http_client or Client(ClientOptions()) + + @property + def http(self) -> Client: + """Get the HTTP client.""" + return self._http + + @http.setter + def http(self, client: Client) -> None: + """Set the HTTP client.""" + self._http = client + + 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/models/__init__.py b/packages/api/src/microsoft/teams/api/models/__init__.py index e5b1999b..59b5b0ef 100644 --- a/packages/api/src/microsoft/teams/api/models/__init__.py +++ b/packages/api/src/microsoft/teams/api/models/__init__.py @@ -4,5 +4,6 @@ """ from .account import Account +from .activity import Activity -__all__ = ["Account"] +__all__ = ["Account", "Activity"] 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..855ffc75 --- /dev/null +++ b/packages/api/src/microsoft/teams/api/models/activity.py @@ -0,0 +1,25 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Any, Dict, Optional + +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel + + +class Activity(BaseModel): + """ + Represents a Teams activity/message in a conversation. + """ + + model_config = ConfigDict( + alias_generator=to_camel, + extra="allow", + ) + + type: str = Field("message", description="The type of activity (e.g. 'message')") + text: Optional[str] = Field(None, description="The text content of the activity") + reply_to_id: Optional[str] = Field(None, description="The ID of the activity this is replying to") + properties: Optional[Dict[str, Any]] = Field(None, description="Additional properties for the activity") diff --git a/src/teams_py/api.py b/src/teams_py/api.py new file mode 100644 index 00000000..38b57b95 --- /dev/null +++ b/src/teams_py/api.py @@ -0,0 +1,162 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import argparse +import asyncio +import os + +from microsoft.teams.api import ConversationActivityClient, ConversationMemberClient +from microsoft.teams.api.models import Activity +from microsoft.teams.common.http import Client, ClientOptions + + +class TeamsApiTester: + """Helper class to test Teams API clients.""" + + def __init__(self, service_url: str, http_client: Client): + """Initialize the tester with service URL and HTTP client.""" + self.service_url = service_url + self.http_client = http_client + self.member_client = ConversationMemberClient(service_url, http_client) + self.activity_client = ConversationActivityClient(service_url, http_client) + + async def test_member_client(self, conversation_id: str) -> None: + """Test the member client functionality.""" + print("\n=== Testing Member Client ===") + + try: + # Test getting all members + print("\nGetting all members...") + members = await self.member_client.get(conversation_id) + print(f"Found {len(members)} members:") + for member in members: + print(f"- {member.name} (ID: {member.id}, Role: {member.role})") + if member.aad_object_id: + print(f" AAD Object ID: {member.aad_object_id}") + if member.properties: + print(f" Properties: {member.properties}") + + # Test getting a specific member if we have any + if members: + member_id = members[0].id + print(f"\nGetting member {member_id}...") + member = await self.member_client.get_by_id(conversation_id, member_id) + print(f"Found member: {member.name} (ID: {member.id}, Role: {member.role})") + + except Exception as e: + print(f"Error testing member client: {e}") + raise + + async def test_activity_client(self, conversation_id: str) -> None: + """Test the activity client functionality.""" + print("\n=== Testing Activity Client ===") + + try: + # Test creating a new activity + print("\nCreating a new activity...") + activity = Activity( + type="message", + text="Hello from Teams Python SDK!", + properties={"test": True}, + ) + created_activity = await self.activity_client.create(conversation_id, activity) + print(f"Created activity: {created_activity.text} (ID: {created_activity.id})") + + # Test updating the activity + print("\nUpdating the activity...") + created_activity.text = "Updated message from Teams Python SDK!" + updated_activity = await self.activity_client.update(conversation_id, created_activity.id, created_activity) + print(f"Updated activity: {updated_activity.text}") + + # Test replying to the activity + print("\nReplying to the activity...") + reply = Activity( + type="message", + text="This is a reply from Teams Python SDK!", + ) + reply_activity = await self.activity_client.reply(conversation_id, created_activity.id, reply) + print(f"Created reply: {reply_activity.text}") + + # Test getting members for the activity + print("\nGetting members for the activity...") + activity_members = await self.activity_client.get_members(conversation_id, created_activity.id) + print(f"Found {len(activity_members)} members for the activity:") + for member in activity_members: + print(f"- {member.name} (ID: {member.id})") + + # Test deleting the activity + print("\nDeleting the activity...") + await self.activity_client.delete(conversation_id, created_activity.id) + print("Activity deleted successfully") + + except Exception as e: + print(f"Error testing activity client: {e}") + raise + + +def parse_args() -> argparse.Namespace: + """Parse command line arguments.""" + parser = argparse.ArgumentParser( + description="Test Teams API clients with provided credentials and conversation ID." + ) + parser.add_argument( + "--token", + required=True, + help="Teams API token (can also be set via TEAMS_TOKEN environment variable)", + ) + parser.add_argument( + "--conversation-id", + required=True, + help="Conversation ID to test with (can also be set via TEAMS_CONVERSATION_ID environment variable)", + ) + parser.add_argument( + "--service-url", + help="Teams service URL (can also be set via TEAMS_SERVICE_URL environment variable)", + default="https://smba.trafficmanager.net/amer/3abdf8e8-c644-4510-9b59-2557b14ed67f", + ) + return parser.parse_args() + + +async def main() -> None: + """Main entry point for testing the Teams API clients.""" + # Parse command line arguments + args = parse_args() + + # Get configuration from args or environment variables + service_url: str = args.service_url or os.getenv("TEAMS_SERVICE_URL", args.service_url) + token: str = args.token or os.getenv("TEAMS_TOKEN") + conversation_id: str = args.conversation_id or os.getenv("TEAMS_CONVERSATION_ID") + + # Validate required parameters + if not token: + raise ValueError("Teams API token is required. Provide it via --token or TEAMS_TOKEN environment variable.") + if not conversation_id: + raise ValueError( + "Conversation ID is required. Provide it via --conversation-id or TEAMS_CONVERSATION_ID env variable." + ) + + # Create HTTP client with options + http_client = Client( + ClientOptions( + headers={ + "User-Agent": "TeamsPythonSDK/0.1.0", + }, + token=token, + ) + ) + + # Create tester and run tests + tester = TeamsApiTester(service_url, http_client) + try: + await tester.test_member_client(conversation_id) + await tester.test_activity_client(conversation_id) + print("\n=== All tests completed successfully! ===") + except Exception as e: + print(f"\n=== Test failed: {e} ===") + raise + + +if __name__ == "__main__": + asyncio.run(main()) From 45fc4d3e5950785bb63c5cd8c080ff56a113ed47 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 13 Jun 2025 19:17:35 -0700 Subject: [PATCH 03/12] Create conversation client --- .../api/clients/conversation/__init__.py | 11 +- .../teams/api/clients/conversation/client.py | 155 +++++++++++ .../teams/api/clients/conversation/params.py | 51 ++++ .../microsoft/teams/api/models/__init__.py | 8 +- .../teams/api/models/conversation/__init__.py | 9 + .../api/models/conversation/conversation.py | 26 ++ .../teams/api/models/conversation/resource.py | 20 ++ .../microsoft/teams/api/models/resource.py | 14 + src/teams_py/api.py | 244 +++++++++--------- 9 files changed, 408 insertions(+), 130 deletions(-) create mode 100644 packages/api/src/microsoft/teams/api/clients/conversation/client.py create mode 100644 packages/api/src/microsoft/teams/api/clients/conversation/params.py create mode 100644 packages/api/src/microsoft/teams/api/models/conversation/__init__.py create mode 100644 packages/api/src/microsoft/teams/api/models/conversation/conversation.py create mode 100644 packages/api/src/microsoft/teams/api/models/conversation/resource.py create mode 100644 packages/api/src/microsoft/teams/api/models/resource.py diff --git a/packages/api/src/microsoft/teams/api/clients/conversation/__init__.py b/packages/api/src/microsoft/teams/api/clients/conversation/__init__.py index 588939c1..dc421bc9 100644 --- a/packages/api/src/microsoft/teams/api/clients/conversation/__init__.py +++ b/packages/api/src/microsoft/teams/api/clients/conversation/__init__.py @@ -4,6 +4,15 @@ """ from .activity import ConversationActivityClient +from .client import ConversationClient from .member import ConversationMemberClient +from .params import CreateConversationParams, GetConversationsParams, GetConversationsResponse -__all__ = ["ConversationActivityClient", "ConversationMemberClient"] +__all__ = [ + "ConversationActivityClient", + "ConversationClient", + "ConversationMemberClient", + "CreateConversationParams", + "GetConversationsParams", + "GetConversationsResponse", +] 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..9760ed5c --- /dev/null +++ b/packages/api/src/microsoft/teams/api/clients/conversation/client.py @@ -0,0 +1,155 @@ +""" +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 .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: + """Client for managing Teams conversations.""" + + def __init__(self, service_url: str, options: 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. + """ + self.service_url = service_url + + if isinstance(options, Client): + self._http = options + else: + self._http = Client(options or ClientOptions()) + + self._activities = ConversationActivityClient(service_url, self._http) + self._members = ConversationMemberClient(service_url, self._http) + + @property + def http(self) -> Client: + """Get the HTTP client. + + Returns: + The HTTP client instance. + """ + return self._http + + @http.setter + def http(self, client: Client) -> None: + """Set the HTTP client. + + Args: + client: The HTTP client to use. + """ + self._http = client + # Update sub-clients with new HTTP client + self._activities = ConversationActivityClient(self.service_url, client) + self._members = ConversationMemberClient(self.service_url, client) + + 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/params.py b/packages/api/src/microsoft/teams/api/clients/conversation/params.py new file mode 100644 index 00000000..8b84e256 --- /dev/null +++ b/packages/api/src/microsoft/teams/api/clients/conversation/params.py @@ -0,0 +1,51 @@ +""" +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] = Field(None, description="Token for pagination") + + +class CreateConversationParams(BaseModel): + """Parameters for creating a conversation.""" + + model_config = ConfigDict( + alias_generator=to_camel, + extra="allow", + ) + + is_group: bool = Field(False, description="Whether this is a group conversation") + bot: Optional[Account] = Field(None, description="Bot account to add to the conversation") + members: Optional[List[Account]] = Field(None, description="Members to add to the conversation") + topic_name: Optional[str] = Field(None, description="Topic name for the conversation") + tenant_id: Optional[str] = Field(None, description="Tenant ID for the conversation") + activity: Optional[Activity] = Field(None, description="Initial activity to post in the conversation") + channel_data: Optional[Dict[str, Any]] = Field(None, description="Channel-specific data") + + +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 index 59b5b0ef..b64940d5 100644 --- a/packages/api/src/microsoft/teams/api/models/__init__.py +++ b/packages/api/src/microsoft/teams/api/models/__init__.py @@ -5,5 +5,11 @@ from .account import Account from .activity import Activity +from .conversation import Conversation, ConversationResource -__all__ = ["Account", "Activity"] +__all__ = [ + "Account", + "Activity", + "Conversation", + "ConversationResource", +] 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..27b3017c --- /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 +from .resource import ConversationResource + +__all__ = ["Conversation", "ConversationResource"] 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..9776fec5 --- /dev/null +++ b/packages/api/src/microsoft/teams/api/models/conversation/conversation.py @@ -0,0 +1,26 @@ +""" +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 ..account import Account + + +class Conversation(BaseModel): + """Represents a Teams conversation.""" + + model_config = ConfigDict( + alias_generator=to_camel, + extra="allow", + ) + + id: str = Field(..., description="The unique identifier for the conversation") + type: str = Field(..., description="The type of conversation (e.g., 'channel', 'chat')") + is_group: bool = Field(False, description="Whether this is a group conversation") + members: Optional[List[Account]] = Field(None, description="The members of the conversation") + properties: Optional[Dict[str, Any]] = Field(None, description="Additional properties for the 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..7dc6025f --- /dev/null +++ b/packages/api/src/microsoft/teams/api/models/conversation/resource.py @@ -0,0 +1,20 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel + + +class ConversationResource(BaseModel): + """Resource returned when creating a conversation.""" + + model_config = ConfigDict( + alias_generator=to_camel, + extra="allow", + ) + + id: str = Field(..., description="The ID of the created conversation") + activity_id: str = Field(..., description="The ID of the activity that created the conversation") + service_url: str = Field(..., description="The service URL for the conversation") diff --git a/packages/api/src/microsoft/teams/api/models/resource.py b/packages/api/src/microsoft/teams/api/models/resource.py new file mode 100644 index 00000000..8979e033 --- /dev/null +++ b/packages/api/src/microsoft/teams/api/models/resource.py @@ -0,0 +1,14 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from pydantic import BaseModel + + +class ConversationResource(BaseModel): + """A response containing a resource.""" + + id: str + activity_id: str + service_url: str diff --git a/src/teams_py/api.py b/src/teams_py/api.py index 38b57b95..881a6f3f 100644 --- a/src/teams_py/api.py +++ b/src/teams_py/api.py @@ -3,159 +3,147 @@ Licensed under the MIT License. """ -import argparse import asyncio import os +from typing import Optional -from microsoft.teams.api import ConversationActivityClient, ConversationMemberClient -from microsoft.teams.api.models import Activity -from microsoft.teams.common.http import Client, ClientOptions +from microsoft.teams.api import ( + Account, + Activity, + ConversationClient, + CreateConversationParams, + GetConversationsParams, +) +from microsoft.teams.common.http import ClientOptions class TeamsApiTester: - """Helper class to test Teams API clients.""" + """Test the Teams API clients.""" - def __init__(self, service_url: str, http_client: Client): - """Initialize the tester with service URL and HTTP client.""" - self.service_url = service_url - self.http_client = http_client - self.member_client = ConversationMemberClient(service_url, http_client) - self.activity_client = ConversationActivityClient(service_url, http_client) + def __init__(self, service_url: str, token: str) -> None: + """Initialize the tester. - async def test_member_client(self, conversation_id: str) -> None: - """Test the member client functionality.""" - print("\n=== Testing Member Client ===") + Args: + service_url: The Teams service URL. + token: The authentication token. + """ + options = ClientOptions( + headers={"Authorization": f"Bearer {token}"}, + ) + self.client = ConversationClient(service_url, options) - try: - # Test getting all members - print("\nGetting all members...") - members = await self.member_client.get(conversation_id) - print(f"Found {len(members)} members:") - for member in members: - print(f"- {member.name} (ID: {member.id}, Role: {member.role})") - if member.aad_object_id: - print(f" AAD Object ID: {member.aad_object_id}") - if member.properties: - print(f" Properties: {member.properties}") - - # Test getting a specific member if we have any - if members: - member_id = members[0].id - print(f"\nGetting member {member_id}...") - member = await self.member_client.get_by_id(conversation_id, member_id) - print(f"Found member: {member.name} (ID: {member.id}, Role: {member.role})") + 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 testing member client: {e}") - raise + 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) - async def test_activity_client(self, conversation_id: str) -> None: - """Test the activity client functionality.""" - print("\n=== Testing Activity Client ===") + # Create an activity + activity = await activities.create(Activity(type="message", text="Hello from Python SDK!")) + print(f"Created activity with ID: {activity.id}") - try: - # Test creating a new activity - print("\nCreating a new activity...") - activity = Activity( - type="message", - text="Hello from Teams Python SDK!", - properties={"test": True}, + # Update the activity + updated = await activities.update( + activity.id, + Activity(type="message", text="Updated message from Python SDK!"), ) - created_activity = await self.activity_client.create(conversation_id, activity) - print(f"Created activity: {created_activity.text} (ID: {created_activity.id})") - - # Test updating the activity - print("\nUpdating the activity...") - created_activity.text = "Updated message from Teams Python SDK!" - updated_activity = await self.activity_client.update(conversation_id, created_activity.id, created_activity) - print(f"Updated activity: {updated_activity.text}") - - # Test replying to the activity - print("\nReplying to the activity...") - reply = Activity( - type="message", - text="This is a reply from Teams 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!"), ) - reply_activity = await self.activity_client.reply(conversation_id, created_activity.id, reply) - print(f"Created reply: {reply_activity.text}") + print(f"Replied to activity: {reply.text}") - # Test getting members for the activity - print("\nGetting members for the activity...") - activity_members = await self.activity_client.get_members(conversation_id, created_activity.id) - print(f"Found {len(activity_members)} members for the activity:") - for member in activity_members: - print(f"- {member.name} (ID: {member.id})") + # Get members for the activity + activity_members = await activities.get_members(activity.id) + print(f"Activity has {len(activity_members)} members") - # Test deleting the activity - print("\nDeleting the activity...") - await self.activity_client.delete(conversation_id, created_activity.id) - print("Activity deleted successfully") + # 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 activity client: {e}") - raise - - -def parse_args() -> argparse.Namespace: - """Parse command line arguments.""" - parser = argparse.ArgumentParser( - description="Test Teams API clients with provided credentials and conversation ID." - ) - parser.add_argument( - "--token", - required=True, - help="Teams API token (can also be set via TEAMS_TOKEN environment variable)", - ) - parser.add_argument( - "--conversation-id", - required=True, - help="Conversation ID to test with (can also be set via TEAMS_CONVERSATION_ID environment variable)", - ) - parser.add_argument( - "--service-url", - help="Teams service URL (can also be set via TEAMS_SERVICE_URL environment variable)", - default="https://smba.trafficmanager.net/amer/3abdf8e8-c644-4510-9b59-2557b14ed67f", - ) - return parser.parse_args() + print(f"Error testing members: {e}") async def main() -> None: - """Main entry point for testing the Teams API clients.""" - # Parse command line arguments - args = parse_args() - - # Get configuration from args or environment variables - service_url: str = args.service_url or os.getenv("TEAMS_SERVICE_URL", args.service_url) - token: str = args.token or os.getenv("TEAMS_TOKEN") - conversation_id: str = args.conversation_id or os.getenv("TEAMS_CONVERSATION_ID") - - # Validate required parameters - if not token: - raise ValueError("Teams API token is required. Provide it via --token or TEAMS_TOKEN environment variable.") - if not conversation_id: - raise ValueError( - "Conversation ID is required. Provide it via --conversation-id or TEAMS_CONVERSATION_ID env variable." - ) + """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") - # Create HTTP client with options - http_client = Client( - ClientOptions( - headers={ - "User-Agent": "TeamsPythonSDK/0.1.0", - }, - token=token, - ) - ) + 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, http_client) - try: - await tester.test_member_client(conversation_id) - await tester.test_activity_client(conversation_id) - print("\n=== All tests completed successfully! ===") - except Exception as e: - print(f"\n=== Test failed: {e} ===") - raise + tester = TeamsApiTester(service_url, token) + await tester.test_conversation_client(conversation_id) if __name__ == "__main__": From c4147447b5b49278d849de17c6396d5fc8b21623 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Sat, 14 Jun 2025 11:20:58 -0700 Subject: [PATCH 04/12] create mypy --- mypy.ini | 1 + .../src/microsoft/teams/api/clients/conversation/client.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) 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/src/microsoft/teams/api/clients/conversation/client.py b/packages/api/src/microsoft/teams/api/clients/conversation/client.py index 9760ed5c..eaeca778 100644 --- a/packages/api/src/microsoft/teams/api/clients/conversation/client.py +++ b/packages/api/src/microsoft/teams/api/clients/conversation/client.py @@ -60,7 +60,7 @@ async def delete(self, member_id: str) -> None: class ConversationClient: """Client for managing Teams conversations.""" - def __init__(self, service_url: str, options: Union[Client, ClientOptions] = None) -> None: + def __init__(self, service_url: str, options: Optional[Union[Client, ClientOptions]] = None) -> None: """Initialize the client. Args: @@ -69,7 +69,9 @@ def __init__(self, service_url: str, options: Union[Client, ClientOptions] = Non """ self.service_url = service_url - if isinstance(options, Client): + if options is None: + self._http = Client(ClientOptions()) + elif isinstance(options, Client): self._http = options else: self._http = Client(options or ClientOptions()) From 59bf2630b59e7a7b053b6d1a821a62f543ce892e Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Sat, 14 Jun 2025 11:26:09 -0700 Subject: [PATCH 05/12] Fix m --- packages/common/src/microsoft/teams/common/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/common/src/microsoft/teams/common/py.typed 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 From cde6b6c8458482030af9ea2511490fa33b7812e2 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Sat, 14 Jun 2025 21:09:53 -0700 Subject: [PATCH 06/12] update --- .../src/microsoft/teams/api/models/account.py | 6 +++-- .../microsoft/teams/api/models/activity.py | 11 +++++----- .../api/models/conversation/conversation.py | 6 +++-- .../teams/api/models/conversation/resource.py | 22 ++++++++++++++----- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/packages/api/src/microsoft/teams/api/models/account.py b/packages/api/src/microsoft/teams/api/models/account.py index 8169a30f..0d942f75 100644 --- a/packages/api/src/microsoft/teams/api/models/account.py +++ b/packages/api/src/microsoft/teams/api/models/account.py @@ -5,7 +5,7 @@ from typing import Any, Dict, Optional -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasGenerator, BaseModel, ConfigDict, Field from pydantic.alias_generators import to_camel @@ -15,7 +15,9 @@ class Account(BaseModel): """ model_config = ConfigDict( - alias_generator=to_camel, + alias_generator=AliasGenerator( + serialization_alias=to_camel, + ), extra="allow", ) diff --git a/packages/api/src/microsoft/teams/api/models/activity.py b/packages/api/src/microsoft/teams/api/models/activity.py index 855ffc75..e6607636 100644 --- a/packages/api/src/microsoft/teams/api/models/activity.py +++ b/packages/api/src/microsoft/teams/api/models/activity.py @@ -5,17 +5,18 @@ from typing import Any, Dict, Optional -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasGenerator, BaseModel, ConfigDict, Field from pydantic.alias_generators import to_camel +# TODO: This is a barebones model for now. class Activity(BaseModel): - """ - Represents a Teams activity/message in a conversation. - """ + """Represents a Teams activity.""" model_config = ConfigDict( - alias_generator=to_camel, + alias_generator=AliasGenerator( + serialization_alias=to_camel, + ), extra="allow", ) diff --git a/packages/api/src/microsoft/teams/api/models/conversation/conversation.py b/packages/api/src/microsoft/teams/api/models/conversation/conversation.py index 9776fec5..fb6af064 100644 --- a/packages/api/src/microsoft/teams/api/models/conversation/conversation.py +++ b/packages/api/src/microsoft/teams/api/models/conversation/conversation.py @@ -5,7 +5,7 @@ from typing import Any, Dict, List, Optional -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasGenerator, BaseModel, ConfigDict, Field from pydantic.alias_generators import to_camel from ..account import Account @@ -15,7 +15,9 @@ class Conversation(BaseModel): """Represents a Teams conversation.""" model_config = ConfigDict( - alias_generator=to_camel, + alias_generator=AliasGenerator( + serialization_alias=to_camel, + ), extra="allow", ) diff --git a/packages/api/src/microsoft/teams/api/models/conversation/resource.py b/packages/api/src/microsoft/teams/api/models/conversation/resource.py index 7dc6025f..419e9d14 100644 --- a/packages/api/src/microsoft/teams/api/models/conversation/resource.py +++ b/packages/api/src/microsoft/teams/api/models/conversation/resource.py @@ -3,18 +3,28 @@ Licensed under the MIT License. """ -from pydantic import BaseModel, ConfigDict, Field +from pydantic import AliasGenerator, BaseModel, ConfigDict, Field from pydantic.alias_generators import to_camel class ConversationResource(BaseModel): - """Resource returned when creating a conversation.""" + """A response containing a resource.""" model_config = ConfigDict( - alias_generator=to_camel, + alias_generator=AliasGenerator( + serialization_alias=to_camel, + ), extra="allow", ) - id: str = Field(..., description="The ID of the created conversation") - activity_id: str = Field(..., description="The ID of the activity that created the conversation") - service_url: str = Field(..., description="The service URL for the conversation") + id: str = Field( + description="Id of the resource", + ) + + activity_id: str = Field( + description="ID of the Activity (if sent)", + ) + + service_url: str = Field( + description="Service endpoint where operations concerning the conversation may be performed", + ) From 3c4b4ac1c02d7acb352119ce5c5661b10f574872 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Sat, 14 Jun 2025 21:17:41 -0700 Subject: [PATCH 07/12] BaseClient --- .../teams/api/clients/base_client.py | 30 ++++++++++++++ .../api/clients/conversation/activity.py | 27 +++++-------- .../teams/api/clients/conversation/client.py | 40 ++++--------------- .../teams/api/clients/conversation/member.py | 23 ++++------- 4 files changed, 53 insertions(+), 67 deletions(-) create mode 100644 packages/api/src/microsoft/teams/api/clients/base_client.py 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/activity.py b/packages/api/src/microsoft/teams/api/clients/conversation/activity.py index 272178b4..ce2d470a 100644 --- a/packages/api/src/microsoft/teams/api/clients/conversation/activity.py +++ b/packages/api/src/microsoft/teams/api/clients/conversation/activity.py @@ -5,12 +5,13 @@ from typing import List, Optional -from microsoft.teams.common.http import Client, ClientOptions +from microsoft.teams.common.http import Client from ...models import Account, Activity +from ..base_client import BaseClient -class ConversationActivityClient: +class ConversationActivityClient(BaseClient): """ Client for managing activities in a Teams conversation. """ @@ -23,18 +24,8 @@ def __init__(self, service_url: str, http_client: Optional[Client] = None): 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 - self._http = http_client or Client(ClientOptions()) - - @property - def http(self) -> Client: - """Get the HTTP client.""" - return self._http - - @http.setter - def http(self, client: Client) -> None: - """Set the HTTP client.""" - self._http = client async def create(self, conversation_id: str, activity: Activity) -> Activity: """ @@ -47,7 +38,7 @@ async def create(self, conversation_id: str, activity: Activity) -> Activity: Returns: The created activity """ - response = await self._http.post( + response = await self.http.post( f"{self.service_url}/v3/conversations/{conversation_id}/activities", json=activity.model_dump(by_alias=True), ) @@ -65,7 +56,7 @@ async def update(self, conversation_id: str, activity_id: str, activity: Activit Returns: The updated activity """ - response = await self._http.put( + response = await self.http.put( f"{self.service_url}/v3/conversations/{conversation_id}/activities/{activity_id}", json=activity.model_dump(by_alias=True), ) @@ -84,7 +75,7 @@ async def reply(self, conversation_id: str, activity_id: str, activity: Activity The created reply activity """ activity.reply_to_id = activity_id - response = await self._http.post( + response = await self.http.post( f"{self.service_url}/v3/conversations/{conversation_id}/activities/{activity_id}", json=activity.model_dump(by_alias=True), ) @@ -98,7 +89,7 @@ async def delete(self, conversation_id: str, activity_id: str) -> None: 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}") + 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]: """ @@ -111,7 +102,7 @@ async def get_members(self, conversation_id: str, activity_id: str) -> List[Acco Returns: List of Account objects representing the activity members """ - response = await self._http.get( + 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 index eaeca778..943b3a7f 100644 --- a/packages/api/src/microsoft/teams/api/clients/conversation/client.py +++ b/packages/api/src/microsoft/teams/api/clients/conversation/client.py @@ -8,6 +8,7 @@ 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 ( @@ -57,7 +58,7 @@ async def delete(self, member_id: str) -> None: await self._client._members.delete(self._conversation_id, member_id) -class ConversationClient: +class ConversationClient(BaseClient): """Client for managing Teams conversations.""" def __init__(self, service_url: str, options: Optional[Union[Client, ClientOptions]] = None) -> None: @@ -67,38 +68,11 @@ def __init__(self, service_url: str, options: Optional[Union[Client, ClientOptio 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 - if options is None: - self._http = Client(ClientOptions()) - elif isinstance(options, Client): - self._http = options - else: - self._http = Client(options or ClientOptions()) - - self._activities = ConversationActivityClient(service_url, self._http) - self._members = ConversationMemberClient(service_url, self._http) - - @property - def http(self) -> Client: - """Get the HTTP client. - - Returns: - The HTTP client instance. - """ - return self._http - - @http.setter - def http(self, client: Client) -> None: - """Set the HTTP client. - - Args: - client: The HTTP client to use. - """ - self._http = client - # Update sub-clients with new HTTP client - self._activities = ConversationActivityClient(self.service_url, client) - self._members = ConversationMemberClient(self.service_url, client) + 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. @@ -135,7 +109,7 @@ async def get(self, params: Optional[GetConversationsParams] = None) -> GetConve if params and params.continuation_token: query_params["continuationToken"] = params.continuation_token - response = await self._http.get( + response = await self.http.get( f"{self.service_url}/v3/conversations", params=query_params, ) @@ -150,7 +124,7 @@ async def create(self, params: CreateConversationParams) -> ConversationResource Returns: The created conversation resource. """ - response = await self._http.post( + response = await self.http.post( f"{self.service_url}/v3/conversations", json=params.model_dump(by_alias=True), ) diff --git a/packages/api/src/microsoft/teams/api/clients/conversation/member.py b/packages/api/src/microsoft/teams/api/clients/conversation/member.py index 0c117ea8..e85c7548 100644 --- a/packages/api/src/microsoft/teams/api/clients/conversation/member.py +++ b/packages/api/src/microsoft/teams/api/clients/conversation/member.py @@ -5,12 +5,13 @@ from typing import List, Optional -from microsoft.teams.common.http import Client, ClientOptions +from microsoft.teams.common.http import Client from ...models import Account +from ..base_client import BaseClient -class ConversationMemberClient: +class ConversationMemberClient(BaseClient): """ Client for managing members in a Teams conversation. """ @@ -23,18 +24,8 @@ def __init__(self, service_url: str, http_client: Optional[Client] = None): 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 - self._http = http_client or Client(ClientOptions()) - - @property - def http(self) -> Client: - """Get the HTTP client.""" - return self._http - - @http.setter - def http(self, client: Client) -> None: - """Set the HTTP client.""" - self._http = client async def get(self, conversation_id: str) -> List[Account]: """ @@ -46,7 +37,7 @@ async def get(self, conversation_id: str) -> List[Account]: Returns: List of Account objects representing the conversation members """ - response = await self._http.get(f"{self.service_url}/v3/conversations/{conversation_id}/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: @@ -60,7 +51,7 @@ async def get_by_id(self, conversation_id: str, member_id: str) -> Account: Returns: Account object representing the conversation member """ - response = await self._http.get(f"{self.service_url}/v3/conversations/{conversation_id}/members/{member_id}") + 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: @@ -71,4 +62,4 @@ async def delete(self, conversation_id: str, member_id: str) -> None: 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}") + await self.http.delete(f"{self.service_url}/v3/conversations/{conversation_id}/members/{member_id}") From 1a4351003ba393f2d5c689d67cc09ca37bb83985 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Sat, 14 Jun 2025 23:26:51 -0700 Subject: [PATCH 08/12] model init --- packages/api/src/microsoft/teams/api/models/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/api/src/microsoft/teams/api/models/__init__.py b/packages/api/src/microsoft/teams/api/models/__init__.py index b64940d5..0dae4715 100644 --- a/packages/api/src/microsoft/teams/api/models/__init__.py +++ b/packages/api/src/microsoft/teams/api/models/__init__.py @@ -5,11 +5,11 @@ from .account import Account from .activity import Activity -from .conversation import Conversation, ConversationResource +from .conversation import * # noqa: F403 +from .conversation import __all__ as conversation_all __all__ = [ "Account", "Activity", - "Conversation", - "ConversationResource", + *conversation_all, ] From 239fdc8050d87272426f975b428b3c5ad7a6ba3e Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Sun, 15 Jun 2025 00:19:09 -0700 Subject: [PATCH 09/12] model init --- .../teams/api/clients/conversation/params.py | 37 +++++++++++++++---- .../src/microsoft/teams/api/models/account.py | 23 +++++++++--- .../microsoft/teams/api/models/activity.py | 15 ++++++-- .../api/models/conversation/conversation.py | 23 ++++++++---- 4 files changed, 74 insertions(+), 24 deletions(-) diff --git a/packages/api/src/microsoft/teams/api/clients/conversation/params.py b/packages/api/src/microsoft/teams/api/clients/conversation/params.py index 8b84e256..6e1087f6 100644 --- a/packages/api/src/microsoft/teams/api/clients/conversation/params.py +++ b/packages/api/src/microsoft/teams/api/clients/conversation/params.py @@ -19,7 +19,7 @@ class GetConversationsParams(BaseModel): extra="allow", ) - continuation_token: Optional[str] = Field(None, description="Token for pagination") + continuation_token: Optional[str] = None class CreateConversationParams(BaseModel): @@ -30,13 +30,34 @@ class CreateConversationParams(BaseModel): extra="allow", ) - is_group: bool = Field(False, description="Whether this is a group conversation") - bot: Optional[Account] = Field(None, description="Bot account to add to the conversation") - members: Optional[List[Account]] = Field(None, description="Members to add to the conversation") - topic_name: Optional[str] = Field(None, description="Topic name for the conversation") - tenant_id: Optional[str] = Field(None, description="Tenant ID for the conversation") - activity: Optional[Activity] = Field(None, description="Initial activity to post in the conversation") - channel_data: Optional[Dict[str, Any]] = Field(None, description="Channel-specific data") + 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): diff --git a/packages/api/src/microsoft/teams/api/models/account.py b/packages/api/src/microsoft/teams/api/models/account.py index 0d942f75..fc613918 100644 --- a/packages/api/src/microsoft/teams/api/models/account.py +++ b/packages/api/src/microsoft/teams/api/models/account.py @@ -5,7 +5,7 @@ from typing import Any, Dict, Optional -from pydantic import AliasGenerator, BaseModel, ConfigDict, Field +from pydantic import AliasGenerator, BaseModel, ConfigDict from pydantic.alias_generators import to_camel @@ -21,8 +21,19 @@ class Account(BaseModel): extra="allow", ) - id: str = Field(..., description="The unique identifier for the account") - aad_object_id: Optional[str] = Field(None, description="The Azure AD object ID") - role: Optional[str] = Field(None, description="The role of the account in the conversation") - name: Optional[str] = Field(None, description="The display name of the account") - properties: Optional[Dict[str, Any]] = Field(None, description="Additional properties for the account") + id: str + """ + The unique identifier for the account. + """ + aad_object_id: Optional[str] = None + """ + The Azure AD object ID. + """ + role: Optional[str] = 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 index e6607636..729f7657 100644 --- a/packages/api/src/microsoft/teams/api/models/activity.py +++ b/packages/api/src/microsoft/teams/api/models/activity.py @@ -21,6 +21,15 @@ class Activity(BaseModel): ) type: str = Field("message", description="The type of activity (e.g. 'message')") - text: Optional[str] = Field(None, description="The text content of the activity") - reply_to_id: Optional[str] = Field(None, description="The ID of the activity this is replying to") - properties: Optional[Dict[str, Any]] = Field(None, description="Additional properties for the activity") + """ + 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/conversation.py b/packages/api/src/microsoft/teams/api/models/conversation/conversation.py index fb6af064..c85c7d2a 100644 --- a/packages/api/src/microsoft/teams/api/models/conversation/conversation.py +++ b/packages/api/src/microsoft/teams/api/models/conversation/conversation.py @@ -3,13 +3,11 @@ Licensed under the MIT License. """ -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Optional from pydantic import AliasGenerator, BaseModel, ConfigDict, Field from pydantic.alias_generators import to_camel -from ..account import Account - class Conversation(BaseModel): """Represents a Teams conversation.""" @@ -22,7 +20,18 @@ class Conversation(BaseModel): ) id: str = Field(..., description="The unique identifier for the conversation") - type: str = Field(..., description="The type of conversation (e.g., 'channel', 'chat')") - is_group: bool = Field(False, description="Whether this is a group conversation") - members: Optional[List[Account]] = Field(None, description="The members of the conversation") - properties: Optional[Dict[str, Any]] = Field(None, description="Additional properties for the conversation") + """ + The unique identifier for the conversation. + """ + type: str + """ + The type of conversation (e.g., 'channel', 'chat'). + """ + is_group: bool + """ + Whether this is a group conversation. + """ + properties: Optional[Dict[str, Any]] = None + """ + Additional properties for the conversation. + """ From 7e7abcf594d56c4d5c4f5be0aa36ccc29b5f7313 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Sun, 15 Jun 2025 00:28:34 -0700 Subject: [PATCH 10/12] model init --- .../microsoft/teams/api/models/activity.py | 4 ++-- .../api/models/conversation/conversation.py | 4 ++-- .../teams/api/models/conversation/resource.py | 23 +++++++++++-------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/api/src/microsoft/teams/api/models/activity.py b/packages/api/src/microsoft/teams/api/models/activity.py index 729f7657..e4c248c3 100644 --- a/packages/api/src/microsoft/teams/api/models/activity.py +++ b/packages/api/src/microsoft/teams/api/models/activity.py @@ -5,7 +5,7 @@ from typing import Any, Dict, Optional -from pydantic import AliasGenerator, BaseModel, ConfigDict, Field +from pydantic import AliasGenerator, BaseModel, ConfigDict from pydantic.alias_generators import to_camel @@ -20,7 +20,7 @@ class Activity(BaseModel): extra="allow", ) - type: str = Field("message", description="The type of activity (e.g. 'message')") + type: str = "message" """ The type of activity (e.g. 'message'). """ diff --git a/packages/api/src/microsoft/teams/api/models/conversation/conversation.py b/packages/api/src/microsoft/teams/api/models/conversation/conversation.py index c85c7d2a..952cc4a5 100644 --- a/packages/api/src/microsoft/teams/api/models/conversation/conversation.py +++ b/packages/api/src/microsoft/teams/api/models/conversation/conversation.py @@ -5,7 +5,7 @@ from typing import Any, Dict, Optional -from pydantic import AliasGenerator, BaseModel, ConfigDict, Field +from pydantic import AliasGenerator, BaseModel, ConfigDict from pydantic.alias_generators import to_camel @@ -19,7 +19,7 @@ class Conversation(BaseModel): extra="allow", ) - id: str = Field(..., description="The unique identifier for the conversation") + id: str """ The unique identifier for the 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 index 419e9d14..f5ad82ad 100644 --- a/packages/api/src/microsoft/teams/api/models/conversation/resource.py +++ b/packages/api/src/microsoft/teams/api/models/conversation/resource.py @@ -3,7 +3,7 @@ Licensed under the MIT License. """ -from pydantic import AliasGenerator, BaseModel, ConfigDict, Field +from pydantic import AliasGenerator, BaseModel, ConfigDict from pydantic.alias_generators import to_camel @@ -17,14 +17,17 @@ class ConversationResource(BaseModel): extra="allow", ) - id: str = Field( - description="Id of the resource", - ) + id: str + """ + Id of the resource. + """ - activity_id: str = Field( - description="ID of the Activity (if sent)", - ) + activity_id: str + """ + Id of the Activity (if sent). + """ - service_url: str = Field( - description="Service endpoint where operations concerning the conversation may be performed", - ) + service_url: str + """ + Service endpoint where operations concerning the conversation may be performed. + """ From 1f1615898f59b66c5091e9b3d3181a470d2efdbd Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Mon, 16 Jun 2025 16:25:10 -0700 Subject: [PATCH 11/12] better models --- .../microsoft/teams/api/models/__init__.py | 3 +- .../src/microsoft/teams/api/models/account.py | 6 ++-- .../teams/api/models/conversation/__init__.py | 4 +-- .../api/models/conversation/conversation.py | 33 ++++++++++++++----- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/packages/api/src/microsoft/teams/api/models/__init__.py b/packages/api/src/microsoft/teams/api/models/__init__.py index 0dae4715..5c62ffd1 100644 --- a/packages/api/src/microsoft/teams/api/models/__init__.py +++ b/packages/api/src/microsoft/teams/api/models/__init__.py @@ -3,7 +3,7 @@ Licensed under the MIT License. """ -from .account import Account +from .account import Account, AccountRole from .activity import Activity from .conversation import * # noqa: F403 from .conversation import __all__ as conversation_all @@ -11,5 +11,6 @@ __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 index fc613918..6184473d 100644 --- a/packages/api/src/microsoft/teams/api/models/account.py +++ b/packages/api/src/microsoft/teams/api/models/account.py @@ -3,11 +3,13 @@ Licensed under the MIT License. """ -from typing import Any, Dict, Optional +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): """ @@ -29,7 +31,7 @@ class Account(BaseModel): """ The Azure AD object ID. """ - role: Optional[str] = None + role: Optional[AccountRole] = None """ The role of the account in the conversation. """ diff --git a/packages/api/src/microsoft/teams/api/models/conversation/__init__.py b/packages/api/src/microsoft/teams/api/models/conversation/__init__.py index 27b3017c..65539723 100644 --- a/packages/api/src/microsoft/teams/api/models/conversation/__init__.py +++ b/packages/api/src/microsoft/teams/api/models/conversation/__init__.py @@ -3,7 +3,7 @@ Licensed under the MIT License. """ -from .conversation import Conversation +from .conversation import Conversation, ConversationType from .resource import ConversationResource -__all__ = ["Conversation", "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 index 952cc4a5..5f6c5521 100644 --- a/packages/api/src/microsoft/teams/api/models/conversation/conversation.py +++ b/packages/api/src/microsoft/teams/api/models/conversation/conversation.py @@ -3,11 +3,15 @@ Licensed under the MIT License. """ -from typing import Any, Dict, Optional +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.""" @@ -21,17 +25,30 @@ class Conversation(BaseModel): id: str """ - The unique identifier for the conversation. + Conversation ID + """ + + tenant_id: Optional[str] = None """ - type: str + Conversation Tenant ID """ - The type of conversation (e.g., 'channel', 'chat'). + + type: ConversationType """ - is_group: bool + The Conversations Type """ - Whether this is a group conversation. + + name: Optional[str] = None """ - properties: Optional[Dict[str, Any]] = None + The Conversations Name + """ + + is_group: Optional[bool] = None + """ + If the Conversation supports multiple participants + """ + + members: Optional[List[Account]] = None """ - Additional properties for the conversation. + List of members in this conversation """ From c5f0a2d2b38f3120b8e7d83675714ba6da2aea1a Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Mon, 16 Jun 2025 16:28:51 -0700 Subject: [PATCH 12/12] better models --- .../api/src/microsoft/teams/api/models/resource.py | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 packages/api/src/microsoft/teams/api/models/resource.py diff --git a/packages/api/src/microsoft/teams/api/models/resource.py b/packages/api/src/microsoft/teams/api/models/resource.py deleted file mode 100644 index 8979e033..00000000 --- a/packages/api/src/microsoft/teams/api/models/resource.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -from pydantic import BaseModel - - -class ConversationResource(BaseModel): - """A response containing a resource.""" - - id: str - activity_id: str - service_url: str