diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c83d48f..d8fc00cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,4 +79,4 @@ jobs: - name: Run Tests run: | source .venv/bin/activate - pytest packages + uv run poe test-cov-ci diff --git a/.gitignore b/.gitignore index 6ac0bf3f..f6c4a05b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,12 @@ venv.bak/ .dmypy.json dmypy.json +# pytest-cov +.coverage +.coverage.* +htmlcov/ +coverage.xml + .copilot-instructions.md # other @@ -28,10 +34,10 @@ dmypy.json ref/ py.typed -CLAUDE.md .env.claude/ .claude/ +tmpclaude-*-cwd examples/**/.vscode/ examples/**/appPackage/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..d02b590d --- /dev/null +++ b/CLAUDE.md @@ -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//` 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` diff --git a/examples/a2a-test/src/main.py b/examples/a2a-test/src/main.py index b8fe1f23..05c6ecf6 100644 --- a/examples/a2a-test/src/main.py +++ b/examples/a2a-test/src/main.py @@ -4,6 +4,7 @@ """ import asyncio +import logging import re import uuid from os import getenv @@ -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: @@ -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 diff --git a/examples/dialogs/src/main.py b/examples/dialogs/src/main.py index c3a10e5c..f5eafffa 100644 --- a/examples/dialogs/src/main.py +++ b/examples/dialogs/src/main.py @@ -4,8 +4,8 @@ """ import asyncio +import logging import os -from logging import Logger from typing import Any, Optional from microsoft_teams.api import ( @@ -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") diff --git a/examples/dialogs/src/views/customform/index.html b/examples/dialogs/src/views/customform/index.html index 02e3b0dc..caf28314 100644 --- a/examples/dialogs/src/views/customform/index.html +++ b/examples/dialogs/src/views/customform/index.html @@ -3,7 +3,7 @@ Microsoft Teams Task Module Demo - +