diff --git a/packages/apps/src/microsoft/teams/apps/app.py b/packages/apps/src/microsoft/teams/apps/app.py index ab43482f..acb2485b 100644 --- a/packages/apps/src/microsoft/teams/apps/app.py +++ b/packages/apps/src/microsoft/teams/apps/app.py @@ -105,11 +105,7 @@ def __init__(self, **options: Unpack[AppOptions]): break if not http_plugin: - app_id = None - if self.credentials and hasattr(self.credentials, "client_id"): - app_id = self.credentials.client_id - - http_plugin = HttpPlugin(app_id, self.log, self.options.enable_token_validation) + http_plugin = HttpPlugin(self.options.enable_token_validation) plugins.insert(0, http_plugin) self.http = cast(HttpPlugin, http_plugin) diff --git a/packages/apps/src/microsoft/teams/apps/http_plugin.py b/packages/apps/src/microsoft/teams/apps/http_plugin.py index 7da2ad46..dbb6147c 100644 --- a/packages/apps/src/microsoft/teams/apps/http_plugin.py +++ b/packages/apps/src/microsoft/teams/apps/http_plugin.py @@ -22,9 +22,9 @@ SentActivity, TokenProtocol, ) +from microsoft.teams.api.auth.credentials import Credentials from microsoft.teams.apps.http_stream import HttpStream from microsoft.teams.common.http.client import Client, ClientOptions -from microsoft.teams.common.logging import ConsoleLogger from pydantic import BaseModel from .auth import create_jwt_validation_middleware @@ -39,7 +39,7 @@ Sender, StreamerProtocol, ) -from .plugins.metadata import Plugin +from .plugins.metadata import CredentialsDependencyOptions, Plugin version = importlib.metadata.version("microsoft-teams-apps") @@ -54,6 +54,7 @@ class HttpPlugin(Sender): on_error_event: Annotated[Callable[[ErrorEvent], None], EventMetadata(name="error")] on_activity_event: Annotated[Callable[[ActivityEvent], None], EventMetadata(name="activity")] + credentials: Annotated[Credentials, CredentialsDependencyOptions(optional=True)] client: Annotated[Client, DependencyMetadata()] @@ -62,12 +63,10 @@ class HttpPlugin(Sender): def __init__( self, - app_id: Optional[str], - logger: Optional[Logger] = None, enable_token_validation: bool = True, ): super().__init__() - self.logger = logger or ConsoleLogger().create_logger("@teams/http-plugin") + self.enable_token_validation = enable_token_validation self._server: Optional[uvicorn.Server] = None self._port: Optional[int] = None self._on_ready_callback: Optional[Callable[[], Awaitable[None]]] = None @@ -76,6 +75,7 @@ def __init__( # Storage for pending HTTP responses by activity ID self.pending: Dict[str, asyncio.Future[Any]] = {} + async def on_init(self) -> None: # Setup FastAPI app with lifespan @asynccontextmanager async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]: @@ -92,12 +92,11 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]: self.app = FastAPI(lifespan=lifespan) # Add JWT validation middleware - if app_id and enable_token_validation: + if self.credentials.client_id and self.enable_token_validation: jwt_middleware = create_jwt_validation_middleware( - app_id=app_id, logger=self.logger, paths=["/api/messages"] + app_id=self.credentials.client_id, logger=self.logger, paths=["/api/messages"] ) self.app.middleware("http")(jwt_middleware) - # Expose FastAPI routing methods (like TypeScript exposes Express methods) self.get = self.app.get self.post = self.app.post @@ -276,36 +275,36 @@ async def _handle_activity_request(self, request: Request) -> Any: return result + async def on_activity_request(self, request: Request, response: Response) -> Any: + """Handle incoming Teams activity.""" + # Process the activity (token validation handled by middleware) + result = await self._handle_activity_request(request) + status_code: Optional[int] = None + body: Optional[Dict[str, Any]] = None + resp_dict: Optional[Dict[str, Any]] = None + if isinstance(result, dict): + resp_dict = cast(Dict[str, Any], result) + elif isinstance(result, BaseModel): + resp_dict = result.model_dump(exclude_none=True) + + # if resp_dict has status set it + if resp_dict and "status" in resp_dict: + status_code = resp_dict.get("status") + + if resp_dict and "body" in resp_dict: + body = resp_dict.get("body", None) + + if status_code is not None: + response.status_code = status_code + + if body is not None: + return body + return cast(Any, result) + def _setup_routes(self) -> None: """Setup FastAPI routes.""" - async def on_activity_request(request: Request, response: Response) -> Any: - """Handle incoming Teams activity.""" - # Process the activity (token validation handled by middleware) - result = await self._handle_activity_request(request) - status_code: Optional[int] = None - body: Optional[Dict[str, Any]] = None - resp_dict: Optional[Dict[str, Any]] = None - if isinstance(result, dict): - resp_dict = cast(Dict[str, Any], result) - elif isinstance(result, BaseModel): - resp_dict = result.model_dump(exclude_none=True) - - # if resp_dict has status set it - if resp_dict and "status" in resp_dict: - status_code = resp_dict.get("status") - - if resp_dict and "body" in resp_dict: - body = resp_dict.get("body", None) - - if status_code is not None: - response.status_code = status_code - - if body is not None: - return body - return cast(Any, result) - - self.app.post("/api/messages")(on_activity_request) + self.app.post("/api/messages")(self.on_activity_request) async def health_check() -> Dict[str, Any]: """Basic health check endpoint.""" diff --git a/packages/apps/src/microsoft/teams/apps/plugins/metadata.py b/packages/apps/src/microsoft/teams/apps/plugins/metadata.py index f8f088ac..3e0c9352 100644 --- a/packages/apps/src/microsoft/teams/apps/plugins/metadata.py +++ b/packages/apps/src/microsoft/teams/apps/plugins/metadata.py @@ -4,7 +4,7 @@ """ from dataclasses import dataclass -from typing import Any, Literal, Optional, Type, Union +from typing import Callable, Literal, Optional, Type, TypeVar, Union from ..plugins.plugin_base import PluginBase @@ -20,10 +20,15 @@ class PluginOptions: description: Optional[str] = None -def Plugin(name: Optional[str] = None, version: Optional[str] = None, description: Optional[str] = None) -> Any: +T = TypeVar("T") + + +def Plugin( + name: Optional[str] = None, version: Optional[str] = None, description: Optional[str] = None +) -> Callable[[Type[T]], Type[T]]: """Turns any class into a plugin using the decorator pattern.""" - def decorator(cls: Type[Any]) -> Any: + def decorator(cls: Type[T]) -> Type[T]: plugin_name = name or cls.__name__ plugin_version = version or "0.0.0" plugin_description = description or "" diff --git a/packages/botbuilder/README.md b/packages/botbuilder/README.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/botbuilder/pyproject.toml b/packages/botbuilder/pyproject.toml new file mode 100644 index 00000000..0e131767 --- /dev/null +++ b/packages/botbuilder/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "microsoft-teams-botbuilder" +version = "0.0.1-alpha.4" +description = "A plugin to use TeamsAI libraries with existing botbuilder applications" +authors = [{ name = "Microsoft", email = "TeamsAISDKFeedback@microsoft.com" }] +readme = "README.md" +requires-python = ">=3.12" +repository = "https://github.com/microsoft/teams.py" +keywords = ["microsoft", "teams", "ai", "bot", "agents"] +license = "MIT" +dependencies = [ + "botbuilder-integration-aiohttp>=4.17.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/microsoft"] + +[tool.hatch.build.targets.sdist] +include = ["src"] diff --git a/packages/botbuilder/src/microsoft/teams/botbuilder/__init__.py b/packages/botbuilder/src/microsoft/teams/botbuilder/__init__.py new file mode 100644 index 00000000..45571fc5 --- /dev/null +++ b/packages/botbuilder/src/microsoft/teams/botbuilder/__init__.py @@ -0,0 +1,8 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + + +def hello() -> str: + return "Hello from botbuilder!" diff --git a/packages/botbuilder/src/microsoft/teams/botbuilder/plugin.py b/packages/botbuilder/src/microsoft/teams/botbuilder/plugin.py new file mode 100644 index 00000000..ed26a1ab --- /dev/null +++ b/packages/botbuilder/src/microsoft/teams/botbuilder/plugin.py @@ -0,0 +1,55 @@ +import importlib.metadata +from typing import Annotated + +from fastapi import Request +from microsoft.teams.api.auth.credentials import ClientCredentials, Credentials +from microsoft.teams.apps import Plugin, Sender +from microsoft.teams.apps.http_plugin import HttpPlugin +from microsoft.teams.apps.plugins.metadata import DependencyMetadata +from microsoft.teams.common.http.client import Client + +from botbuilder.core import ActivityHandler, Bot +from botbuilder.integration.aiohttp import CloudAdapter, ConfigurationServiceClientCredentialFactory + +version = importlib.metadata.version("microsoft-teams-botbuilder") + + +@Plugin(name="http", version=version, description="A plugin to use teams ai library with bot builder") +class BotbuilderPlugin(HttpPlugin, Sender): + client: Annotated[Client, DependencyMetadata()] + credentials: Annotated[Credentials | None, DependencyMetadata()] + + adapter: CloudAdapter + + def __init__(self, bot: Bot, adapter: CloudAdapter | None = None, handler: ActivityHandler | None = None): + self.adapter = adapter + self.bot = bot + self.botbuilder_handler = handler + + async def on_init(self) -> None: + await super().on_init() + if self.adapter is None: + client_id = self.credentials.client_id if self.credentials else None + secret = ( + self.credentials.client_secret + if self.credentials and isinstance(self.credentials, ClientCredentials) + else None + ) + tenant_id = ( + self.credentials.tenant_id + if self.credentials and isinstance(self.credentials, ClientCredentials) + else None + ) + self.adapter = CloudAdapter( + ConfigurationServiceClientCredentialFactory( + { + "APP_TYPE": "SingleTenant" if tenant_id is not None else "MultiTenant", + "APP_ID": client_id, + "APP_PASSWORD": secret, + } + ) + ) + + async def on_activity_request(self, request: Request, response: Response) -> Any: + await self.adapter.process(request, self.bot) + await super().on_activity_request(request, response) diff --git a/pyproject.toml b/pyproject.toml index 6ee2381f..c12e71fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ "microsoft-teams-graph" = { workspace = true } "microsoft-teams-ai" = { workspace = true } "microsoft-teams-openai" = { workspace = true } +"microsoft-teams-botbuilder" = { workspace = true } [tool.uv.workspace] members = ["packages/*", "tests/*"] diff --git a/pyrightconfig.json b/pyrightconfig.json index 58d60fe0..2f16bfd9 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -13,7 +13,8 @@ "packages/graph/src", "packages/devtools/src", "packages/ai/src", - "packages/openai/src" + "packages/openai/src", + "packages/botbuilder/src" ], "typeCheckingMode": "strict", "executionEnvironments": [ diff --git a/tests/botbuilder-test/README.md b/tests/botbuilder-test/README.md new file mode 100644 index 00000000..e69de29b diff --git a/tests/botbuilder-test/pyproject.toml b/tests/botbuilder-test/pyproject.toml new file mode 100644 index 00000000..732ac7d3 --- /dev/null +++ b/tests/botbuilder-test/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "botbuilder-test" +version = "0.1.0" +description = "test to make sure botbuilder plugin seemlessly integrates with botbuilder and teamsai" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "dotenv>=0.9.9", + "microsoft-teams-apps", +] + +[tool.uv.sources] +microsoft-teams-apps = { workspace = true } diff --git a/tests/botbuilder-test/src/main.py b/tests/botbuilder-test/src/main.py new file mode 100644 index 00000000..4ab2fc9f --- /dev/null +++ b/tests/botbuilder-test/src/main.py @@ -0,0 +1,27 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import asyncio + +from microsoft.teams.api import MessageActivity +from microsoft.teams.apps import ActivityContext, App + +app = App() + + +@app.on_message +async def handle_message(ctx: ActivityContext[MessageActivity]): + """Handle message activities using the new generated handler system.""" + print(f"[GENERATED onMessage] Message received: {ctx.activity.text}") + print(f"[GENERATED onMessage] From: {ctx.activity.from_}") + + if "reply" in ctx.activity.text.lower(): + await ctx.reply("Hello! How can I assist you today?") + else: + await ctx.send(f"You said '{ctx.activity.text}'") + + +if __name__ == "__main__": + asyncio.run(app.start()) diff --git a/uv.lock b/uv.lock index 4e8c7b25..4f38f74c 100644 --- a/uv.lock +++ b/uv.lock @@ -5,6 +5,7 @@ requires-python = ">=3.12" [manifest] members = [ "ai-test", + "botbuilder-test", "dialogs", "echo", "graph", @@ -12,6 +13,7 @@ members = [ "microsoft-teams-ai", "microsoft-teams-api", "microsoft-teams-apps", + "microsoft-teams-botbuilder", "microsoft-teams-cards", "microsoft-teams-common", "microsoft-teams-devtools", @@ -209,6 +211,88 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/7e/f7b6f453e6481d1e233540262ccbfcf89adcd43606f44a028d7f5fae5eb2/binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4", size = 9006, upload-time = "2017-08-03T15:55:31.23Z" }, ] +[[package]] +name = "botbuilder-core" +version = "4.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botbuilder-schema" }, + { name = "botframework-connector" }, + { name = "botframework-streaming" }, + { name = "jsonpickle" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/51/0e4d0ba1fc25d57977e17d7bfd55c7bd06a9d8403d5efb23e6fc512356fa/botbuilder_core-4.17.0-py3-none-any.whl", hash = "sha256:56828b11d9af663a200fba0fae4e0b9ad6f815a679c3e90e9b6425b9cbe16d53", size = 116148, upload-time = "2025-05-29T15:10:00.474Z" }, +] + +[[package]] +name = "botbuilder-integration-aiohttp" +version = "4.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "botbuilder-core" }, + { name = "botbuilder-schema" }, + { name = "botframework-connector" }, + { name = "yarl" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/e2/78d9ed5beeeaf9dcdcfd8b7dd0fe4116c8b3ed55b81eb9b01b28037a2b7c/botbuilder_integration_aiohttp-4.17.0-py3-none-any.whl", hash = "sha256:30cd8de3eeec132463bf1834fbd6cfbc6c4a9a361bf478627b6b2f0e7ee211b6", size = 19030, upload-time = "2025-05-29T15:10:02.884Z" }, +] + +[[package]] +name = "botbuilder-schema" +version = "4.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msrest" }, + { name = "urllib3" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/10/db5e16bf91fe60b78c88bf42b46e87af7f75097303cece71478a5ae46ccf/botbuilder_schema-4.17.0-py2.py3-none-any.whl", hash = "sha256:97219f8361a91bfa1529ba1231100b527adbe7c4c249d6b23fe1bffaf012b31b", size = 38197, upload-time = "2025-05-29T15:10:05.111Z" }, +] + +[[package]] +name = "botbuilder-test" +version = "0.1.0" +source = { virtual = "tests/botbuilder-test" } +dependencies = [ + { name = "dotenv" }, + { name = "microsoft-teams-apps" }, +] + +[package.metadata] +requires-dist = [ + { name = "dotenv", specifier = ">=0.9.9" }, + { name = "microsoft-teams-apps", editable = "packages/apps" }, +] + +[[package]] +name = "botframework-connector" +version = "4.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botbuilder-schema" }, + { name = "msal" }, + { name = "msrest" }, + { name = "pyjwt" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/df/8daa548a5c73d673e8464cbd156dd8235d6efc35296ad45e89ff9f97c109/botframework_connector-4.17.0-py2.py3-none-any.whl", hash = "sha256:51b8702cc348c63efbdb571f6a4da868d1660efb80ed77d6e04f081720105a87", size = 100886, upload-time = "2025-05-29T15:10:07.271Z" }, +] + +[[package]] +name = "botframework-streaming" +version = "4.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botbuilder-schema" }, + { name = "botframework-connector" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/d2/c55470c918c403ebd76b3c1b54e312021bf7edacaf61e4b1c3b132216490/botframework_streaming-4.17.0-py3-none-any.whl", hash = "sha256:ed64fd2a9f56a3d32dd252bf378d0ce145a9bbc9c9a9a0e770889c7d140df435", size = 41953, upload-time = "2025-05-29T15:10:08.499Z" }, +] + [[package]] name = "certifi" version = "2025.8.3" @@ -760,6 +844,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -820,6 +913,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213, upload-time = "2025-05-18T19:04:41.894Z" }, ] +[[package]] +name = "jsonpickle" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/df/8072fb98c12d78dd29b4a52c50af7ab548f84166b8a3d363c1c754c14af0/jsonpickle-1.4.2.tar.gz", hash = "sha256:c9b99b28a9e6a3043ec993552db79f4389da11afcb1d0246d93c79f4b5e64062", size = 104745, upload-time = "2020-11-30T03:21:56.571Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/d5/1cc282dc23346a43aab461bf2e8c36593aacd34242bee1a13fa750db0cfe/jsonpickle-1.4.2-py2.py3-none-any.whl", hash = "sha256:2ac5863099864c63d7f0c367af5e512c94f3384977dd367f2eae5f2303f7b92c", size = 36529, upload-time = "2020-11-30T03:21:53.857Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -1077,6 +1179,17 @@ test = [ { name = "pytest-asyncio", specifier = ">=0.24.0" }, ] +[[package]] +name = "microsoft-teams-botbuilder" +version = "0.0.1a4" +source = { editable = "packages/botbuilder" } +dependencies = [ + { name = "botbuilder-integration-aiohttp" }, +] + +[package.metadata] +requires-dist = [{ name = "botbuilder-integration-aiohttp", specifier = ">=4.17.0" }] + [[package]] name = "microsoft-teams-cards" version = "0.0.1a4" @@ -1254,6 +1367,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/c4/3c315d2a00de25780a83f3a0392861571c0b00b74c0e66ed66dc32a357c3/msgraph_sdk-1.40.0-py3-none-any.whl", hash = "sha256:1f2e966ccfded5fade55225f2f671b965a7ad58ba16f34be05fccc459596a076", size = 24750562, upload-time = "2025-07-30T16:02:14.48Z" }, ] +[[package]] +name = "msrest" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "certifi" }, + { name = "isodate" }, + { name = "requests" }, + { name = "requests-oauthlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/77/8397c8fb8fc257d8ea0fa66f8068e073278c65f05acb17dcb22a02bfdc42/msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9", size = 175332, upload-time = "2022-06-13T22:41:25.111Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/cf/f2966a2638144491f8696c27320d5219f48a072715075d168b31d3237720/msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32", size = 85384, upload-time = "2022-06-13T22:41:22.42Z" }, +] + [[package]] name = "multidict" version = "6.6.4" @@ -1341,6 +1470,15 @@ requires-dist = [ { name = "microsoft-teams-apps", editable = "packages/apps" }, ] +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + [[package]] name = "openai" version = "1.102.0" @@ -1726,6 +1864,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + [[package]] name = "rich" version = "14.1.0"