Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,4 @@ jobs:
- name: Run Tests
run: |
source .venv/bin/activate
pytest packages
uv run poe test-cov-ci
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ venv.bak/
.dmypy.json
dmypy.json

# pytest-cov
.coverage
.coverage.*
htmlcov/
coverage.xml

.copilot-instructions.md

# other
Expand All @@ -28,10 +34,10 @@ dmypy.json

ref/
py.typed
CLAUDE.md

.env.claude/
.claude/
tmpclaude-*-cwd

examples/**/.vscode/
examples/**/appPackage/
Expand Down
85 changes: 85 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Microsoft Teams Python SDK — a UV workspace with multiple packages providing APIs, common utilities, and integrations for Microsoft Teams.

## Development Setup

### Prerequisites
- UV >= 0.8.11
- Python >= 3.12

### Commands
```bash
uv sync # Install virtual env and dependencies
source .venv/bin/activate # Activate virtual environment
pre-commit install # Install pre-commit hooks

poe fmt # Format code with ruff
poe lint # Lint code with ruff
poe check # Run both format and lint
poe test # Run tests with pytest
pyright # Run type checker
```

## Tooling

- **Formatter/Linter**: Ruff — line length 120, rules: E, F, W, B, Q, I, ASYNC
- **Type checker**: Pyright
- **Test framework**: pytest + pytest-asyncio (Ruff bans importing the unittest test framework; unittest.mock is allowed and used)

## Architecture

### Workspace Structure
All packages live in `packages/`, each with `src/microsoft_teams/<package>/` layout:

| Package | Description |
|---------|-------------|
| `api` | Core API clients, models (Account, Activity, Conversation), auth |
| `apps` | App orchestrator, plugins, routing, events, HttpServer |
| `common` | HTTP client abstraction, logging, storage |
| `cards` | Adaptive cards |
| `ai` | AI/function calling utilities |
| `botbuilder` | Bot Framework integration plugin |
| `devtools` | Development tools plugin |
| `mcpplugin` | MCP server plugin |
| `a2aprotocol` | A2A protocol plugin |
| `graph` | Microsoft Graph integration |
| `openai` | OpenAI integration |

### Key Patterns

**Imports**
- ALL imports MUST be at the top of the file — no imports inside functions, classes, or conditional blocks
- Avoid `TYPE_CHECKING` blocks unless absolutely necessary (genuine circular imports that can't be restructured)
- Avoid dynamic/deferred imports unless absolutely necessary
- Relative imports within the same package, absolute for external packages

**Models**
- Pydantic with `ConfigDict(alias_generator=to_camel)` — snake_case in Python, camelCase in JSON
- `model_dump(by_alias=True)` for serialization, `model_dump(exclude_none=True)` for query params

**Interfaces**
- Protocol classes instead of Abstract Base Classes (ABC)
- Prefer composition over inheritance

**Clients**
- Concrete clients inherit from `BaseClient` (`packages/api/src/microsoft_teams/api/clients/base_client.py`)
- Composition with operation classes for sub-functionality
- async/await for all API calls, return domain models

## Scaffolding (cookiecutter)

```bash
cookiecutter templates/package -o packages # New package
cookiecutter templates/test -o tests # New test package
```

## Dependencies and Build

- UV workspace — packages reference each other via `{ workspace = true }`
- Hatchling build backend
- Dev dependencies in root `pyproject.toml`
13 changes: 10 additions & 3 deletions examples/a2a-test/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

import asyncio
import logging
import re
import uuid
from os import getenv
Expand All @@ -24,14 +25,20 @@
from microsoft_teams.ai import ChatPrompt, Function, ModelMessage
from microsoft_teams.api import MessageActivity, TypingActivityInput
from microsoft_teams.apps import ActivityContext, App, PluginBase
from microsoft_teams.common import ConsoleLogger, ConsoleLoggerOptions
from microsoft_teams.common import ConsoleFormatter
from microsoft_teams.devtools import DevToolsPlugin
from microsoft_teams.openai.completions_model import OpenAICompletionsAIModel
from pydantic import BaseModel

logger = ConsoleLogger().create_logger("a2a", ConsoleLoggerOptions(level="debug"))
PORT = getenv("PORT", "4000")

# Setup logging
logging.getLogger().setLevel(logging.DEBUG)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(ConsoleFormatter())
logging.getLogger().addHandler(stream_handler)
logger = logging.getLogger(__name__)


# Setup AI
def get_required_env(key: str) -> str:
Expand Down Expand Up @@ -146,7 +153,7 @@ class LocationParams(BaseModel):

# Setup the A2A Server Plugin
plugins: List[PluginBase] = [A2APlugin(A2APluginOptions(agent_card=agent_card)), DevToolsPlugin()]
app = App(logger=logger, plugins=plugins)
app = App(plugins=plugins)


# A2A Server Event Handler
Expand Down
13 changes: 9 additions & 4 deletions examples/dialogs/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"""

import asyncio
import logging
import os
from logging import Logger
from typing import Any, Optional

from microsoft_teams.api import (
Expand All @@ -25,10 +25,15 @@
from microsoft_teams.apps import ActivityContext, App
from microsoft_teams.apps.events.types import ErrorEvent
from microsoft_teams.cards import AdaptiveCard, SubmitAction, SubmitActionData, TaskFetchSubmitActionData, TextBlock
from microsoft_teams.common.logging import ConsoleLogger
from microsoft_teams.common import ConsoleFormatter

# Setup logging
logging.getLogger().setLevel(logging.DEBUG)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(ConsoleFormatter())
logging.getLogger().addHandler(stream_handler)
logger = logging.getLogger(__name__)

logger_instance = ConsoleLogger()
logger: Logger = logger_instance.create_logger("@apps/dialogs")

if not os.getenv("BOT_ENDPOINT"):
logger.warning("No remote endpoint detected. Using webpages for dialog will not work as expected")
Expand Down
30 changes: 17 additions & 13 deletions examples/dialogs/src/views/customform/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<title>Microsoft Teams Task Module Demo</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
<script src="https://statics.teams.cdn.office.net/sdk/v1.11.0/js/MicrosoftTeams.min.js"></script>
<script src="https://res.cdn.office.net/teams-js/2.31.1/js/MicrosoftTeams.min.js"></script>
<style>
body { margin: 0; padding: 10px; }
.form-group { margin-bottom: 10px; }
Expand All @@ -25,17 +25,21 @@
</div>

<script>
microsoftTeams.initialize();

document.getElementById('customForm').addEventListener('submit', function(event) {
event.preventDefault();
let formData = {
name: document.getElementById('name').value,
email: document.getElementById('email').value,
submissiondialogtype: 'webpage_dialog'
};
microsoftTeams.tasks.submitTask(formData);
});
async function init() {
await microsoftTeams.app.initialize();

document.getElementById('customForm').addEventListener('submit', function(event) {
event.preventDefault();
let formData = {
name: document.getElementById('name').value,
email: document.getElementById('email').value,
submissiondialogtype: 'webpage_dialog'
};
microsoftTeams.dialog.url.submit(formData);
});
}

init();
</script>
</body>
</html>
</html>
16 changes: 11 additions & 5 deletions examples/graph/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@
from azure.core.exceptions import ClientAuthenticationError
from microsoft_teams.api import MessageActivity
from microsoft_teams.apps import ActivityContext, App, AppOptions, ErrorEvent, SignInEvent
from microsoft_teams.common import ConsoleFormatter
from microsoft_teams.graph import get_graph_client
from msgraph.generated.users.item.messages.messages_request_builder import ( # type: ignore
MessagesRequestBuilder,
)

# Setup logging
logging.getLogger().setLevel(logging.DEBUG)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(ConsoleFormatter())
logging.getLogger().addHandler(stream_handler)
logger = logging.getLogger(__name__)

app_options = AppOptions(default_connection_name=os.getenv("CONNECTION_NAME", "graph"))
Expand All @@ -39,7 +45,7 @@ async def get_authenticated_graph_client(ctx: ActivityContext[MessageActivity]):
return get_graph_client(ctx.user_token)

except Exception as e:
ctx.logger.error(f"Failed to create Graph client: {e}")
logger.error(f"Failed to create Graph client: {e}")
await ctx.send("🔐 Failed to create authenticated client. Please try signing in again.")
await ctx.sign_in()
return None
Expand Down Expand Up @@ -90,12 +96,12 @@ async def handle_profile_command(ctx: ActivityContext[MessageActivity]):
await ctx.send("❌ Could not retrieve your profile information.")

except ClientAuthenticationError as e:
ctx.logger.error(f"Authentication error: {e}")
logger.error(f"Authentication error: {e}")
await ctx.send("🔐 Authentication failed. Please try signing in again.")
await ctx.sign_in()

except Exception as e:
ctx.logger.error(f"Error getting profile: {e}")
logger.error(f"Error getting profile: {e}")
await ctx.send(f"❌ Failed to get your profile: {str(e)}")


Expand Down Expand Up @@ -138,11 +144,11 @@ async def handle_emails_command(ctx: ActivityContext[MessageActivity]):
await ctx.send("📪 No recent emails found.")

except ClientAuthenticationError as e:
ctx.logger.error(f"Authentication error: {e}")
logger.error(f"Authentication error: {e}")
await ctx.send("🔐 Authentication failed. You may need additional permissions to read emails.")

except Exception as e:
ctx.logger.error(f"Error getting emails: {e}")
logger.error(f"Error getting emails: {e}")
await ctx.send(f"❌ Failed to get your emails: {str(e)}")


Expand Down
55 changes: 55 additions & 0 deletions examples/http-adapters/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# HTTP Adapters Examples

Examples showing how to use custom `HttpServerAdapter` implementations and non-managed server patterns with the Teams Python SDK.

## Examples

### 1. Starlette Adapter (`starlette_echo.py`)

A custom `HttpServerAdapter` implementation for [Starlette](https://www.starlette.io/). Demonstrates how to write an adapter for any ASGI framework.

**Pattern**: Custom adapter, SDK-managed server lifecycle (`app.start()`)

```bash
python src/starlette_echo.py
```

### 2. Non-Managed FastAPI (`fastapi_non_managed.py`)

Use your own FastAPI app with your own routes, and let the SDK register `/api/messages` on it. You manage the server lifecycle yourself.

**Pattern**: Default `FastAPIAdapter` with user-provided FastAPI instance, user-managed server (`app.initialize()` + your own `uvicorn.Server`)

```bash
python src/fastapi_non_managed.py
```

## Key Concepts

### Managed vs Non-Managed

| | Managed | Non-Managed |
|---|---|---|
| **Entry point** | `app.start(port)` | `app.initialize()` + start server yourself |
| **Who starts the server** | The SDK (via adapter) | You |
| **When to use** | New apps, simple setup | Existing apps, custom server config |

### Writing a Custom Adapter

Implement the `HttpServerAdapter` protocol:

```python
class MyAdapter:
def register_route(self, method, path, handler): ...
def serve_static(self, path, directory): ...
async def start(self, port): ...
async def stop(self): ...
```

The handler signature is framework-agnostic:

```python
async def handler(request: HttpRequest) -> HttpResponse:
# request = { "body": dict, "headers": dict }
# return { "status": int, "body": object }
```
17 changes: 17 additions & 0 deletions examples/http-adapters/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[project]
name = "http-adapters"
version = "0.1.0"
description = "Examples showing custom HttpServerAdapter and non-managed server patterns"
readme = "README.md"
requires-python = ">=3.12,<3.15"
dependencies = [
"dotenv>=0.9.9",
"microsoft-teams-apps",
"microsoft-teams-api",
"starlette",
"uvicorn",
"httptools",
]

[tool.uv.sources]
microsoft-teams-apps = { workspace = true }
Loading
Loading