diff --git a/.azdo/publish.yml b/.azdo/publish.yml index 9276439b7..a6105de68 100644 --- a/.azdo/publish.yml +++ b/.azdo/publish.yml @@ -20,7 +20,7 @@ parameters: variables: - group: TeamsSDK-Release - name: ExcludePackageFolders - value: 'devtools' # Space-separated list of distribution name fragments to exclude (matched as microsoft_teams_*) + value: '' # Space-separated list of distribution name fragments to exclude (matched as microsoft_teams_*) resources: repositories: diff --git a/.github/scripts/analyze_issue.py b/.github/scripts/analyze_issue.py new file mode 100644 index 000000000..c3bb98e62 --- /dev/null +++ b/.github/scripts/analyze_issue.py @@ -0,0 +1,266 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +# GitHub Issue Analysis → Teams Notification +# Analyzes newly opened GitHub issues using the GitHub Models API (GPT-4o) +# and sends a summary card + action plan to a Microsoft Teams channel. + +import asyncio +import json +import os +import sys + +from microsoft_teams.apps import App +from microsoft_teams.cards import ( + ActionSet, + AdaptiveCard, + Column, + ColumnSet, + Container, + Fact, + FactSet, + OpenUrlAction, + TextBlock, +) +from openai import OpenAI + +TRIAGE_PROMPT = """\ +You are a GitHub issue triage assistant for the Microsoft Teams Python SDK. + +The SDK is a UV workspace with these packages: +- api: Core API clients, models, 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 + +Analyze the issue and respond with ONLY valid JSON (no markdown fencing): +{ + "category": "bug | feature | question | docs | security", + "severity": "critical | high | medium | low | info", + "summary": "1-2 sentence plain-text summary of the issue", + "affected_packages": ["list", "of", "affected", "packages"], + "suggested_labels": ["list", "of", "suggested", "labels"] +}\ +""" + +SEVERITY_COLORS: dict[str, str] = { + "critical": "Attention", + "high": "Attention", + "medium": "Warning", + "low": "Good", + "info": "Default", +} + + +def load_issue_from_env() -> dict: + """Read issue details from environment variables set by the workflow.""" + number = os.environ.get("ISSUE_NUMBER") + if not number: + print("ERROR: ISSUE_NUMBER not set") + sys.exit(1) + + labels_str = os.environ.get("ISSUE_LABELS", "") + return { + "number": int(number), + "title": os.environ.get("ISSUE_TITLE", ""), + "body": os.environ.get("ISSUE_BODY", "") or "", + "author": os.environ.get("ISSUE_AUTHOR", "unknown"), + "html_url": os.environ.get("ISSUE_HTML_URL", ""), + "labels": [label.strip() for label in labels_str.split(",") if label.strip()], + } + + +def _call_model(system_prompt: str, user_message: str) -> str: + """Call GitHub Models API and return the response content.""" + token = os.environ.get("GITHUB_TOKEN") + if not token: + print("ERROR: GITHUB_TOKEN not set") + sys.exit(1) + + client = OpenAI( + base_url="https://models.inference.ai.azure.com", + api_key=token, + ) + + response = client.chat.completions.create( + model="gpt-4o", + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_message}, + ], + temperature=0.2, + ) + + return response.choices[0].message.content or "" + + +def _issue_as_user_message(issue: dict) -> str: + """Format issue data as a user message for the model.""" + return ( + f"Issue #{issue['number']}: {issue['title']}\n\n" + f"Author: {issue['author']}\n" + f"Labels: {', '.join(issue['labels']) or 'none'}\n\n" + f"Body:\n{issue['body'][:3000]}" + ) + + +def triage_issue(issue: dict) -> dict: + """Triage the issue: category, severity, summary, etc.""" + content = _call_model(TRIAGE_PROMPT, _issue_as_user_message(issue)) + try: + return json.loads(content) + except json.JSONDecodeError: + # Model may wrap JSON in markdown fences — try extracting it + start = content.find("{") + end = content.rfind("}") + if start != -1 and end > start: + try: + return json.loads(content[start : end + 1]) + except json.JSONDecodeError: + pass + return { + "category": "question", + "severity": "info", + "summary": f"Automated triage failed to parse model response. Review issue #{issue['number']} manually.", + "affected_packages": [], + "suggested_labels": [], + } + + +def load_copilot_analysis() -> str: + """Read the Copilot CLI analysis from file.""" + path = os.environ.get("COPILOT_ANALYSIS_FILE", "/tmp/analysis.txt") + if not os.path.exists(path): + return "_No Copilot analysis available._" + with open(path) as f: + return f.read().strip() or "_No Copilot analysis available._" + + +def build_triage_card(issue: dict, triage: dict) -> AdaptiveCard: + """Build an Adaptive Card with the triage summary.""" + repo = os.environ.get("GITHUB_UPSTREAM_REPO") or os.environ.get("GITHUB_REPOSITORY", "microsoft/teams.py") + severity = triage.get("severity", "info") + severity_color = SEVERITY_COLORS.get(severity, "Default") + + return AdaptiveCard( + version="1.5", + body=[ + TextBlock( + text=f"{repo}#{issue['number']}: {issue['title']}", + size="Medium", + weight="Bolder", + wrap=True, + ), + ColumnSet( + columns=[ + Column( + width="auto", + items=[ + TextBlock( + text=triage.get("category", "unknown").upper(), + weight="Bolder", + is_subtle=True, + size="Small", + ), + ], + ), + Column( + width="auto", + items=[ + TextBlock( + text=severity.upper(), + color=severity_color, + weight="Bolder", + size="Small", + ), + ], + ), + Column( + width="stretch", + items=[ + TextBlock( + text=f"by @{issue['author']}", + is_subtle=True, + size="Small", + horizontal_alignment="Right", + ), + ], + ), + ], + ), + Container( + style="emphasis", + items=[ + TextBlock( + text=triage.get("summary", "No summary available."), + wrap=True, + ), + ], + ), + FactSet( + facts=[ + Fact( + title="Packages", + value=", ".join(triage.get("affected_packages", [])) or "N/A", + ), + Fact( + title="Suggested labels", + value=", ".join(triage.get("suggested_labels", [])) or "N/A", + ), + ], + ), + ActionSet( + actions=[ + OpenUrlAction(title="View Issue", url=issue["html_url"]), + ], + ), + ], + ) + + +async def main() -> None: + print("Loading issue from environment...") + issue = load_issue_from_env() + print(f"Issue #{issue['number']}: {issue['title']}") + + print("Triaging issue...") + triage = triage_issue(issue) + print(f"Triage: category={triage.get('category')}, severity={triage.get('severity')}") + + print("Loading Copilot analysis...") + action_plan = load_copilot_analysis() + + print("Building triage card...") + card = build_triage_card(issue, triage) + + conversation_id = os.environ.get("TEAMS_CONVERSATION_ID") + if not conversation_id: + print("ERROR: TEAMS_CONVERSATION_ID not set") + sys.exit(1) + + app = App() + await app.initialize() + + print("Sending triage card...") + result = await app.send(conversation_id, card) + print(f"Triage card sent. Activity ID: {result.id}") + + print("Sending action plan as threaded reply...") + thread_id = f"{conversation_id};messageid={result.id}" + result = await app.send(thread_id, action_plan) + print(f"Action plan sent. Activity ID: {result.id}") + + print("Done!") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/.github/workflows/issue-analysis.yml b/.github/workflows/issue-analysis.yml new file mode 100644 index 000000000..ec7d91ad5 --- /dev/null +++ b/.github/workflows/issue-analysis.yml @@ -0,0 +1,158 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +name: "Issue Analysis → Teams" + +on: + issues: + types: [opened] + workflow_dispatch: + inputs: + issue_number: + description: "Issue number to analyze" + required: true + type: number + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analyze-and-notify: + name: Analyze Issue & Notify Teams + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' || github.event.issue.performed_via_github_app == null + + permissions: + issues: read + contents: read + models: read + + steps: + - name: Harden Runner + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 + with: + egress-policy: audit + + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + + - name: Install Python dependencies + run: | + uv venv .venv + uv pip install --python .venv/bin/python openai microsoft-teams-apps microsoft-teams-cards + + - name: Install Copilot CLI + run: npm install -g @github/copilot + + - name: Resolve issue details + id: issue + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + EVENT_NAME: ${{ github.event_name }} + INPUT_NUMBER: ${{ inputs.issue_number }} + # Pass event data through env vars to avoid shell injection + EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + EVENT_ISSUE_TITLE: ${{ github.event.issue.title }} + EVENT_ISSUE_AUTHOR: ${{ github.event.issue.user.login }} + EVENT_ISSUE_URL: ${{ github.event.issue.html_url }} + EVENT_ISSUE_LABELS: ${{ toJSON(github.event.issue.labels) }} + EVENT_ISSUE_BODY: ${{ github.event.issue.body }} + run: | + # Use heredoc form for all fields to prevent output injection + # (issue titles/authors could contain newlines or %) + write_output() { + local name="$1" + local value="$2" + local delimiter="EOF_${name}_$$" + { + printf '%s<<%s\n' "$name" "$delimiter" + printf '%s\n' "$value" + printf '%s\n' "$delimiter" + } >> "$GITHUB_OUTPUT" + } + + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + ISSUE=$(gh api repos/microsoft/teams.py/issues/"$INPUT_NUMBER") + write_output "number" "$(echo "$ISSUE" | jq -r '.number')" + write_output "title" "$(echo "$ISSUE" | jq -r '.title')" + write_output "author" "$(echo "$ISSUE" | jq -r '.user.login')" + write_output "html_url" "$(echo "$ISSUE" | jq -r '.html_url')" + write_output "labels" "$(echo "$ISSUE" | jq -r '[.labels[].name] | join(",")')" + write_output "body" "$(echo "$ISSUE" | jq -r '.body // ""')" + else + write_output "number" "$EVENT_ISSUE_NUMBER" + write_output "title" "$EVENT_ISSUE_TITLE" + write_output "author" "$EVENT_ISSUE_AUTHOR" + write_output "html_url" "$EVENT_ISSUE_URL" + write_output "labels" "$(echo "$EVENT_ISSUE_LABELS" | jq -r '[.[].name] | join(",")')" + write_output "body" "$EVENT_ISSUE_BODY" + fi + + - name: Analyze issue with Copilot CLI + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_PAT }} + GITHUB_UPSTREAM_REPO: microsoft/teams.py + run: | + ISSUE_NUMBER="${{ steps.issue.outputs.number }}" + ISSUE_URL="https://github.com/${GITHUB_UPSTREAM_REPO}/issues/${ISSUE_NUMBER}" + + copilot -p "You are analyzing GitHub issue #${ISSUE_NUMBER} for the Microsoft Teams Python SDK. + + Issue URL: ${ISSUE_URL} + + Read the codebase to understand the architecture, then provide a concrete action plan in markdown: + 1. **Root cause** — What's likely going wrong or what's missing + 2. **Files to investigate** — Specific file paths to look at (use actual paths you found) + 3. **Proposed approach** — Step-by-step what a developer should do to resolve this + 4. **Estimated complexity** — Small (< 1 day), Medium (1-3 days), or Large (3+ days) + + Be specific. Reference actual files and code you found in the repo." \ + --allow-tool='shell(git:*)' \ + --allow-tool='read' \ + --allow-tool='glob' \ + --allow-tool='grep' \ + --no-ask-user > /tmp/raw_analysis.txt + + # Strip tool call lines (● prefix and └ prefix) to get just the analysis + python3 -c " + import sys + lines = open('/tmp/raw_analysis.txt').readlines() + # Find the last block of non-tool-call text + result = [] + in_result = False + for line in lines: + stripped = line.strip() + if stripped.startswith('●') or stripped.startswith('└') or stripped.startswith('│'): + in_result = False + result = [] + else: + in_result = True + result.append(line) + # Write the final analysis block + sys.stdout.write(''.join(result).strip()) + " > /tmp/analysis.txt + + echo "--- Copilot analysis ---" + cat /tmp/analysis.txt + + - name: Triage and notify Teams + env: + CLIENT_ID: ${{ secrets.TEAMS_CLIENT_ID }} + CLIENT_SECRET: ${{ secrets.TEAMS_CLIENT_SECRET }} + TENANT_ID: ${{ secrets.TEAMS_TENANT_ID }} + TEAMS_CONVERSATION_ID: ${{ secrets.TEAMS_CONVERSATION_ID }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_UPSTREAM_REPO: microsoft/teams.py + COPILOT_ANALYSIS_FILE: /tmp/analysis.txt + ISSUE_NUMBER: ${{ steps.issue.outputs.number }} + ISSUE_TITLE: ${{ steps.issue.outputs.title }} + ISSUE_AUTHOR: ${{ steps.issue.outputs.author }} + ISSUE_HTML_URL: ${{ steps.issue.outputs.html_url }} + ISSUE_LABELS: ${{ steps.issue.outputs.labels }} + ISSUE_BODY: ${{ steps.issue.outputs.body }} + run: .venv/bin/python .github/scripts/analyze_issue.py diff --git a/examples/cards/src/main.py b/examples/cards/src/main.py index efee8699a..236f8c65e 100644 --- a/examples/cards/src/main.py +++ b/examples/cards/src/main.py @@ -8,10 +8,8 @@ from microsoft_teams.api import AdaptiveCardInvokeActivity, MessageActivity, MessageActivityInput from microsoft_teams.api.models.adaptive_card import ( - AdaptiveCardActionErrorResponse, AdaptiveCardActionMessageResponse, ) -from microsoft_teams.api.models.error import HttpError, InnerHttpError from microsoft_teams.api.models.invoke_response import AdaptiveCardInvokeResponse from microsoft_teams.apps import ActivityContext, App from microsoft_teams.cards import ( @@ -20,6 +18,7 @@ ExecuteAction, NumberInput, OpenUrlAction, + SubmitData, TextBlock, ToggleInput, ) @@ -29,15 +28,33 @@ def create_basic_adaptive_card() -> AdaptiveCard: - """Create a basic adaptive card for testing.""" + """Create a basic adaptive card for testing - uses ExecuteAction with specific action routing.""" card = AdaptiveCard( schema="http://adaptivecards.io/schemas/adaptive-card.json", body=[ - TextBlock(text="Hello world", wrap=True, weight="Bolder"), + TextBlock(text="Specific Action Routing", wrap=True, weight="Bolder"), ToggleInput(label="Notify me").with_id("notify"), ActionSet( actions=[ - ExecuteAction(title="Submit").with_data({"action": "submit_basic"}).with_associated_inputs("auto") + ExecuteAction(title="Submit").with_data(SubmitData("submit_basic")).with_associated_inputs("auto") + ] + ), + ], + ) + return card + + +def create_generic_execute_card() -> AdaptiveCard: + """Create a card with ExecuteAction that uses global handler (no specific action routing).""" + card = AdaptiveCard( + schema="http://adaptivecards.io/schemas/adaptive-card.json", + body=[ + TextBlock(text="Global Handler (No Action)", wrap=True, weight="Bolder"), + TextBlock(text="This card doesn't have a specific action handler", wrap=True), + ToggleInput(label="Enable feature").with_id("enabled"), + ActionSet( + actions=[ + ExecuteAction(title="Submit").with_data({"some_field": "some_value"}).with_associated_inputs("auto") ] ), ], @@ -110,7 +127,7 @@ def create_profile_card() -> AdaptiveCard: ActionSet( actions=[ ExecuteAction(title="Save") - .with_data({"action": "save_profile", "entity_id": "12345"}) + .with_data(SubmitData("save_profile", {"entity_id": "12345"})) .with_associated_inputs("auto"), OpenUrlAction(url="https://adaptivecards.microsoft.com").with_title("Learn More"), ] @@ -134,7 +151,9 @@ def create_profile_card_input_validation() -> AdaptiveCard: TextInput(id="location").with_label("Location"), ActionSet( actions=[ - ExecuteAction(title="Save").with_data({"action": "save_profile"}).with_associated_inputs("auto") + ExecuteAction(title="Save") + .with_data(SubmitData("save_profile")) + .with_associated_inputs("auto") ] ), ], @@ -156,7 +175,7 @@ def create_feedback_card() -> AdaptiveCard: ActionSet( actions=[ ExecuteAction(title="Submit Feedback") - .with_data({"action": "submit_feedback"}) + .with_data(SubmitData("submit_feedback")) .with_associated_inputs("auto") ] ), @@ -167,12 +186,20 @@ def create_feedback_card() -> AdaptiveCard: @app.on_message_pattern("card") async def handle_card_message(ctx: ActivityContext[MessageActivity]): - """Handle card request messages.""" - print(f"[CARD] Card requested by: {ctx.activity.from_}") + """Handle card request messages - specific action routing.""" + print(f"[CARD] Card with specific action routing requested by: {ctx.activity.from_}") card = create_basic_adaptive_card() await ctx.send(card) +@app.on_message_pattern("generic") +async def handle_generic_card_message(ctx: ActivityContext[MessageActivity]): + """Handle generic card request messages - global handler.""" + print(f"[GENERIC] Card with global handler requested by: {ctx.activity.from_}") + card = create_generic_execute_card() + await ctx.send(card) + + @app.on_message_pattern("json") async def handle_validate_card_message(ctx: ActivityContext[MessageActivity]): """Handle model validation card request messages.""" @@ -207,7 +234,7 @@ async def handle_form(ctx: ActivityContext[MessageActivity]): ActionSet( actions=[ ExecuteAction(title="Create Task") - .with_data({"action": "create_task"}) + .with_data(SubmitData("create_task")) .with_associated_inputs("auto") .with_style("positive") ] @@ -242,69 +269,84 @@ async def handle_feedback_card(ctx: ActivityContext[MessageActivity]): await ctx.send(card) -@app.on_card_action -async def handle_form_action(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse: - """Handle card action submissions from form example.""" +@app.on_card_action_execute +async def handle_all_execute_actions(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse: + """Handle all Action.Execute events without specific action routing (global handler).""" data = ctx.activity.value.action.data - if not data.get("action"): - print(ctx.activity) - return AdaptiveCardActionErrorResponse( - status_code=400, - type="application/vnd.microsoft.error", - value=HttpError( - code="BadRequest", - message="No action specified", - inner_http_error=InnerHttpError( - status_code=400, - body={"error": "No action specified"}, - ), - ), - ) - - print("Received action data:", data) - - if data["action"] == "submit_basic": - notify_value = data.get("notify", "false") - await ctx.send(f"Basic card submitted! Notify setting: {notify_value}") - elif data["action"] == "submit_feedback": - feedback_text = data.get("feedback", "No feedback provided") - await ctx.send(f"Feedback received: {feedback_text}") - elif data["action"] == "create_task": - title = data.get("title", "Untitled") - priority = data.get("priority", "medium") - due_date = data.get("due_date", "No date") - await ctx.send(f"Task created!\nTitle: {title}\nPriority: {priority}\nDue: {due_date}") - elif data["action"] == "save_profile": - entity_id = data.get("entity_id") - name = data.get("name", "Unknown") - email = data.get("email", "No email") - subscribe = data.get("subscribe", "false") - age = data.get("age") - location = data.get("location", "Not specified") - - response_text = f"Profile saved!\nName: {name}\nEmail: {email}\nSubscribed: {subscribe}" - if entity_id: - response_text += f"\nEntity ID: {entity_id}" - if age: - response_text += f"\nAge: {age}" - if location != "Not specified": - response_text += f"\nLocation: {location}" - - await ctx.send(response_text) - else: - return AdaptiveCardActionErrorResponse( - status_code=400, - type="application/vnd.microsoft.error", - value=HttpError( - code="BadRequest", - message="Unknown action", - inner_http_error=InnerHttpError( - status_code=400, - body={"error": "Unknown action"}, - ), - ), - ) + print(f"[GLOBAL HANDLER] Received Action.Execute data: {data}") + await ctx.send(f"Global handler processed Action.Execute. Data: {data}") + return AdaptiveCardActionMessageResponse( + status_code=200, + type="application/vnd.microsoft.activity.message", + value="Global handler processed action", + ) + +@app.on_card_action_execute("submit_basic") +async def handle_submit_basic(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse: + """Handle basic card submission - specific action routing.""" + data = ctx.activity.value.action.data + notify_value = data.get("notify", "false") + print(f"[SPECIFIC HANDLER] Received submit_basic action. Notify: {notify_value}") + await ctx.send(f"Specific handler: submit_basic. Notify setting: {notify_value}") + return AdaptiveCardActionMessageResponse( + status_code=200, + type="application/vnd.microsoft.activity.message", + value="Action processed successfully", + ) + + +@app.on_card_action_execute("submit_feedback") +async def handle_submit_feedback(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse: + """Handle feedback submission.""" + data = ctx.activity.value.action.data + print("Received submit_feedback action data:", data) + feedback_text = data.get("feedback", "No feedback provided") + await ctx.send(f"Feedback received: {feedback_text}") + return AdaptiveCardActionMessageResponse( + status_code=200, + type="application/vnd.microsoft.activity.message", + value="Action processed successfully", + ) + + +@app.on_card_action_execute("create_task") +async def handle_create_task(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse: + """Handle task creation.""" + data = ctx.activity.value.action.data + print("Received create_task action data:", data) + title = data.get("title", "Untitled") + priority = data.get("priority", "medium") + due_date = data.get("due_date", "No date") + await ctx.send(f"Task created!\nTitle: {title}\nPriority: {priority}\nDue: {due_date}") + return AdaptiveCardActionMessageResponse( + status_code=200, + type="application/vnd.microsoft.activity.message", + value="Action processed successfully", + ) + + +@app.on_card_action_execute("save_profile") +async def handle_save_profile(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse: + """Handle profile save.""" + data = ctx.activity.value.action.data + print("Received save_profile action data:", data) + entity_id = data.get("entity_id") + name = data.get("name", "Unknown") + email = data.get("email", "No email") + subscribe = data.get("subscribe", "false") + age = data.get("age") + location = data.get("location", "Not specified") + + response_text = f"Profile saved!\nName: {name}\nEmail: {email}\nSubscribed: {subscribe}" + if entity_id: + response_text += f"\nEntity ID: {entity_id}" + if age: + response_text += f"\nAge: {age}" + if location != "Not specified": + response_text += f"\nLocation: {location}" + + await ctx.send(response_text) return AdaptiveCardActionMessageResponse( status_code=200, type="application/vnd.microsoft.activity.message", diff --git a/examples/dialogs/src/main.py b/examples/dialogs/src/main.py index f5eafffaf..a16ebe913 100644 --- a/examples/dialogs/src/main.py +++ b/examples/dialogs/src/main.py @@ -11,7 +11,6 @@ from microsoft_teams.api import ( AdaptiveCardAttachment, CardTaskModuleTaskInfo, - InvokeResponse, MessageActivity, MessageActivityInput, TaskFetchInvokeActivity, @@ -24,7 +23,14 @@ ) 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.cards import ( + AdaptiveCard, + OpenDialogData, + SubmitAction, + SubmitData, + TextBlock, + TextInput, +) from microsoft_teams.common import ConsoleFormatter # Setup logging @@ -47,34 +53,15 @@ async def handle_message(ctx: ActivityContext[MessageActivity]) -> None: """Handle message activities and show dialog launcher card.""" - # Create the launcher adaptive card using Python objects to demonstrate SubmitActionData - # This tests that ms_teams correctly serializes to 'msteams' + # Create the launcher adaptive card with dialog buttons card = AdaptiveCard(version="1.4") card.body = [TextBlock(text="Select the examples you want to see!", size="Large", weight="Bolder")] - # Use SubmitActionData with ms_teams to test serialization - # SubmitActionData uses extra="allow" to accept custom fields - simple_form_data = SubmitActionData.model_validate({"opendialogtype": "simple_form"}) - simple_form_data.ms_teams = TaskFetchSubmitActionData().model_dump() - - webpage_data = SubmitActionData.model_validate({"opendialogtype": "webpage_dialog"}) - webpage_data.ms_teams = TaskFetchSubmitActionData().model_dump() - - multistep_data = SubmitActionData.model_validate({"opendialogtype": "multi_step_form"}) - multistep_data.ms_teams = TaskFetchSubmitActionData().model_dump() - + # Use OpenDialogData to create dialog open actions with clean API card.actions = [ - SubmitAction(title="Simple form test").with_data(simple_form_data), - SubmitAction(title="Webpage Dialog").with_data(webpage_data), - SubmitAction(title="Multi-step Form").with_data(multistep_data), - # Keep this one as JSON to show mixed usage - SubmitAction.model_validate( - { - "type": "Action.Submit", - "title": "Mixed Example (JSON)", - "data": {"msteams": {"type": "task/fetch"}, "opendialogtype": "mixed_example"}, - } - ), + SubmitAction(title="Simple form test").with_data(OpenDialogData("simple_form")), + SubmitAction(title="Webpage Dialog").with_data(OpenDialogData("webpage_dialog")), + SubmitAction(title="Multi-step Form").with_data(OpenDialogData("multi_step_form")), ] # Send the card as an attachment @@ -82,163 +69,134 @@ async def handle_message(ctx: ActivityContext[MessageActivity]) -> None: await ctx.send(message) -@app.on_dialog_open -async def handle_dialog_open(ctx: ActivityContext[TaskFetchInvokeActivity]): - """Handle dialog open events for all dialog types.""" - data: Optional[Any] = ctx.activity.value.data - dialog_type = data.get("opendialogtype") if data else None - - if dialog_type == "simple_form": - dialog_card = AdaptiveCard.model_validate( - { - "type": "AdaptiveCard", - "version": "1.4", - "body": [ - {"type": "TextBlock", "text": "This is a simple form", "size": "Large", "weight": "Bolder"}, - { - "type": "Input.Text", - "id": "name", - "label": "Name", - "placeholder": "Enter your name", - "isRequired": True, - }, - ], - "actions": [ - {"type": "Action.Submit", "title": "Submit", "data": {"submissiondialogtype": "simple_form"}} - ], - } - ) - - return InvokeResponse( - body=TaskModuleResponse( - task=TaskModuleContinueResponse( - value=CardTaskModuleTaskInfo( - title="Simple Form Dialog", - card=card_attachment(AdaptiveCardAttachment(content=dialog_card)), - ) - ) +@app.on_dialog_open("simple_form") +async def handle_simple_form_open(ctx: ActivityContext[TaskFetchInvokeActivity]): + """Handle simple form dialog open.""" + dialog_card = AdaptiveCard.model_validate( + { + "type": "AdaptiveCard", + "version": "1.4", + "body": [ + {"type": "TextBlock", "text": "This is a simple form", "size": "Large", "weight": "Bolder"}, + { + "type": "Input.Text", + "id": "name", + "label": "Name", + "placeholder": "Enter your name", + "isRequired": True, + }, + ], + "actions": [ + # Alternative: Use SubmitData for cleaner action-based routing + # SubmitAction(title="Submit").with_data(SubmitData("submit_simple_form")) + {"type": "Action.Submit", "title": "Submit", "data": {"action": "submit_simple_form"}} + ], + } + ) + + return TaskModuleResponse( + task=TaskModuleContinueResponse( + value=CardTaskModuleTaskInfo( + title="Simple Form Dialog", + card=card_attachment(AdaptiveCardAttachment(content=dialog_card)), ) ) - - elif dialog_type == "webpage_dialog": - return InvokeResponse( - body=TaskModuleResponse( - task=TaskModuleContinueResponse( - value=UrlTaskModuleTaskInfo( - title="Webpage Dialog", - url=f"{os.getenv('BOT_ENDPOINT', 'http://localhost:3978')}/tabs/dialog-form", - width=1000, - height=800, - ) - ) + ) + + +@app.on_dialog_open("webpage_dialog") +async def handle_webpage_dialog_open(ctx: ActivityContext[TaskFetchInvokeActivity]): + """Handle webpage dialog open.""" + return TaskModuleResponse( + task=TaskModuleContinueResponse( + value=UrlTaskModuleTaskInfo( + title="Webpage Dialog", + url=f"{os.getenv('BOT_ENDPOINT', 'http://localhost:3978')}/tabs/dialog-form", + width=1000, + height=800, ) ) - - elif dialog_type == "multi_step_form": - dialog_card = AdaptiveCard.model_validate( - { - "type": "AdaptiveCard", - "version": "1.4", - "body": [ - {"type": "TextBlock", "text": "This is a multi-step form", "size": "Large", "weight": "Bolder"}, - { - "type": "Input.Text", - "id": "name", - "label": "Name", - "placeholder": "Enter your name", - "isRequired": True, - }, - ], - "actions": [ - { - "type": "Action.Submit", - "title": "Submit", - "data": {"submissiondialogtype": "webpage_dialog_step_1"}, - } - ], - } + ) + + +@app.on_dialog_open("multi_step_form") +async def handle_multi_step_form_open(ctx: ActivityContext[TaskFetchInvokeActivity]): + """Handle multi-step form dialog open.""" + dialog_card = ( + AdaptiveCard() + .with_body( + [ + TextBlock(text="This is a multi-step form", size="Large", weight="Bolder"), + TextInput(id="name").with_label("Name").with_placeholder("Enter your name").with_is_required(True), + ] ) - - return InvokeResponse( - body=TaskModuleResponse( - task=TaskModuleContinueResponse( - value=CardTaskModuleTaskInfo( - title="Multi-step Form Dialog", - card=card_attachment(AdaptiveCardAttachment(content=dialog_card)), - ) - ) + .with_actions([SubmitAction(title="Submit").with_data(SubmitData("submit_multi_step_1"))]) + ) + + return TaskModuleResponse( + task=TaskModuleContinueResponse( + value=CardTaskModuleTaskInfo( + title="Multi-step Form Dialog", + card=card_attachment(AdaptiveCardAttachment(content=dialog_card)), ) ) + ) + - # Default return for unknown dialog types - return TaskModuleResponse(task=TaskModuleMessageResponse(value="Unknown dialog type")) +@app.on_dialog_submit("submit_simple_form") +async def handle_simple_form_submit(ctx: ActivityContext[TaskSubmitInvokeActivity]): + """Handle simple form submission.""" + data: Optional[Any] = ctx.activity.value.data + name = data.get("name") if data else None + await ctx.send(f"Hi {name}, thanks for submitting the form!") + return TaskModuleResponse(task=TaskModuleMessageResponse(value="Form was submitted")) -@app.on_dialog_submit -async def handle_dialog_submit(ctx: ActivityContext[TaskSubmitInvokeActivity]): - """Handle dialog submit events for all dialog types.""" +@app.on_dialog_submit("submit_webpage_dialog") +async def handle_webpage_dialog_submit(ctx: ActivityContext[TaskSubmitInvokeActivity]): + """Handle webpage dialog submission.""" data: Optional[Any] = ctx.activity.value.data - dialog_type = data.get("submissiondialogtype") if data else None - - if dialog_type == "simple_form": - name = data.get("name") if data else None - await ctx.send(f"Hi {name}, thanks for submitting the form!") - return TaskModuleResponse(task=TaskModuleMessageResponse(value="Form was submitted")) - - elif dialog_type == "webpage_dialog": - name = data.get("name") if data else None - email = data.get("email") if data else None - await ctx.send(f"Hi {name}, thanks for submitting the form! We got that your email is {email}") - return InvokeResponse( - body=TaskModuleResponse(task=TaskModuleMessageResponse(value="Form submitted successfully")) - ) + name = data.get("name") if data else None + email = data.get("email") if data else None + await ctx.send(f"Hi {name}, thanks for submitting the form! We got that your email is {email}") + return TaskModuleResponse(task=TaskModuleMessageResponse(value="Form submitted successfully")) - elif dialog_type == "webpage_dialog_step_1": - name = data.get("name") if data else None - next_step_card = AdaptiveCard.model_validate( - { - "type": "AdaptiveCard", - "version": "1.4", - "body": [ - {"type": "TextBlock", "text": "Email", "size": "Large", "weight": "Bolder"}, - { - "type": "Input.Text", - "id": "email", - "label": "Email", - "placeholder": "Enter your email", - "isRequired": True, - }, - ], - "actions": [ - { - "type": "Action.Submit", - "title": "Submit", - "data": {"submissiondialogtype": "webpage_dialog_step_2", "name": name}, - } - ], - } - ) - return InvokeResponse( - body=TaskModuleResponse( - task=TaskModuleContinueResponse( - value=CardTaskModuleTaskInfo( - title=f"Thanks {name} - Get Email", - card=card_attachment(AdaptiveCardAttachment(content=next_step_card)), - ) - ) +@app.on_dialog_submit("submit_multi_step_1") +async def handle_multi_step_1_submit(ctx: ActivityContext[TaskSubmitInvokeActivity]): + """Handle multi-step form step 1 submission.""" + data: Optional[Any] = ctx.activity.value.data + name = data.get("name") if data else None + + next_step_card = ( + AdaptiveCard() + .with_body( + [ + TextBlock(text="Email", size="Large", weight="Bolder"), + TextInput(id="email").with_label("Email").with_placeholder("Enter your email").with_is_required(True), + ] + ) + .with_actions([SubmitAction(title="Submit").with_data(SubmitData("submit_multi_step_2", {"name": name}))]) + ) + + return TaskModuleResponse( + task=TaskModuleContinueResponse( + value=CardTaskModuleTaskInfo( + title=f"Thanks {name} - Get Email", + card=card_attachment(AdaptiveCardAttachment(content=next_step_card)), ) ) + ) - elif dialog_type == "webpage_dialog_step_2": - name = data.get("name") if data else None - email = data.get("email") if data else None - await ctx.send(f"Hi {name}, thanks for submitting the form! We got that your email is {email}") - return InvokeResponse( - body=TaskModuleResponse(task=TaskModuleMessageResponse(value="Multi-step form completed successfully")) - ) - return TaskModuleResponse(task=TaskModuleMessageResponse(value="Unknown submission type")) +@app.on_dialog_submit("submit_multi_step_2") +async def handle_multi_step_2_submit(ctx: ActivityContext[TaskSubmitInvokeActivity]): + """Handle multi-step form step 2 submission.""" + data: Optional[Any] = ctx.activity.value.data + name = data.get("name") if data else None + email = data.get("email") if data else None + await ctx.send(f"Hi {name}, thanks for submitting the form! We got that your email is {email}") + return TaskModuleResponse(task=TaskModuleMessageResponse(value="Multi-step form completed successfully")) @app.event("error") diff --git a/examples/tab/Web/package-lock.json b/examples/tab/Web/package-lock.json index 7a4811e89..969cfa175 100644 --- a/examples/tab/Web/package-lock.json +++ b/examples/tab/Web/package-lock.json @@ -2085,10 +2085,11 @@ "dev": true }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, diff --git a/examples/targeted-messages/src/main.py b/examples/targeted-messages/src/main.py index 443c94bb0..f2e3de26f 100644 --- a/examples/targeted-messages/src/main.py +++ b/examples/targeted-messages/src/main.py @@ -38,7 +38,7 @@ async def handle_message(ctx: ActivityContext[MessageActivity]): targeted_message = MessageActivityInput( text="🔒 [SEND] This is a targeted message - only YOU can see this!" - ).with_recipient(Account(id=member.id, name=member.name, role="user"), is_targeted=True) + ).with_recipient(Account(id=member.id, name=member.name), is_targeted=True) result = await ctx.send(targeted_message) print("[SEND] Sent targeted message") diff --git a/packages/api/src/microsoft_teams/api/activities/__init__.py b/packages/api/src/microsoft_teams/api/activities/__init__.py index 224ea6ada..be3f19c38 100644 --- a/packages/api/src/microsoft_teams/api/activities/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/__init__.py @@ -15,8 +15,6 @@ ConversationChannelData, ConversationEventType, ConversationUpdateActivity, - EndOfConversationActivity, - EndOfConversationCode, ) from .event import * # noqa: F403 from .event import EventActivity @@ -62,8 +60,6 @@ "ConversationActivity", "ConversationUpdateActivity", "ConversationChannelData", - "EndOfConversationActivity", - "EndOfConversationCode", "EventActivity", "HandoffActivity", "InstallUpdateActivity", diff --git a/packages/api/src/microsoft_teams/api/activities/activity_params.py b/packages/api/src/microsoft_teams/api/activities/activity_params.py index 9fd70f0b9..a566f2635 100644 --- a/packages/api/src/microsoft_teams/api/activities/activity_params.py +++ b/packages/api/src/microsoft_teams/api/activities/activity_params.py @@ -9,7 +9,7 @@ from pydantic import Field from .command import CommandResultActivityInput, CommandSendActivityInput -from .conversation import ConversationUpdateActivityInput, EndOfConversationActivityInput +from .conversation import ConversationUpdateActivityInput from .handoff import HandoffActivityInput from .message import ( MessageActivityInput, @@ -24,7 +24,6 @@ Union[ # Simple activities ConversationUpdateActivityInput, - EndOfConversationActivityInput, HandoffActivityInput, TraceActivityInput, TypingActivityInput, diff --git a/packages/api/src/microsoft_teams/api/activities/command/command_result.py b/packages/api/src/microsoft_teams/api/activities/command/command_result.py index 3e0396056..5d32ceeba 100644 --- a/packages/api/src/microsoft_teams/api/activities/command/command_result.py +++ b/packages/api/src/microsoft_teams/api/activities/command/command_result.py @@ -24,7 +24,7 @@ class CommandResultValue(CustomBaseModel): as defined by the name. The value of the data field is a complex type. """ - error: Optional[Exception] = None + error: Optional[Any] = None """The optional error, if the command result indicates a failure.""" diff --git a/packages/api/src/microsoft_teams/api/activities/conversation/__init__.py b/packages/api/src/microsoft_teams/api/activities/conversation/__init__.py index 41100f922..d6aa67bec 100644 --- a/packages/api/src/microsoft_teams/api/activities/conversation/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/conversation/__init__.py @@ -3,29 +3,19 @@ Licensed under the MIT License. """ -from typing import Annotated, Union - -from pydantic import Field - from .conversation_update import ( ConversationChannelData, ConversationEventType, ConversationUpdateActivity, ConversationUpdateActivityInput, ) -from .end_of_conversation import EndOfConversationActivity, EndOfConversationActivityInput, EndOfConversationCode -ConversationActivity = Annotated[ - Union[ConversationUpdateActivity, EndOfConversationActivity], Field(discriminator="type") -] +ConversationActivity = ConversationUpdateActivity __all__ = [ "ConversationEventType", "ConversationChannelData", "ConversationUpdateActivity", "ConversationUpdateActivityInput", - "EndOfConversationCode", - "EndOfConversationActivity", - "EndOfConversationActivityInput", "ConversationActivity", ] diff --git a/packages/api/src/microsoft_teams/api/activities/conversation/conversation_update.py b/packages/api/src/microsoft_teams/api/activities/conversation/conversation_update.py index 7464b7470..c13aa3382 100644 --- a/packages/api/src/microsoft_teams/api/activities/conversation/conversation_update.py +++ b/packages/api/src/microsoft_teams/api/activities/conversation/conversation_update.py @@ -44,9 +44,6 @@ class _ConversationUpdateBase(CustomBaseModel): topic_name: Optional[str] = None """The updated topic name of the conversation.""" - history_disclosed: Optional[bool] = None - """Indicates whether the prior history of the channel is disclosed.""" - channel_data: Optional[ConversationChannelData] = None """Channel data with event type information.""" diff --git a/packages/api/src/microsoft_teams/api/activities/conversation/end_of_conversation.py b/packages/api/src/microsoft_teams/api/activities/conversation/end_of_conversation.py deleted file mode 100644 index c9856049a..000000000 --- a/packages/api/src/microsoft_teams/api/activities/conversation/end_of_conversation.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -from typing import Literal, Optional - -from ...models import ActivityBase, ActivityInputBase, CustomBaseModel - -EndOfConversationCode = Literal[ - "unknown", "completedSuccessfully", "userCancelled", "botTimedOut", "botIssuedInvalidMessage", "channelFailed" -] - - -class _EndOfConversationBase(CustomBaseModel): - """Base class containing shared end of conversation activity fields (all Optional except type).""" - - type: Literal["endOfConversation"] = "endOfConversation" - - code: Optional[EndOfConversationCode] = None - """ - The code for endOfConversation activities that indicates why the conversation ended. - Possible values include: 'unknown', 'completedSuccessfully', 'userCancelled', 'botTimedOut', - 'botIssuedInvalidMessage', 'channelFailed' - """ - - text: Optional[str] = None - """The text content of the message.""" - - -class EndOfConversationActivity(_EndOfConversationBase, ActivityBase): - """Output model for received end of conversation activities with required fields and read-only properties.""" - - text: str # pyright: ignore [reportGeneralTypeIssues] - """The text content of the message.""" - - -class EndOfConversationActivityInput(_EndOfConversationBase, ActivityInputBase): - """Input model for creating end of conversation activities with builder methods.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/config/config_fetch.py b/packages/api/src/microsoft_teams/api/activities/invoke/config/config_fetch.py index fbed6fb21..011e24ebb 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/config/config_fetch.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/config/config_fetch.py @@ -3,9 +3,9 @@ Licensed under the MIT License. """ -from typing import Any, Literal, Optional +from typing import Any, Literal -from ....models import ConversationReference, CustomBaseModel +from ....models import CustomBaseModel from ...invoke_activity import InvokeActivity @@ -21,6 +21,3 @@ class ConfigFetchInvokeActivity(InvokeActivity, CustomBaseModel): value: Any """The value associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/config/config_submit.py b/packages/api/src/microsoft_teams/api/activities/invoke/config/config_submit.py index 383dc3eaa..0dafffbe5 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/config/config_submit.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/config/config_submit.py @@ -3,9 +3,8 @@ Licensed under the MIT License. """ -from typing import Any, Literal, Optional +from typing import Any, Literal -from ....models import ConversationReference from ...invoke_activity import InvokeActivity @@ -21,6 +20,3 @@ class ConfigSubmitInvokeActivity(InvokeActivity): value: Any """The value associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/execute_action.py b/packages/api/src/microsoft_teams/api/activities/invoke/execute_action.py index b928752c4..f9ad3299f 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/execute_action.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/execute_action.py @@ -3,9 +3,9 @@ Licensed under the MIT License. """ -from typing import Literal, Optional +from typing import Literal -from ...models import ConversationReference, O365ConnectorCardActionQuery +from ...models import O365ConnectorCardActionQuery from ..invoke_activity import InvokeActivity @@ -22,6 +22,3 @@ class ExecuteActionInvokeActivity(InvokeActivity): value: O365ConnectorCardActionQuery """A value that is associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/file_consent.py b/packages/api/src/microsoft_teams/api/activities/invoke/file_consent.py index ff0e9e72f..a04f9f7ed 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/file_consent.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/file_consent.py @@ -3,9 +3,9 @@ Licensed under the MIT License. """ -from typing import Literal, Optional +from typing import Literal -from ...models import ConversationReference, FileConsentCardResponse +from ...models import FileConsentCardResponse from ..invoke_activity import InvokeActivity @@ -22,6 +22,3 @@ class FileConsentInvokeActivity(InvokeActivity): value: FileConsentCardResponse """A value that is associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/handoff_action.py b/packages/api/src/microsoft_teams/api/activities/invoke/handoff_action.py index ec79b073d..1e8d01671 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/handoff_action.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/handoff_action.py @@ -3,9 +3,9 @@ Licensed under the MIT License. """ -from typing import Literal, Optional +from typing import Literal -from ...models import ConversationReference, CustomBaseModel +from ...models import CustomBaseModel from ..invoke_activity import InvokeActivity @@ -29,6 +29,3 @@ class HandoffActionInvokeActivity(InvokeActivity): value: HandoffActionValue """A value that is associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/message/submit_action.py b/packages/api/src/microsoft_teams/api/activities/invoke/message/submit_action.py index 2519c3021..a6872a935 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/message/submit_action.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/message/submit_action.py @@ -3,9 +3,9 @@ Licensed under the MIT License. """ -from typing import Literal, Optional +from typing import Literal -from ....models import ConversationReference, CustomBaseModel +from ....models import CustomBaseModel from ...invoke_activity import InvokeActivity @@ -39,6 +39,3 @@ class MessageSubmitActionInvokeActivity(InvokeActivity): value: MessageSubmitActionInvokeValue """The value associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/anon_query_link.py b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/anon_query_link.py index 90ec593f7..29936193b 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/anon_query_link.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/anon_query_link.py @@ -3,9 +3,9 @@ Licensed under the MIT License. """ -from typing import Literal, Optional +from typing import Literal -from ....models import AppBasedLinkQuery, ConversationReference +from ....models import AppBasedLinkQuery from ...invoke_activity import InvokeActivity @@ -22,6 +22,3 @@ class MessageExtensionAnonQueryLinkInvokeActivity(InvokeActivity): value: AppBasedLinkQuery """A value that is associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/card_button_clicked.py b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/card_button_clicked.py index 46714c335..a70203058 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/card_button_clicked.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/card_button_clicked.py @@ -3,9 +3,8 @@ Licensed under the MIT License. """ -from typing import Any, Literal, Optional +from typing import Any, Literal -from ....models import ConversationReference from ...invoke_activity import InvokeActivity @@ -22,6 +21,3 @@ class MessageExtensionCardButtonClickedInvokeActivity(InvokeActivity): value: Any """A value that is associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/fetch_task.py b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/fetch_task.py index a63cb622c..883337f72 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/fetch_task.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/fetch_task.py @@ -3,9 +3,9 @@ Licensed under the MIT License. """ -from typing import Literal, Optional +from typing import Literal -from ....models import ConversationReference, MessagingExtensionAction +from ....models import MessagingExtensionAction from ...invoke_activity import InvokeActivity @@ -22,6 +22,3 @@ class MessageExtensionFetchTaskInvokeActivity(InvokeActivity): value: MessagingExtensionAction """A value that is associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/query.py b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/query.py index a8b3796c0..86a4fc559 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/query.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/query.py @@ -3,9 +3,9 @@ Licensed under the MIT License. """ -from typing import Literal, Optional +from typing import Literal -from ....models import ConversationReference, MessagingExtensionQuery +from ....models import MessagingExtensionQuery from ...invoke_activity import InvokeActivity @@ -22,6 +22,3 @@ class MessageExtensionQueryInvokeActivity(InvokeActivity): value: MessagingExtensionQuery """A value that is associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/query_link.py b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/query_link.py index dba74c86b..0e04ce57c 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/query_link.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/query_link.py @@ -3,9 +3,9 @@ Licensed under the MIT License. """ -from typing import Literal, Optional +from typing import Literal -from ....models import AppBasedLinkQuery, ConversationReference +from ....models import AppBasedLinkQuery from ...invoke_activity import InvokeActivity @@ -22,6 +22,3 @@ class MessageExtensionQueryLinkInvokeActivity(InvokeActivity): value: AppBasedLinkQuery """A value that is associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/query_setting_url.py b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/query_setting_url.py index e86cc98d7..11a162721 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/query_setting_url.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/query_setting_url.py @@ -3,9 +3,9 @@ Licensed under the MIT License. """ -from typing import Literal, Optional +from typing import Literal -from ....models import ConversationReference, MessagingExtensionQuery +from ....models import MessagingExtensionQuery from ...invoke_activity import InvokeActivity @@ -22,6 +22,3 @@ class MessageExtensionQuerySettingUrlInvokeActivity(InvokeActivity): value: MessagingExtensionQuery """A value that is associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/select_item.py b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/select_item.py index a4ecd4d55..da509078e 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/select_item.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/select_item.py @@ -3,9 +3,8 @@ Licensed under the MIT License. """ -from typing import Any, Literal, Optional +from typing import Any, Literal -from ....models import ConversationReference from ...invoke_activity import InvokeActivity @@ -22,6 +21,3 @@ class MessageExtensionSelectItemInvokeActivity(InvokeActivity): value: Any """A value that is associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/setting.py b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/setting.py index 48dee6ffb..e1064392a 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/setting.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/setting.py @@ -3,9 +3,9 @@ Licensed under the MIT License. """ -from typing import Literal, Optional +from typing import Literal -from ....models import ConversationReference, MessagingExtensionQuery +from ....models import MessagingExtensionQuery from ...invoke_activity import InvokeActivity @@ -22,6 +22,3 @@ class MessageExtensionSettingInvokeActivity(InvokeActivity): value: MessagingExtensionQuery """A value that is associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/submit_action.py b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/submit_action.py index 51a5c83f6..02e75dbfa 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/submit_action.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/message_extension/submit_action.py @@ -3,9 +3,9 @@ Licensed under the MIT License. """ -from typing import Literal, Optional +from typing import Literal -from ....models import ConversationReference, MessagingExtensionAction +from ....models import MessagingExtensionAction from ...invoke_activity import InvokeActivity @@ -22,6 +22,3 @@ class MessageExtensionSubmitActionInvokeActivity(InvokeActivity): value: MessagingExtensionAction """A value that is associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/tab/tab_fetch.py b/packages/api/src/microsoft_teams/api/activities/invoke/tab/tab_fetch.py index 1a237e75e..6bf1717bb 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/tab/tab_fetch.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/tab/tab_fetch.py @@ -3,9 +3,9 @@ Licensed under the MIT License. """ -from typing import Literal, Optional +from typing import Literal -from ....models import ConversationReference, TabRequest +from ....models import TabRequest from ...invoke_activity import InvokeActivity @@ -22,6 +22,3 @@ class TabFetchInvokeActivity(InvokeActivity): value: TabRequest """A value that is associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/tab/tab_submit.py b/packages/api/src/microsoft_teams/api/activities/invoke/tab/tab_submit.py index 55aa63d3d..a67426bd7 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/tab/tab_submit.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/tab/tab_submit.py @@ -3,9 +3,9 @@ Licensed under the MIT License. """ -from typing import Literal, Optional +from typing import Literal -from ....models import ConversationReference, TabRequest +from ....models import TabRequest from ...invoke_activity import InvokeActivity @@ -22,6 +22,3 @@ class TabSubmitInvokeActivity(InvokeActivity): value: TabRequest """A value that is associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/task/task_fetch.py b/packages/api/src/microsoft_teams/api/activities/invoke/task/task_fetch.py index 8888a15aa..a1b5b3c61 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/task/task_fetch.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/task/task_fetch.py @@ -3,9 +3,9 @@ Licensed under the MIT License. """ -from typing import Literal, Optional +from typing import Literal -from ....models import ConversationReference, TaskModuleRequest +from ....models import TaskModuleRequest from ...invoke_activity import InvokeActivity @@ -22,6 +22,3 @@ class TaskFetchInvokeActivity(InvokeActivity): value: TaskModuleRequest """A value that is associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/invoke/task/task_submit.py b/packages/api/src/microsoft_teams/api/activities/invoke/task/task_submit.py index bf9416ed3..5e1b90a11 100644 --- a/packages/api/src/microsoft_teams/api/activities/invoke/task/task_submit.py +++ b/packages/api/src/microsoft_teams/api/activities/invoke/task/task_submit.py @@ -3,9 +3,9 @@ Licensed under the MIT License. """ -from typing import Literal, Optional +from typing import Literal -from ....models import ConversationReference, TaskModuleRequest +from ....models import TaskModuleRequest from ...invoke_activity import InvokeActivity @@ -22,6 +22,3 @@ class TaskSubmitInvokeActivity(InvokeActivity): value: TaskModuleRequest """A value that is associated with the activity.""" - - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" diff --git a/packages/api/src/microsoft_teams/api/activities/message/message.py b/packages/api/src/microsoft_teams/api/activities/message/message.py index 1b5111a0c..41cac9705 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/message.py +++ b/packages/api/src/microsoft_teams/api/activities/message/message.py @@ -3,7 +3,6 @@ Licensed under the MIT License. """ -from datetime import datetime from typing import Any, List, Literal, Optional, Self from microsoft_teams.cards import AdaptiveCard @@ -18,8 +17,6 @@ ChannelData, CustomBaseModel, DeliveryMode, - Importance, - InputHint, MentionEntity, StreamInfoEntity, SuggestedActions, @@ -36,15 +33,6 @@ class _MessageBase(CustomBaseModel): text: Optional[str] = None """The text content of the message.""" - speak: Optional[str] = None - """The text to speak.""" - - input_hint: Optional[InputHint] = None - """ - Indicates whether your bot is accepting, expecting, or ignoring user input - after the message is delivered to the client. - """ - summary: Optional[str] = None """The text to display if the channel cannot render cards.""" @@ -60,18 +48,9 @@ class _MessageBase(CustomBaseModel): suggested_actions: Optional[SuggestedActions] = None """The suggested actions for the activity.""" - importance: Optional[Importance] = None - """The importance of the activity.""" - delivery_mode: Optional[DeliveryMode] = None """A delivery hint to signal to the recipient alternate delivery paths for the activity.""" - expiration: Optional[datetime] = None - """ - The time at which the activity should be considered to be "expired" - and should not be presented to the recipient. - """ - value: Optional[Any] = None """A value that is associated with the activity.""" @@ -151,32 +130,6 @@ def with_text(self, text: str) -> Self: self.text = text return self - def with_speak(self, speak: str) -> Self: - """ - Set the text to speak. - - Args: - speak: Text to speak - - Returns: - Self for method chaining - """ - self.speak = speak - return self - - def with_input_hint(self, input_hint: InputHint) -> Self: - """ - Set the input hint. - - Args: - input_hint: Input hint value - - Returns: - Self for method chaining - """ - self.input_hint = input_hint - return self - def with_summary(self, summary: str) -> Self: """ Set the text to display if the channel cannot render cards. @@ -229,19 +182,6 @@ def with_suggested_actions(self, suggested_actions: SuggestedActions) -> Self: self.suggested_actions = suggested_actions return self - def with_importance(self, importance: Importance) -> Self: - """ - Set the importance of the activity. - - Args: - importance: Importance (low, normal, high) - - Returns: - Self for method chaining - """ - self.importance = importance - return self - def with_delivery_mode(self, delivery_mode: DeliveryMode) -> Self: """ Set the delivery mode for the activity. @@ -255,19 +195,6 @@ def with_delivery_mode(self, delivery_mode: DeliveryMode) -> Self: self.delivery_mode = delivery_mode return self - def with_expiration(self, expiration: datetime) -> Self: - """ - Set the expiration time for the activity. - - Args: - expiration: Expiration datetime - - Returns: - Self for method chaining - """ - self.expiration = expiration - return self - def add_text(self, text: str) -> Self: """ Append text to the message. diff --git a/packages/api/src/microsoft_teams/api/activities/trace.py b/packages/api/src/microsoft_teams/api/activities/trace.py index 7de8550ef..ff12876ac 100644 --- a/packages/api/src/microsoft_teams/api/activities/trace.py +++ b/packages/api/src/microsoft_teams/api/activities/trace.py @@ -5,7 +5,7 @@ from typing import Any, Literal, Optional -from ..models import ActivityBase, ActivityInputBase, ConversationReference, CustomBaseModel +from ..models import ActivityBase, ActivityInputBase, CustomBaseModel class _TraceBase(CustomBaseModel): @@ -33,11 +33,6 @@ class _TraceBase(CustomBaseModel): A value that is associated with the activity. """ - relates_to: Optional[ConversationReference] = None - """ - A reference to another conversation or activity. - """ - class TraceActivity(_TraceBase, ActivityBase): """Output model for received trace activities with required fields and read-only properties.""" 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 dc421bc96..8dba14686 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/__init__.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/__init__.py @@ -6,13 +6,11 @@ from .activity import ConversationActivityClient from .client import ConversationClient from .member import ConversationMemberClient -from .params import CreateConversationParams, GetConversationsParams, GetConversationsResponse +from .params import CreateConversationParams __all__ = [ "ConversationActivityClient", "ConversationClient", "ConversationMemberClient", "CreateConversationParams", - "GetConversationsParams", - "GetConversationsResponse", ] diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/activity.py b/packages/api/src/microsoft_teams/api/clients/conversation/activity.py index e72d11048..a1ee86ef8 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/activity.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/activity.py @@ -9,7 +9,7 @@ from microsoft_teams.common.http import Client from ...activities import ActivityParams, SentActivity -from ...models import Account +from ...models import TeamsChannelAccount from ..api_client_settings import ApiClientSettings from ..base_client import BaseClient @@ -111,7 +111,7 @@ async def delete(self, conversation_id: str, activity_id: str) -> None: """ 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]: + async def get_members(self, conversation_id: str, activity_id: str) -> List[TeamsChannelAccount]: """ Get the members associated with an activity. @@ -120,12 +120,12 @@ async def get_members(self, conversation_id: str, activity_id: str) -> List[Acco activity_id: The ID of the activity Returns: - List of Account objects representing the activity members + List of TeamsChannelAccount 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()] + return [TeamsChannelAccount.model_validate(member) for member in response.json()] @experimental("ExperimentalTeamsTargeted") async def create_targeted(self, conversation_id: str, activity: ActivityParams) -> SentActivity: 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 7ad5aa015..1c42a103b 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/client.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/client.py @@ -3,7 +3,7 @@ Licensed under the MIT License. """ -from typing import Dict, Optional, Union +from typing import Optional, Union from microsoft_teams.common.http import Client, ClientOptions @@ -12,11 +12,7 @@ from ..base_client import BaseClient from .activity import ActivityParams, ConversationActivityClient from .member import ConversationMemberClient -from .params import ( - CreateConversationParams, - GetConversationsParams, - GetConversationsResponse, -) +from .params import CreateConversationParams class ConversationOperations: @@ -64,12 +60,12 @@ class MemberOperations(ConversationOperations): async def get_all(self): return await self._client.members_client.get(self._conversation_id) + async def get_paged(self, page_size: Optional[int] = None, continuation_token: Optional[str] = None): + return await self._client.members_client.get_paged(self._conversation_id, page_size, continuation_token) + async def get(self, member_id: str): return await self._client.members_client.get_by_id(self._conversation_id, member_id) - async def delete(self, member_id: str) -> None: - await self._client.members_client.delete(self._conversation_id, member_id) - class ConversationClient(BaseClient): """Client for managing Teams conversations.""" @@ -137,25 +133,6 @@ def members(self, conversation_id: str) -> MemberOperations: """ 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: Dict[str, str] = {} - 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. 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 f46692d31..78a7a958a 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/member.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/member.py @@ -8,6 +8,7 @@ from microsoft_teams.common.http import Client from ...models import TeamsChannelAccount +from ...models.conversation import PagedMembersResult from ..api_client_settings import ApiClientSettings from ..base_client import BaseClient @@ -47,6 +48,28 @@ async def get(self, conversation_id: str) -> List[TeamsChannelAccount]: response = await self.http.get(f"{self.service_url}/v3/conversations/{conversation_id}/members") return [TeamsChannelAccount.model_validate(member) for member in response.json()] + async def get_paged( + self, + conversation_id: str, + page_size: Optional[int] = None, + continuation_token: Optional[str] = None, + ) -> PagedMembersResult: + """ + Get a page of members in a conversation. + + Args: + conversation_id: The ID of the conversation. + page_size: Optional maximum number of members to return per page. + continuation_token: Optional token from a previous call to fetch the next page. + + Returns: + PagedMembersResult containing the members and an optional continuation token + for fetching subsequent pages. + """ + url = f"{self.service_url}/v3/conversations/{conversation_id}/pagedMembers" + response = await self.http.get(url, params={"pageSize": page_size, "continuationToken": continuation_token}) + return PagedMembersResult.model_validate(response.json()) + async def get_by_id(self, conversation_id: str, member_id: str) -> TeamsChannelAccount: """ Get a specific member in a conversation. @@ -60,13 +83,3 @@ async def get_by_id(self, conversation_id: str, member_id: str) -> TeamsChannelA """ response = await self.http.get(f"{self.service_url}/v3/conversations/{conversation_id}/members/{member_id}") return TeamsChannelAccount.model_validate(response.json()) - - async def delete(self, conversation_id: str, member_id: str) -> None: - """ - Remove a member from a conversation. - - Args: - conversation_id: The ID of the conversation - member_id: The ID of the member to remove - """ - await self.http.delete(f"{self.service_url}/v3/conversations/{conversation_id}/members/{member_id}") diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/params.py b/packages/api/src/microsoft_teams/api/clients/conversation/params.py index c1f3c3809..3d35407c0 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/params.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/params.py @@ -5,35 +5,17 @@ from typing import Any, Dict, List, Optional -from ...models import Account, Conversation, CustomBaseModel +from ...models import Account, CustomBaseModel from .activity import ActivityParams -class GetConversationsParams(CustomBaseModel): - """Parameters for getting conversations.""" - - continuation_token: Optional[str] = None - - class CreateConversationParams(CustomBaseModel): """Parameters for creating a conversation.""" - 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. @@ -46,16 +28,3 @@ class CreateConversationParams(CustomBaseModel): """ The channel-specific data for the conversation. """ - - -class GetConversationsResponse(CustomBaseModel): - """Response from getting conversations.""" - - continuation_token: Optional[str] = None - """ - Token for getting the next page of conversations. - """ - conversations: List[Conversation] = [] - """ - List of conversations. - """ diff --git a/packages/api/src/microsoft_teams/api/clients/meeting/client.py b/packages/api/src/microsoft_teams/api/clients/meeting/client.py index 7b467fbd3..cea7277b9 100644 --- a/packages/api/src/microsoft_teams/api/clients/meeting/client.py +++ b/packages/api/src/microsoft_teams/api/clients/meeting/client.py @@ -8,6 +8,7 @@ from microsoft_teams.common.http import Client, ClientOptions from ...models import MeetingInfo, MeetingParticipant +from ...models.meetings.meeting_notification import MeetingNotificationParams, MeetingNotificationResponse from ..api_client_settings import ApiClientSettings from ..base_client import BaseClient @@ -57,7 +58,31 @@ async def get_participant(self, meeting_id: str, id: str, tenant_id: str) -> Mee Returns: MeetingParticipant: The meeting participant information. """ - url = f"{self.service_url}/v1/meetings/{meeting_id}/participants/{id}?tenantId={tenant_id}" response = await self.http.get(url) return MeetingParticipant.model_validate(response.json()) + + async def send_notification( + self, meeting_id: str, params: MeetingNotificationParams + ) -> Optional[MeetingNotificationResponse]: + """ + Send a targeted meeting notification to participants. + + Returns None on full success (HTTP 202). Returns a MeetingNotificationResponse + with failure details on partial success (HTTP 207). + + Args: + meeting_id: The BASE64-encoded meeting ID. + params: The notification parameters including recipients and surfaces. + + Returns: + None if all notifications were sent successfully, or a MeetingNotificationResponse + with per-recipient failure details on partial success. + """ + response = await self.http.post( + f"{self.service_url}/v1/meetings/{meeting_id}/notification", + json=params.model_dump(by_alias=True, exclude_none=True), + ) + if not response.text: + return None + return MeetingNotificationResponse.model_validate(response.json()) diff --git a/packages/api/src/microsoft_teams/api/clients/team/__init__.py b/packages/api/src/microsoft_teams/api/clients/team/__init__.py index 730d78cac..f05df8a28 100644 --- a/packages/api/src/microsoft_teams/api/clients/team/__init__.py +++ b/packages/api/src/microsoft_teams/api/clients/team/__init__.py @@ -4,5 +4,6 @@ """ from .client import TeamClient +from .params import GetTeamConversationsResponse -__all__ = ["TeamClient"] +__all__ = ["GetTeamConversationsResponse", "TeamClient"] diff --git a/packages/api/src/microsoft_teams/api/clients/team/client.py b/packages/api/src/microsoft_teams/api/clients/team/client.py index ddda3b10c..7ffca2e92 100644 --- a/packages/api/src/microsoft_teams/api/clients/team/client.py +++ b/packages/api/src/microsoft_teams/api/clients/team/client.py @@ -10,6 +10,7 @@ from ...models import ChannelInfo, TeamDetails from ..api_client_settings import ApiClientSettings from ..base_client import BaseClient +from .params import GetTeamConversationsResponse class TeamClient(BaseClient): @@ -56,4 +57,4 @@ async def get_conversations(self, id: str) -> List[ChannelInfo]: List of channel information. """ response = await self.http.get(f"{self.service_url}/v3/teams/{id}/conversations") - return [ChannelInfo.model_validate(channel) for channel in response.json()] + return GetTeamConversationsResponse.model_validate(response.json()).conversations diff --git a/packages/api/src/microsoft_teams/api/clients/team/params.py b/packages/api/src/microsoft_teams/api/clients/team/params.py new file mode 100644 index 000000000..23a038ebb --- /dev/null +++ b/packages/api/src/microsoft_teams/api/clients/team/params.py @@ -0,0 +1,15 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import List + +from ...models import ChannelInfo, CustomBaseModel + + +class GetTeamConversationsResponse(CustomBaseModel): + """Response model for getting team conversations.""" + + conversations: List[ChannelInfo] = [] + """List of conversations in the team.""" diff --git a/packages/api/src/microsoft_teams/api/models/__init__.py b/packages/api/src/microsoft_teams/api/models/__init__.py index 1b9062d50..df7e229fd 100644 --- a/packages/api/src/microsoft_teams/api/models/__init__.py +++ b/packages/api/src/microsoft_teams/api/models/__init__.py @@ -22,7 +22,7 @@ token, token_exchange, ) -from .account import Account, AccountRole, ConversationAccount, TeamsChannelAccount +from .account import Account, AccountType, ConversationAccount, TeamsChannelAccount from .action import Action from .activity import Activity as ActivityBase from .activity import ActivityInput as ActivityInputBase @@ -71,7 +71,7 @@ # Combine all exports from submodules __all__: list[str] = [ "Account", - "AccountRole", + "AccountType", "Action", "ActivityBase", "ActivityInputBase", diff --git a/packages/api/src/microsoft_teams/api/models/account.py b/packages/api/src/microsoft_teams/api/models/account.py index 5f3100c52..9c58c8f2b 100644 --- a/packages/api/src/microsoft_teams/api/models/account.py +++ b/packages/api/src/microsoft_teams/api/models/account.py @@ -5,9 +5,11 @@ from typing import Any, Dict, Literal, Optional +from pydantic import AliasChoices, Field + from .custom_base_model import CustomBaseModel -AccountRole = Literal["user", "bot"] +AccountType = Literal["person", "tag", "channel", "team", "bot"] class Account(CustomBaseModel): @@ -23,9 +25,9 @@ class Account(CustomBaseModel): """ The Azure AD object ID. """ - role: Optional[AccountRole] = None + type: Optional[AccountType] = None """ - The role of the account in the conversation. + The type of account. """ properties: Optional[Dict[str, Any]] = None """ @@ -59,13 +61,17 @@ class TeamsChannelAccount(CustomBaseModel): """ Display-friendly name of the user or bot. """ - aad_object_id: Optional[str] = None + aad_object_id: Optional[str] = Field( + default=None, + validation_alias=AliasChoices("aadObjectId", "objectId"), + serialization_alias="aadObjectId", + ) """ The user's Object ID in Azure Active Directory (AAD). """ - role: Optional[AccountRole] = None + user_role: Optional[str] = None """ - Role of the user (e.g., 'user' or 'bot'). + Role of the user in the conversation. """ given_name: Optional[str] = None """ diff --git a/packages/api/src/microsoft_teams/api/models/activity.py b/packages/api/src/microsoft_teams/api/models/activity.py index e021f779d..68db57bda 100644 --- a/packages/api/src/microsoft_teams/api/models/activity.py +++ b/packages/api/src/microsoft_teams/api/models/activity.py @@ -13,7 +13,6 @@ from microsoft_teams.api.models.channel_data.notification_info import NotificationInfo from microsoft_teams.api.models.channel_data.team_info import TeamInfo from microsoft_teams.api.models.channel_id import ChannelID -from microsoft_teams.api.models.conversation.conversation_reference import ConversationReference from microsoft_teams.api.models.entity.ai_message_entity import AIMessageEntity from microsoft_teams.api.models.entity.citation_entity import ( Appearance, @@ -62,9 +61,6 @@ class _ActivityBase(CustomBaseModel): conversation: ConversationAccount """Identifies the conversation to which the activity belongs.""" - relates_to: Optional[ConversationReference] = None - """A reference to another conversation or activity.""" - reply_to_id: Optional[str] = None """Contains the ID of the message to which this message is a reply.""" @@ -146,11 +142,6 @@ def with_conversation(self, value: ConversationAccount) -> Self: self.conversation = value return self - def with_relates_to(self, value: ConversationReference) -> Self: - """Set the relates_to field.""" - self.relates_to = value - return self - def with_recipient(self, value: Account, is_targeted: Optional[bool] = None) -> Self: """ Set the recipient. diff --git a/packages/api/src/microsoft_teams/api/models/attachment/attachment.py b/packages/api/src/microsoft_teams/api/models/attachment/attachment.py index 876dde8fb..69d73af9d 100644 --- a/packages/api/src/microsoft_teams/api/models/attachment/attachment.py +++ b/packages/api/src/microsoft_teams/api/models/attachment/attachment.py @@ -11,9 +11,6 @@ class Attachment(CustomBaseModel): """A model representing an attachment.""" - id: Optional[str] = None - "The id of the attachment." - content_type: str "mimetype/Contenttype for the file" 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 ccd35b434..fc336f5c7 100644 --- a/packages/api/src/microsoft_teams/api/models/conversation/__init__.py +++ b/packages/api/src/microsoft_teams/api/models/conversation/__init__.py @@ -6,5 +6,6 @@ from .conversation import Conversation, ConversationType from .conversation_reference import ConversationReference from .conversation_resource import ConversationResource +from .paged_members_result import PagedMembersResult -__all__ = ["Conversation", "ConversationReference", "ConversationResource", "ConversationType"] +__all__ = ["Conversation", "ConversationReference", "ConversationResource", "ConversationType", "PagedMembersResult"] diff --git a/packages/api/src/microsoft_teams/api/models/conversation/paged_members_result.py b/packages/api/src/microsoft_teams/api/models/conversation/paged_members_result.py new file mode 100644 index 000000000..89aa7afce --- /dev/null +++ b/packages/api/src/microsoft_teams/api/models/conversation/paged_members_result.py @@ -0,0 +1,23 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import List, Optional + +from pydantic import Field + +from ..account import TeamsChannelAccount +from ..custom_base_model import CustomBaseModel + + +class PagedMembersResult(CustomBaseModel): + """ + Result of a paged members request. + """ + + members: List[TeamsChannelAccount] = Field(default_factory=list[TeamsChannelAccount]) + "The members in this page." + + continuation_token: Optional[str] = None + "Token to fetch the next page of members. None if this is the last page." diff --git a/packages/api/src/microsoft_teams/api/models/meetings/__init__.py b/packages/api/src/microsoft_teams/api/models/meetings/__init__.py index 8e7183676..1a07032e3 100644 --- a/packages/api/src/microsoft_teams/api/models/meetings/__init__.py +++ b/packages/api/src/microsoft_teams/api/models/meetings/__init__.py @@ -6,11 +6,23 @@ from .meeting import Meeting from .meeting_details import MeetingDetails from .meeting_info import MeetingInfo +from .meeting_notification import ( + MeetingNotificationParams, + MeetingNotificationRecipientFailure, + MeetingNotificationResponse, + MeetingNotificationSurface, + MeetingNotificationValue, +) from .meeting_participant import MeetingParticipant __all__ = [ "Meeting", "MeetingDetails", "MeetingInfo", + "MeetingNotificationParams", + "MeetingNotificationRecipientFailure", + "MeetingNotificationResponse", + "MeetingNotificationSurface", + "MeetingNotificationValue", "MeetingParticipant", ] diff --git a/packages/api/src/microsoft_teams/api/models/meetings/meeting_notification.py b/packages/api/src/microsoft_teams/api/models/meetings/meeting_notification.py new file mode 100644 index 000000000..71d2effd3 --- /dev/null +++ b/packages/api/src/microsoft_teams/api/models/meetings/meeting_notification.py @@ -0,0 +1,75 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Any, Dict, List, Optional + +from ..custom_base_model import CustomBaseModel + + +class MeetingNotificationSurface(CustomBaseModel): + """ + A surface target for a meeting notification. + """ + + surface: str + "The surface type. E.g. 'meetingStage', 'meetingTabIcon', 'meetingCopilotPane'." + + content_type: Optional[str] = None + "The content type for surfaces that carry content. E.g. 'task'." + + content: Optional[Dict[str, Any]] = None + "The content payload for the surface." + + tab_entity_id: Optional[str] = None + "The tab entity ID, required for 'meetingTabIcon' surfaces." + + +class MeetingNotificationValue(CustomBaseModel): + """ + The value of a targeted meeting notification. + """ + + recipients: List[str] + "AAD object IDs of the meeting participants to notify." + + surfaces: List[MeetingNotificationSurface] + "The surfaces to send the notification to." + + +class MeetingNotificationParams(CustomBaseModel): + """ + Parameters for sending a meeting notification. + """ + + type: str = "targetedMeetingNotification" + "The notification type." + + value: MeetingNotificationValue + "The notification value containing recipients and surfaces." + + +class MeetingNotificationRecipientFailure(CustomBaseModel): + """ + Information about a recipient that failed to receive a meeting notification. + """ + + recipient_mri: Optional[str] = None + "The MRI of the recipient." + + error_code: Optional[str] = None + "The error code." + + failure_reason: Optional[str] = None + "The reason for the failure." + + +class MeetingNotificationResponse(CustomBaseModel): + """ + Response from a meeting notification request when some or all recipients failed (HTTP 207). + None is returned when all notifications were sent successfully (HTTP 202). + """ + + recipients_failure_info: Optional[List[MeetingNotificationRecipientFailure]] = None + "Information about recipients that failed to receive the notification." diff --git a/packages/api/src/microsoft_teams/api/models/team_details.py b/packages/api/src/microsoft_teams/api/models/team_details.py index 70d30ba05..527cbaa4a 100644 --- a/packages/api/src/microsoft_teams/api/models/team_details.py +++ b/packages/api/src/microsoft_teams/api/models/team_details.py @@ -30,3 +30,6 @@ class TeamDetails(CustomBaseModel): member_count: Optional[int] = None "Count of members in the team." + + tenant_id: Optional[str] = None + "Azure Active Directory (AAD) tenant ID for the team." diff --git a/packages/api/tests/conftest.py b/packages/api/tests/conftest.py index 701969443..2fa97f016 100644 --- a/packages/api/tests/conftest.py +++ b/packages/api/tests/conftest.py @@ -86,18 +86,41 @@ def handler(request: httpx.Request) -> httpx.Response: "tokenExchangeResource": {"id": "mock_resource_id"}, } elif "/v3/teams/" in str(request.url) and "/conversations" in str(request.url): - response_data = [ - { - "id": "mock_channel_id_1", - "name": "General", - "type": "standard", - }, - { - "id": "mock_channel_id_2", - "name": "Random", - "type": "standard", - }, - ] + response_data = { + "conversations": [ + { + "id": "mock_channel_id_1", + "name": "General", + "type": "standard", + }, + { + "id": "mock_channel_id_2", + "name": "Random", + "type": "standard", + }, + ] + } + elif "/conversations/" in str(request.url) and "/pagedMembers" in str(request.url): + response_data = { + "members": [ + { + "id": "mock_member_id", + "name": "Mock Member", + "aadObjectId": "mock_aad_object_id", + } + ], + "continuationToken": "mock_continuation_token", + } + elif "/notification" in str(request.url) and request.method == "POST": + response_data = { + "recipientsFailureInfo": [ + { + "recipientMri": "8:orgid:mock_recipient", + "errorCode": "BadArgument", + "failureReason": "Invalid recipient", + } + ] + } elif "/conversations/" in str(request.url) and str(request.url).endswith("/members"): response_data = [ { @@ -112,17 +135,6 @@ def handler(request: httpx.Request) -> httpx.Response: "name": "Mock Member", "aadObjectId": "mock_aad_object_id", } - elif "/conversations" in str(request.url) and request.method == "GET": - response_data = { - "conversations": [ - { - "id": "mock_conversation_id", - "conversationType": "personal", - "isGroup": True, - } - ], - "continuationToken": "mock_continuation_token", - } elif "/conversations" in str(request.url) and request.method == "POST": # Parse request body to check if activity is present try: @@ -228,6 +240,128 @@ def mock_http_client(mock_transport): return client +@pytest.fixture +def request_capture(): + """Fixture to capture HTTP request details for testing. + + Returns: + A Client instance with an attached `_capture` attribute containing the RequestCapture helper. + Access captured requests via `client._capture.last_request` or `client._capture.requests`. + """ + + class RequestCapture: + """Helper class to capture request details.""" + + def __init__(self): + self.requests: list[httpx.Request] = [] + + def handler(self, request: httpx.Request) -> httpx.Response: + """Handler that captures requests and returns mock responses.""" + self.requests.append(request) + + # Default response + response_data: Any = { + "ok": True, + "url": str(request.url), + "method": request.method, + } + + # Handle specific endpoints with realistic responses + if "GetAadTokens" in str(request.url): + response_data = { + "https://graph.microsoft.com": { + "connectionName": "test_connection", + "token": "mock_graph_token_123", + "expiration": "2024-12-01T12:00:00Z", + }, + } + elif "/conversations/" in str(request.url) and str(request.url).endswith("/members"): + response_data = [ + { + "id": "mock_member_id", + "name": "Mock Member", + "aadObjectId": "mock_aad_object_id", + } + ] + elif "/conversations/" in str(request.url) and "/members/" in str(request.url) and request.method == "GET": + response_data = { + "id": "mock_member_id", + "name": "Mock Member", + "aadObjectId": "mock_aad_object_id", + } + elif "/conversations" in str(request.url) and request.method == "GET": + response_data = { + "conversations": [ + { + "id": "mock_conversation_id", + "conversationType": "personal", + "isGroup": True, + } + ], + "continuationToken": "mock_continuation_token", + } + elif "/conversations" in str(request.url) and request.method == "POST": + # Parse request body to check if activity is included + try: + import json + + request_body = json.loads(request.content.decode("utf-8")) if request.content else {} + has_activity = "activity" in request_body and request_body["activity"] is not None + except Exception: + has_activity = True # Default to including activity_id if we can't parse + + response_data = { + "id": "mock_conversation_id", + "type": "message", + "serviceUrl": "https://mock.service.url", + } + if has_activity: + response_data["activityId"] = "mock_activity_id" + elif "/activities" in str(request.url): + if request.method == "POST": + response_data = { + "id": "mock_activity_id", + "type": "message", + "text": "Mock activity response", + } + elif request.method == "PUT": + response_data = { + "id": "mock_activity_id", + "type": "message", + "text": "Updated mock activity", + } + elif request.method == "GET": + response_data = [ + { + "id": "mock_member_id", + "name": "Mock Member", + "aadObjectId": "mock_aad_object_id", + } + ] + + return httpx.Response( + status_code=200, + json=response_data, + headers={"content-type": "application/json"}, + ) + + @property + def last_request(self) -> httpx.Request | None: + """Get the last captured request.""" + return self.requests[-1] if self.requests else None + + def clear(self): + """Clear all captured requests.""" + self.requests.clear() + + capture = RequestCapture() + transport = httpx.MockTransport(capture.handler) + client = Client(ClientOptions(base_url="https://mock.api.com")) + client.http._transport = transport + client._capture = capture # type: ignore[attr-defined] # Attach for test access + return client + + @pytest.fixture def mock_client_credentials(): """Create mock client credentials for testing.""" diff --git a/packages/api/tests/unit/test_activity.py b/packages/api/tests/unit/test_activity.py index 0c7c4ea24..0d643f0ac 100644 --- a/packages/api/tests/unit/test_activity.py +++ b/packages/api/tests/unit/test_activity.py @@ -15,7 +15,6 @@ ChannelInfo, CitationIconName, ConversationAccount, - ConversationReference, MeetingInfo, NotificationInfo, TeamInfo, @@ -26,12 +25,12 @@ @pytest.fixture def user() -> Account: - return Account(id="1", name="test", role="user") + return Account(id="1", name="test") @pytest.fixture def bot() -> Account: - return Account(id="2", name="test-bot", role="bot") + return Account(id="2", name="test-bot") @pytest.fixture @@ -66,14 +65,6 @@ def test_should_build( ) -> None: activity = ( test_activity.with_locale("en") - .with_relates_to( - ConversationReference( - channel_id="msteams", - service_url="http://localhost", - bot=bot, - conversation=chat, - ) - ) .with_recipient(bot) .with_reply_to_id("3") .with_service_url("http://localhost") @@ -86,12 +77,6 @@ def test_should_build( assert activity.locale == "en" assert activity.from_ == user assert activity.conversation == chat - assert activity.relates_to == ConversationReference( - channel_id="msteams", - service_url="http://localhost", - bot=bot, - conversation=chat, - ) assert activity.recipient == bot assert activity.reply_to_id == "3" assert activity.service_url == "http://localhost" diff --git a/packages/api/tests/unit/test_conversation_client.py b/packages/api/tests/unit/test_conversation_client.py index 86053b3da..cc309015d 100644 --- a/packages/api/tests/unit/test_conversation_client.py +++ b/packages/api/tests/unit/test_conversation_client.py @@ -4,13 +4,14 @@ """ # pyright: basic +import json +from unittest.mock import AsyncMock, patch + +import httpx import pytest from microsoft_teams.api.clients.conversation import ConversationClient -from microsoft_teams.api.clients.conversation.params import ( - CreateConversationParams, - GetConversationsParams, -) -from microsoft_teams.api.models import ConversationResource, TeamsChannelAccount +from microsoft_teams.api.clients.conversation.params import CreateConversationParams +from microsoft_teams.api.models import ConversationResource, PagedMembersResult, TeamsChannelAccount from microsoft_teams.common.http import Client, ClientOptions @@ -48,40 +49,13 @@ def test_conversation_client_initialization_with_options(self): assert client.service_url == service_url @pytest.mark.asyncio - async def test_get_conversations(self, mock_http_client): - """Test getting conversations.""" - service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) - - params = GetConversationsParams(continuation_token="test_token") - response = await client.get(params) - - assert response.conversations is not None - assert isinstance(response.conversations, list) - assert response.continuation_token is not None - - @pytest.mark.asyncio - async def test_get_conversations_without_params(self, mock_http_client): - """Test getting conversations without parameters.""" - service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) - - response = await client.get() - - assert response.conversations is not None - assert isinstance(response.conversations, list) - - @pytest.mark.asyncio - async def test_create_conversation(self, mock_http_client, mock_account, mock_activity): + async def test_create_conversation(self, request_capture, mock_account, mock_activity): """Test creating a conversation with an activity.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) params = CreateConversationParams( - is_group=True, - bot=mock_account, members=[mock_account], - topic_name="Test Conversation", tenant_id="test_tenant_id", activity=mock_activity, channel_data={"custom": "data"}, @@ -89,30 +63,43 @@ async def test_create_conversation(self, mock_http_client, mock_account, mock_ac response = await client.create(params) + # Validate response assert response.id is not None assert response.activity_id is not None assert response.service_url is not None + # Validate request details + last_request = request_capture._capture.last_request + assert last_request.method == "POST" + assert str(last_request.url) == "https://test.service.url/v3/conversations" + + # Validate request payload + payload = json.loads(last_request.content) + assert payload["tenantId"] == "test_tenant_id" + @pytest.mark.asyncio - async def test_create_conversation_without_activity(self, mock_http_client, mock_account): + async def test_create_conversation_without_activity(self, request_capture, mock_account): """Test creating a conversation without an activity.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) params = CreateConversationParams( - is_group=True, - bot=mock_account, members=[mock_account], - topic_name="Test Conversation", tenant_id="test_tenant_id", ) response = await client.create(params) + # Validate response assert response.id is not None assert response.activity_id is None assert response.service_url is not None + # Validate request details + last_request = request_capture._capture.last_request + assert last_request.method == "POST" + assert str(last_request.url) == "https://test.service.url/v3/conversations" + def test_conversation_resource_with_all_fields(self): """Test that ConversationResource correctly handles all fields present.""" resource = ConversationResource.model_validate( @@ -189,22 +176,33 @@ def test_members_operations(self, mock_http_client): class TestConversationActivityOperations: """Unit tests for ConversationClient activity operations.""" - async def test_activity_create(self, mock_http_client, mock_activity): + async def test_activity_create(self, request_capture, mock_activity): """Test creating an activity.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) conversation_id = "test_conversation_id" activities = client.activities(conversation_id) result = await activities.create(mock_activity) + # Validate response assert result is not None + assert result.id is not None - async def test_activity_update(self, mock_http_client, mock_activity): + # Validate request details + last_request = request_capture._capture.last_request + assert last_request.method == "POST" + assert str(last_request.url) == f"https://test.service.url/v3/conversations/{conversation_id}/activities" + + # Validate request payload + payload = json.loads(last_request.content) + assert payload["type"] == "message" + + async def test_activity_update(self, request_capture, mock_activity): """Test updating an activity.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) conversation_id = "test_conversation_id" activity_id = "test_activity_id" @@ -212,12 +210,26 @@ async def test_activity_update(self, mock_http_client, mock_activity): result = await activities.update(activity_id, mock_activity) + # Validate response assert result is not None + assert result.id is not None + + # Validate request details + last_request = request_capture._capture.last_request + assert last_request.method == "PUT" + assert ( + str(last_request.url) + == f"https://test.service.url/v3/conversations/{conversation_id}/activities/{activity_id}" + ) - async def test_activity_reply(self, mock_http_client, mock_activity): + # Validate request payload + payload = json.loads(last_request.content) + assert payload["type"] == "message" + + async def test_activity_reply(self, request_capture, mock_activity): """Test replying to an activity.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) conversation_id = "test_conversation_id" activity_id = "test_activity_id" @@ -225,12 +237,26 @@ async def test_activity_reply(self, mock_http_client, mock_activity): result = await activities.reply(activity_id, mock_activity) + # Validate response assert result is not None + assert result.id is not None + + # Validate request details + last_request = request_capture._capture.last_request + assert last_request.method == "POST" + assert ( + str(last_request.url) + == f"https://test.service.url/v3/conversations/{conversation_id}/activities/{activity_id}" + ) + + # Validate request payload - check that replyToId was added + payload = json.loads(last_request.content) + assert payload["replyToId"] == activity_id - async def test_activity_delete(self, mock_http_client): + async def test_activity_delete(self, request_capture): """Test deleting an activity.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) conversation_id = "test_conversation_id" activity_id = "test_activity_id" @@ -239,10 +265,33 @@ async def test_activity_delete(self, mock_http_client): # Should not raise an exception await activities.delete(activity_id) - async def test_activity_get_members(self, mock_http_client): + # Validate request details + last_request = request_capture._capture.last_request + assert last_request.method == "DELETE" + assert ( + str(last_request.url) + == f"https://test.service.url/v3/conversations/{conversation_id}/activities/{activity_id}" + ) + + conversation_id = "test_conversation_id" + activity_id = "test_activity_id" + activities = client.activities(conversation_id) + + # Should not raise an exception + await activities.delete(activity_id) + + # Validate request details + last_request = request_capture._capture.last_request + assert last_request.method == "DELETE" + assert ( + str(last_request.url) + == f"https://test.service.url/v3/conversations/{conversation_id}/activities/{activity_id}" + ) + + async def test_activity_get_members(self, request_capture): """Test getting activity members.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) conversation_id = "test_conversation_id" activity_id = "test_activity_id" @@ -250,7 +299,17 @@ async def test_activity_get_members(self, mock_http_client): result = await activities.get_members(activity_id) + # Validate response assert result is not None + assert len(result) > 0 + + # Validate request details + last_request = request_capture._capture.last_request + assert last_request.method == "GET" + assert ( + str(last_request.url) + == f"https://test.service.url/v3/conversations/{conversation_id}/activities/{activity_id}/members" + ) @pytest.mark.unit @@ -258,16 +317,17 @@ async def test_activity_get_members(self, mock_http_client): class TestConversationMemberOperations: """Unit tests for ConversationClient member operations.""" - async def test_member_get_all(self, mock_http_client): + async def test_member_get_all(self, request_capture): """Test getting all members returns TeamsChannelAccount instances.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) conversation_id = "test_conversation_id" members = client.members(conversation_id) result = await members.get_all() + # Validate response assert result is not None assert len(result) > 0 assert isinstance(result[0], TeamsChannelAccount) @@ -275,10 +335,15 @@ async def test_member_get_all(self, mock_http_client): assert result[0].name == "Mock Member" assert result[0].aad_object_id == "mock_aad_object_id" - async def test_member_get(self, mock_http_client): + # Validate request details + last_request = request_capture._capture.last_request + assert last_request.method == "GET" + assert str(last_request.url) == f"https://test.service.url/v3/conversations/{conversation_id}/members" + + async def test_member_get(self, request_capture): """Test getting a specific member returns TeamsChannelAccount instance.""" service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) + client = ConversationClient(service_url, request_capture) conversation_id = "test_conversation_id" member_id = "test_member_id" @@ -286,23 +351,55 @@ async def test_member_get(self, mock_http_client): result = await members.get(member_id) + # Validate response assert result is not None assert isinstance(result, TeamsChannelAccount) assert result.id == "mock_member_id" assert result.name == "Mock Member" assert result.aad_object_id == "mock_aad_object_id" - async def test_member_delete(self, mock_http_client): - """Test deleting a member.""" + # Validate request details + last_request = request_capture._capture.last_request + assert last_request.method == "GET" + assert ( + str(last_request.url) == f"https://test.service.url/v3/conversations/{conversation_id}/members/{member_id}" + ) + + async def test_member_get_paged(self, mock_http_client): + """Test getting a page of members returns PagedMembersResult.""" + service_url = "https://test.service.url" client = ConversationClient(service_url, mock_http_client) conversation_id = "test_conversation_id" - member_id = "test_member_id" members = client.members(conversation_id) - # Should not raise an exception - await members.delete(member_id) + result = await members.get_paged() + + assert isinstance(result, PagedMembersResult) + assert len(result.members) == 1 + assert isinstance(result.members[0], TeamsChannelAccount) + assert result.members[0].id == "mock_member_id" + assert result.continuation_token == "mock_continuation_token" + + async def test_member_get_paged_with_token(self, mock_http_client): + """Test get_paged passes continuation_token and page_size.""" + + service_url = "https://test.service.url" + client = ConversationClient(service_url, mock_http_client) + members = client.members("test_conversation_id") + + mock_response = httpx.Response( + 200, + json={"members": [], "continuationToken": None}, + headers={"content-type": "application/json"}, + ) + with patch.object(mock_http_client, "get", new_callable=AsyncMock, return_value=mock_response) as mock_get: + await members.get_paged(page_size=50, continuation_token="some_token") + + called_params = mock_get.call_args.kwargs.get("params", {}) + assert called_params.get("pageSize") == 50 + assert called_params.get("continuationToken") == "some_token" @pytest.mark.unit diff --git a/packages/api/tests/unit/test_meeting_client.py b/packages/api/tests/unit/test_meeting_client.py index 7a2701e87..4f4e7d183 100644 --- a/packages/api/tests/unit/test_meeting_client.py +++ b/packages/api/tests/unit/test_meeting_client.py @@ -4,9 +4,19 @@ """ # pyright: basic +from unittest.mock import AsyncMock, patch + +import httpx import pytest from microsoft_teams.api.clients.meeting import MeetingClient -from microsoft_teams.api.models import MeetingInfo, MeetingParticipant +from microsoft_teams.api.models import ( + MeetingInfo, + MeetingNotificationParams, + MeetingNotificationResponse, + MeetingNotificationSurface, + MeetingNotificationValue, + MeetingParticipant, +) from microsoft_teams.common.http import Client, ClientOptions @@ -57,3 +67,59 @@ def test_meeting_client_strips_trailing_slash(self, mock_http_client): client = MeetingClient(service_url, mock_http_client) assert client.service_url == "https://test.service.url" + + @pytest.mark.asyncio + async def test_send_notification_partial_failure(self, mock_http_client): + """Test send_notification returns MeetingNotificationResponse on partial failure (HTTP 207).""" + + service_url = "https://test.service.url" + client = MeetingClient(service_url, mock_http_client) + + params = MeetingNotificationParams( + value=MeetingNotificationValue( + recipients=["mock_aad_oid"], + surfaces=[MeetingNotificationSurface(surface="meetingTabIcon", tab_entity_id="test")], + ) + ) + + partial_failure_response = httpx.Response( + 207, + json={ + "recipientsFailureInfo": [ + { + "recipientMri": "8:orgid:mock_recipient", + "errorCode": "BadArgument", + "failureReason": "Invalid recipient", + } + ] + }, + headers={"content-type": "application/json"}, + ) + with patch.object(mock_http_client, "post", new_callable=AsyncMock, return_value=partial_failure_response): + result = await client.send_notification("mock_meeting_id", params) + + assert isinstance(result, MeetingNotificationResponse) + assert result.recipients_failure_info is not None + assert len(result.recipients_failure_info) == 1 + assert result.recipients_failure_info[0].error_code == "BadArgument" + + @pytest.mark.asyncio + async def test_send_notification_full_success(self, mock_http_client): + """Test send_notification returns None on full success (HTTP 202, empty body).""" + import httpx + + service_url = "https://test.service.url" + client = MeetingClient(service_url, mock_http_client) + + params = MeetingNotificationParams( + value=MeetingNotificationValue( + recipients=["mock_aad_oid"], + surfaces=[MeetingNotificationSurface(surface="meetingTabIcon", tab_entity_id="test")], + ) + ) + + empty_response = httpx.Response(202, content=b"", headers={"content-type": "application/json"}) + with patch.object(mock_http_client, "post", new_callable=AsyncMock, return_value=empty_response): + result = await client.send_notification("mock_meeting_id", params) + + assert result is None diff --git a/packages/api/tests/unit/test_message_activities.py b/packages/api/tests/unit/test_message_activities.py index 1109a8adb..cf09f16a4 100644 --- a/packages/api/tests/unit/test_message_activities.py +++ b/packages/api/tests/unit/test_message_activities.py @@ -23,7 +23,6 @@ Attachment, ChannelData, ConversationAccount, - Importance, MentionEntity, MessageReaction, MessageUser, @@ -37,8 +36,8 @@ class TestMessageActivity: def create_message_activity(self, text: str = "Hello world!") -> MessageActivityInput: """Create a basic message activity for testing""" - from_account = Account(id="bot-123", name="Test Bot", role="bot") - recipient = Account(id="user-456", name="Test User", role="user") + from_account = Account(id="bot-123", name="Test Bot") + recipient = Account(id="user-456", name="Test User") conversation = ConversationAccount(id="conv-789", conversation_type="personal") activity = MessageActivityInput( @@ -52,8 +51,8 @@ def create_message_activity(self, text: str = "Hello world!") -> MessageActivity def create_incoming_message_activity(self, text: str = "Hello world!") -> MessageActivity: """Create a basic incoming MessageActivity for testing""" - from_account = Account(id="bot-123", name="Test Bot", role="bot") - recipient = Account(id="user-456", name="Test User", role="user") + from_account = Account(id="bot-123", name="Test Bot") + recipient = Account(id="user-456", name="Test User") conversation = ConversationAccount(id="conv-789", conversation_type="personal") activity = MessageActivity( @@ -74,8 +73,8 @@ def test_message_activity_creation(self): def test_message_activity_text_defaults_to_empty_string(self): """Test that text field defaults to empty string in MessageActivity""" - from_account = Account(id="bot-123", name="Test Bot", role="bot") - recipient = Account(id="user-456", name="Test User", role="user") + from_account = Account(id="bot-123", name="Test Bot") + recipient = Account(id="user-456", name="Test User") conversation = ConversationAccount(id="conv-789", conversation_type="personal") activity = MessageActivity( @@ -145,7 +144,7 @@ def test_add_attachments_to_existing(self): def test_add_mention_basic(self): """Test adding a basic mention""" activity = self.create_message_activity("Hello ") - account = Account(id="user-123", name="John Doe", role="user") + account = Account(id="user-123", name="John Doe") result = activity.add_mention(account) @@ -162,7 +161,7 @@ def test_add_mention_basic(self): def test_add_mention_custom_text(self): """Test adding a mention with custom text""" activity = self.create_message_activity("Hello ") - account = Account(id="bot-456", name="Test Bot", role="bot") + account = Account(id="bot-456", name="Test Bot") activity.add_mention(account, text="Custom Bot Name") @@ -174,7 +173,7 @@ def test_add_mention_custom_text(self): def test_add_mention_without_adding_text(self): """Test adding a mention without adding text to message""" activity = self.create_message_activity("Hello world") - account = Account(id="user-789", name="Jane Doe", role="user") + account = Account(id="user-789", name="Jane Doe") activity.add_mention(account, add_text=False) @@ -200,8 +199,8 @@ def test_add_card_method(self): def test_strip_mentions_text_basic(self): """Test stripping mentions from text""" - from_account = Account(id="bot-123", name="Test Bot", role="bot") - recipient = Account(id="user-456", name="Test User", role="user") + from_account = Account(id="bot-123", name="Test Bot") + recipient = Account(id="user-456", name="Test User") conversation = ConversationAccount(id="conv-789", conversation_type="personal") activity = MessageActivity( id="msg-123", @@ -211,7 +210,7 @@ def test_strip_mentions_text_basic(self): recipient=recipient, ) # Add mention entity - account = Account(id="bot-123", name="Bot", role="bot") + account = Account(id="bot-123", name="Bot") mention = MentionEntity(mentioned=account, text="Bot") activity.entities = [mention] @@ -222,8 +221,8 @@ def test_strip_mentions_text_basic(self): def test_strip_mentions_text_with_options(self): """Test stripping mentions with options""" - from_account = Account(id="bot-123", name="Test Bot", role="bot") - recipient = Account(id="user-456", name="Test User", role="user") + from_account = Account(id="bot-123", name="Test Bot") + recipient = Account(id="user-456", name="Test User") conversation = ConversationAccount(id="conv-789", conversation_type="personal") activity = MessageActivity( id="msg-123", @@ -233,7 +232,7 @@ def test_strip_mentions_text_with_options(self): recipient=recipient, ) # Add mention entity - account = Account(id="bot-123", name="Bot", role="bot") + account = Account(id="bot-123", name="Bot") mention = MentionEntity(mentioned=account, text="Bot") activity.entities = [mention] @@ -245,7 +244,7 @@ def test_strip_mentions_text_with_options(self): def test_is_recipient_mentioned_true(self): """Test detecting when recipient is mentioned""" activity = self.create_message_activity("Hello Bot!") - recipient = Account(id="bot-123", name="Bot", role="bot") + recipient = Account(id="bot-123", name="Bot") activity.recipient = recipient mention = MentionEntity(mentioned=recipient, text="Bot") @@ -256,7 +255,7 @@ def test_is_recipient_mentioned_true(self): def test_is_recipient_mentioned_false(self): """Test detecting when recipient is not mentioned""" activity = self.create_message_activity("Hello world!") - recipient = Account(id="bot-123", name="Bot", role="bot") + recipient = Account(id="bot-123", name="Bot") activity.recipient = recipient # Mention someone else @@ -269,7 +268,7 @@ def test_is_recipient_mentioned_false(self): def test_is_recipient_mentioned_no_recipient(self): """Test is_recipient_mentioned when no recipient is set""" activity = self.create_message_activity("Hello Bot!") - account = Account(id="bot-123", name="Bot", role="bot") + account = Account(id="bot-123", name="Bot") mention = MentionEntity(mentioned=account, text="Bot") activity.entities = [mention] @@ -278,7 +277,7 @@ def test_is_recipient_mentioned_no_recipient(self): def test_is_recipient_mentioned_no_entities(self): """Test is_recipient_mentioned when no entities exist""" activity = self.create_message_activity("Hello world!") - recipient = Account(id="bot-123", name="Bot", role="bot") + recipient = Account(id="bot-123", name="Bot") activity.recipient = recipient assert activity.is_recipient_mentioned() is False @@ -286,7 +285,7 @@ def test_is_recipient_mentioned_no_entities(self): def test_get_account_mention_found(self): """Test getting account mention when it exists""" activity = self.create_message_activity("Hello Bot!") - account = Account(id="bot-123", name="Bot", role="bot") + account = Account(id="bot-123", name="Bot") mention = MentionEntity(mentioned=account, text="Bot") activity.entities = [mention] @@ -335,8 +334,8 @@ def test_complex_message_building(self): activity = self.create_message_activity("Meeting with ") # Add mentions - user1 = Account(id="user-1", name="Alice", role="user") - user2 = Account(id="user-2", name="Bob", role="user") + user1 = Account(id="user-1", name="Alice") + user2 = Account(id="user-2", name="Bob") activity.add_mention(user1).add_text(" and ").add_mention(user2).add_text(" scheduled.") @@ -345,17 +344,13 @@ def test_complex_message_building(self): activity.add_card(card) # Set properties - activity.importance = Importance.HIGH activity.delivery_mode = "notification" - activity.input_hint = "acceptingInput" # Verify final state assert activity.text and activity.text.find("Meeting with Alice and Bob scheduled.") >= 0 assert activity.entities and len(activity.entities) == 2 # Two mentions assert activity.attachments and len(activity.attachments) == 1 - assert activity.importance == Importance.HIGH assert activity.delivery_mode == "notification" - assert activity.input_hint == "acceptingInput" def test_is_recipient_mentioned_no_entities_received(self): """Test MessageActivity.is_recipient_mentioned when no entities exist""" @@ -364,7 +359,7 @@ def test_is_recipient_mentioned_no_entities_received(self): def test_is_recipient_mentioned_no_recipient_received(self): """Test MessageActivity.is_recipient_mentioned when recipient is None""" - from_account = Account(id="bot-123", name="Test Bot", role="bot") + from_account = Account(id="bot-123", name="Test Bot") conversation = ConversationAccount(id="conv-789", conversation_type="personal") activity = MessageActivity( id="msg-123", @@ -375,7 +370,7 @@ def test_is_recipient_mentioned_no_recipient_received(self): ) # Override recipient to None to test that branch activity.recipient = None # type: ignore[assignment] - account = Account(id="r-1", name="R", role="bot") + account = Account(id="r-1", name="R") mention = MentionEntity(mentioned=account, text="R") activity.entities = [mention] @@ -384,7 +379,7 @@ def test_is_recipient_mentioned_no_recipient_received(self): def test_is_recipient_mentioned_when_mentioned_received(self): """Test MessageActivity.is_recipient_mentioned returns True when recipient is mentioned""" activity = self.create_incoming_message_activity("Hello Test User!") - recipient = Account(id="user-456", name="Test User", role="user") + recipient = Account(id="user-456", name="Test User") activity.recipient = recipient mention = MentionEntity(mentioned=recipient, text="Test User") activity.entities = [mention] @@ -394,9 +389,9 @@ def test_is_recipient_mentioned_when_mentioned_received(self): def test_is_recipient_mentioned_not_mentioned_received(self): """Test MessageActivity.is_recipient_mentioned returns False when recipient is not mentioned""" activity = self.create_incoming_message_activity("Hello Other!") - recipient = Account(id="user-456", name="Test User", role="user") + recipient = Account(id="user-456", name="Test User") activity.recipient = recipient - other = Account(id="other-999", name="Other", role="user") + other = Account(id="other-999", name="Other") mention = MentionEntity(mentioned=other, text="Other") activity.entities = [mention] @@ -412,7 +407,7 @@ def test_get_account_mention_no_entities_received(self): def test_get_account_mention_found_received(self): """Test MessageActivity.get_account_mention returns MentionEntity when found""" activity = self.create_incoming_message_activity("Hello Test User!") - account = Account(id="user-456", name="Test User", role="user") + account = Account(id="user-456", name="Test User") mention = MentionEntity(mentioned=account, text="Test User") activity.entities = [mention] @@ -426,7 +421,7 @@ def test_get_account_mention_found_received(self): def test_get_account_mention_not_found_received(self): """Test MessageActivity.get_account_mention returns None when account not in mentions""" activity = self.create_incoming_message_activity("Hello Test User!") - account = Account(id="user-456", name="Test User", role="user") + account = Account(id="user-456", name="Test User") mention = MentionEntity(mentioned=account, text="Test User") activity.entities = [mention] @@ -436,7 +431,7 @@ def test_get_account_mention_not_found_received(self): def test_strip_mentions_text_updates_text_received(self): """Test MessageActivity.strip_mentions_text sets text when stripped_text is not None""" activity = self.create_incoming_message_activity("Hi Test User! How are you?") - account = Account(id="user-456", name="Test User", role="user") + account = Account(id="user-456", name="Test User") mention = MentionEntity(mentioned=account, text="Test User") activity.entities = [mention] @@ -451,8 +446,8 @@ class TestMessageDeleteActivity: def create_message_delete_activity(self, activity_id: str = "delete-123") -> MessageDeleteActivity: """Create a basic message delete activity for testing""" - from_account = Account(id="bot-123", name="Test Bot", role="bot") - recipient = Account(id="user-456", name="Test User", role="user") + from_account = Account(id="bot-123", name="Test Bot") + recipient = Account(id="user-456", name="Test User") conversation = ConversationAccount(id="conv-789", conversation_type="personal") channel_data = MessageDeleteChannelData() @@ -486,8 +481,8 @@ def create_message_update_activity( self, activity_id: str = "update-123", text: str = "Updated text", event_type: MessageEventType = "editMessage" ) -> MessageUpdateActivity: """Create a basic message update activity for testing""" - from_account = Account(id="bot-123", name="Test Bot", role="bot") - recipient = Account(id="user-456", name="Test User", role="user") + from_account = Account(id="bot-123", name="Test Bot") + recipient = Account(id="user-456", name="Test User") conversation = ConversationAccount(id="conv-789", conversation_type="personal") channel_data = MessageUpdateChannelData(event_type=event_type) @@ -518,8 +513,8 @@ def test_message_update_activity_creation_undelete(self): def test_message_update_activity_optional_fields(self): """Test message update activity with optional fields""" - from_account = Account(id="bot-123", name="Test Bot", role="bot") - recipient = Account(id="user-456", name="Test User", role="user") + from_account = Account(id="bot-123", name="Test Bot") + recipient = Account(id="user-456", name="Test User") conversation = ConversationAccount(id="conv-789", conversation_type="personal") channel_data = MessageUpdateChannelData(event_type="editMessage") expiration = datetime.now() @@ -548,8 +543,8 @@ class TestMessageReactionActivity: def create_message_reaction_activity(self, activity_id: str = "reaction-123") -> MessageReactionActivityInput: """Create a basic message reaction activity for testing""" - from_account = Account(id="bot-123", name="Test Bot", role="bot") - recipient = Account(id="user-456", name="Test User", role="user") + from_account = Account(id="bot-123", name="Test Bot") + recipient = Account(id="user-456", name="Test User") conversation = ConversationAccount(id="conv-789", conversation_type="personal") activity = MessageReactionActivityInput( @@ -730,21 +725,20 @@ class TestMessageActivityIntegration: def test_message_activity_serialization(self): """Test that message activity can be serialized properly""" - from_account = Account(id="bot-123", name="Test Bot", role="bot") - recipient = Account(id="user-456", name="Test User", role="user") + from_account = Account(id="bot-123", name="Test Bot") + recipient = Account(id="user-456", name="Test User") conversation = ConversationAccount(id="conv-789", conversation_type="personal") activity = MessageActivityInput( id="msg-serialize", text="Hello Bot!", - importance=Importance.HIGH, from_=from_account, conversation=conversation, recipient=recipient, ) # Add mention - account = Account(id="bot-123", name="Bot", role="bot") + account = Account(id="bot-123", name="Bot") activity.add_mention(account, add_text=False) # Serialize to dict @@ -757,14 +751,13 @@ def test_message_activity_serialization(self): assert data["type"] == "message" assert data["text"] == "Hello Bot!" - assert data["importance"] == Importance.HIGH or data["importance"] == "high" assert "entities" in data assert len(data["entities"]) == 1 def test_message_activity_deserialization(self): """Test that message activity can be deserialized properly""" - from_account = Account(id="bot-123", name="Test Bot", role="bot") - recipient = Account(id="user-456", name="Test User", role="user") + from_account = Account(id="bot-123", name="Test Bot") + recipient = Account(id="user-456", name="Test User") conversation = ConversationAccount(id="conv-789", conversation_type="personal") data = { @@ -778,7 +771,7 @@ def test_message_activity_deserialization(self): "entities": [ { "type": "mention", - "mentioned": {"id": "user-123", "name": "User", "role": "user"}, + "mentioned": {"id": "user-123", "name": "User"}, "text": "User", } ], @@ -787,13 +780,12 @@ def test_message_activity_deserialization(self): activity = MessageActivityInput( id=data["id"], text=data["text"], - importance=Importance.NORMAL, from_=data["from"], conversation=data["conversation"], recipient=data["recipient"], entities=[ MentionEntity( - mentioned=Account(id="user-123", name="User", role="user"), + mentioned=Account(id="user-123", name="User"), text="User", ) ], @@ -801,7 +793,6 @@ def test_message_activity_deserialization(self): assert activity.id == "msg-deserialize" assert activity.text == "Hello world!" - assert activity.importance == Importance.NORMAL assert activity.entities is not None and len(activity.entities) == 1 def test_all_activity_types_have_correct_type(self): @@ -859,7 +850,7 @@ def test_all_activity_types_have_correct_type(self): def test_with_recipient_defaults_to_not_targeted(self): """Test that with_recipient without is_targeted clears recipient-targeting.""" - account = Account(id="user-123", name="Test User", role="user") + account = Account(id="user-123", name="Test User") activity = MessageActivityInput(text="hello").with_recipient(account) assert activity.recipient is not None @@ -868,18 +859,17 @@ def test_with_recipient_defaults_to_not_targeted(self): def test_with_recipient_sets_is_targeted_and_recipient(self): """Test that calling with_recipient with is_targeted=True sets both properties""" - account = Account(id="user-123", name="Test User", role="user") + account = Account(id="user-123", name="Test User") activity = MessageActivityInput(text="hello").with_recipient(account, is_targeted=True) assert activity.recipient is not None assert activity.recipient.is_targeted is True assert activity.recipient.id == "user-123" assert activity.recipient.name == "Test User" - assert activity.recipient.role == "user" def test_with_recipient_maintains_fluent_chaining(self): """Test that with_recipient maintains fluent API chaining""" - account = Account(id="user-123", name="Test User", role="user") + account = Account(id="user-123", name="Test User") activity = MessageActivityInput(text="hello").with_recipient(account, is_targeted=True).add_text(" world") assert activity.text == "hello world" @@ -889,7 +879,7 @@ def test_with_recipient_maintains_fluent_chaining(self): def test_with_recipient_targeted_serializes_on_recipient(self): """Test that targeted routing serializes as recipient.isTargeted in payload JSON.""" - account = Account(id="user-123", name="Test User", role="user") + account = Account(id="user-123", name="Test User") activity = MessageActivityInput(text="targeted message").with_recipient(account, is_targeted=True) data = activity.model_dump(by_alias=True, exclude_none=True) diff --git a/packages/apps/src/microsoft_teams/apps/app.py b/packages/apps/src/microsoft_teams/apps/app.py index 2c76c9c0d..f678910dc 100644 --- a/packages/apps/src/microsoft_teams/apps/app.py +++ b/packages/apps/src/microsoft_teams/apps/app.py @@ -151,7 +151,9 @@ def __init__(self, **options: Unpack[AppOptions]): self.entra_token_validator: Optional[TokenValidator] = None if self.credentials and hasattr(self.credentials, "client_id"): self.entra_token_validator = TokenValidator.for_entra( - self.credentials.client_id, self.credentials.tenant_id + self.credentials.client_id, + self.credentials.tenant_id, + application_id_uri=self.options.application_id_uri, ) @property @@ -287,7 +289,7 @@ async def send(self, conversation_id: str, activity: str | ActivityParams | Adap conversation_ref = ConversationReference( channel_id="msteams", service_url=self.api.service_url, - bot=Account(id=self.id, role="bot"), + bot=Account(id=self.id), conversation=ConversationAccount(id=conversation_id, conversation_type="personal"), ) diff --git a/packages/apps/src/microsoft_teams/apps/auth/token_validator.py b/packages/apps/src/microsoft_teams/apps/auth/token_validator.py index 0a0f1ea57..969ba5eec 100644 --- a/packages/apps/src/microsoft_teams/apps/auth/token_validator.py +++ b/packages/apps/src/microsoft_teams/apps/auth/token_validator.py @@ -45,6 +45,11 @@ def __init__(self, jwt_validation_options: JwtValidationOptions): jwt_validation_options: Configuration for JWT validation """ self.options = jwt_validation_options + self._jwks_client = jwt.PyJWKClient(jwt_validation_options.jwks_uri) + + @staticmethod + def _default_audiences(app_id: str) -> List[str]: + return [app_id, f"api://{app_id}", f"api://botid-{app_id}"] # ----- Factory constructors ----- @classmethod @@ -59,20 +64,28 @@ def for_service(cls, app_id: str, service_url: Optional[str] = None) -> "TokenVa options = JwtValidationOptions( valid_issuers=["https://api.botframework.com"], - valid_audiences=[app_id, f"api://{app_id}"], + valid_audiences=cls._default_audiences(app_id), jwks_uri="https://login.botframework.com/v1/.well-known/keys", service_url=service_url, ) return cls(options) @classmethod - def for_entra(cls, app_id: str, tenant_id: Optional[str], scope: Optional[str] = None) -> "TokenValidator": + def for_entra( + cls, + app_id: str, + tenant_id: Optional[str], + scope: Optional[str] = None, + application_id_uri: Optional[str] = None, + ) -> "TokenValidator": """Create a validator for Entra ID tokens. Args: app_id: The app's Microsoft App ID (used for audience validation) tenant_id: The Azure AD tenant ID scope: Optional scope that must be present in the token + application_id_uri: Optional Application ID URI from Azure portal. + Matches webApplicationInfo.resource in the app manifest. """ @@ -80,9 +93,12 @@ def for_entra(cls, app_id: str, tenant_id: Optional[str], scope: Optional[str] = if tenant_id: valid_issuers.append(f"https://login.microsoftonline.com/{tenant_id}/v2.0") tenant_id = tenant_id or "common" + valid_audiences = cls._default_audiences(app_id) + if application_id_uri: + valid_audiences.append(application_id_uri) options = JwtValidationOptions( valid_issuers=valid_issuers, - valid_audiences=[app_id, f"api://{app_id}"], + valid_audiences=valid_audiences, jwks_uri=f"https://login.microsoftonline.com/{tenant_id}/discovery/v2.0/keys", scope=scope, ) @@ -110,9 +126,7 @@ async def validate_token( raise jwt.InvalidTokenError("No token provided") try: - jwks_client = jwt.PyJWKClient(self.options.jwks_uri) - # Get signing key automatically from JWKS - signing_key = jwks_client.get_signing_key_from_jwt(raw_token) + signing_key = self._jwks_client.get_signing_key_from_jwt(raw_token) # Validate token payload: Dict[str, Any] = jwt.decode( diff --git a/packages/apps/src/microsoft_teams/apps/contexts/function_context.py b/packages/apps/src/microsoft_teams/apps/contexts/function_context.py index eb209ec5f..4ce90e9e3 100644 --- a/packages/apps/src/microsoft_teams/apps/contexts/function_context.py +++ b/packages/apps/src/microsoft_teams/apps/contexts/function_context.py @@ -68,7 +68,7 @@ async def send(self, activity: str | ActivityParams | AdaptiveCard) -> Optional[ conversation_ref = ConversationReference( channel_id="msteams", service_url=self.api.service_url, - bot=Account(id=self.id, name=self.name, role="bot"), + bot=Account(id=self.id, name=self.name), conversation=ConversationAccount(id=conversation_id, conversation_type="personal"), ) @@ -120,10 +120,8 @@ async def _resolve_conversation_id(self, activity: str | ActivityParams | Adapti or return a pre-existing one.""" try: conversation_params = CreateConversationParams( - bot=Account(id=self.id, name=self.name, role="bot"), # type: ignore - members=[Account(id=self.user_id, role="user", name=self.user_name)], + members=[Account(id=self.user_id, name=self.user_name)], tenant_id=self.tenant_id, - is_group=False, ) conversation = await self.api.conversations.create(conversation_params) self._resolved_conversation_id = conversation.id diff --git a/packages/apps/src/microsoft_teams/apps/options.py b/packages/apps/src/microsoft_teams/apps/options.py index 517ff6ae0..396d9a04c 100644 --- a/packages/apps/src/microsoft_teams/apps/options.py +++ b/packages/apps/src/microsoft_teams/apps/options.py @@ -25,6 +25,9 @@ class AppOptions(TypedDict, total=False): """The client secret. If provided with client_id, uses ClientCredentials auth.""" tenant_id: Optional[str] """The tenant ID. Required for single-tenant apps.""" + application_id_uri: Optional[str] + """Application ID URI from the Azure portal. Used for user authentication. + Matches webApplicationInfo.resource in the app manifest.""" # Custom token provider function token: Optional[Callable[[Union[str, list[str]], Optional[str]], Union[str, Awaitable[str]]]] """Custom token provider function. If provided with client_id (no client_secret), uses TokenCredentials.""" @@ -86,6 +89,9 @@ class InternalAppOptions: """The client secret. If provided with client_id, uses ClientCredentials auth.""" tenant_id: Optional[str] = None """The tenant ID. Required for single-tenant apps.""" + application_id_uri: Optional[str] = None + """Application ID URI from the Azure portal. Used for user authentication. + Matches webApplicationInfo.resource in the app manifest.""" token: Optional[Callable[[Union[str, list[str]], Optional[str]], Union[str, Awaitable[str]]]] = None """Custom token provider function. If provided with client_id (no client_secret), uses TokenCredentials.""" managed_identity_client_id: Optional[str] = None diff --git a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py index 243463b32..9f273a190 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py +++ b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py @@ -261,7 +261,6 @@ async def sign_in(self, options: Optional[SignInOptions] = None) -> Optional[str token_exchange_state = TokenExchangeState( connection_name=connection_name, conversation=self.conversation_ref, - relates_to=self.activity.relates_to, ms_app_id=self.app_id, ) @@ -273,8 +272,6 @@ async def sign_in(self, options: Optional[SignInOptions] = None) -> Optional[str one_on_one_conversation = await self.api.conversations.create( CreateConversationParams( tenant_id=self.activity.conversation.tenant_id, - is_group=False, - bot=self.activity.recipient, members=[self.activity.from_], ) ) @@ -288,7 +285,7 @@ async def sign_in(self, options: Optional[SignInOptions] = None) -> Optional[str resource_params = GetBotSignInResourceParams(state=state) resource = await self.api.bots.sign_in.get_resource(resource_params) - payload = MessageActivityInput(recipient=self.activity.from_, input_hint="acceptingInput").add_attachments( + payload = MessageActivityInput(recipient=self.activity.from_).add_attachments( card_attachment( attachment=OAuthCardAttachment( content=OAuthCard( diff --git a/packages/apps/src/microsoft_teams/apps/routing/activity_handlers.py b/packages/apps/src/microsoft_teams/apps/routing/activity_handlers.py index d44cc87d5..1ad23d3db 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/activity_handlers.py +++ b/packages/apps/src/microsoft_teams/apps/routing/activity_handlers.py @@ -4,16 +4,22 @@ """ from abc import ABC, abstractmethod -from typing import Awaitable, Callable, Optional, Pattern, Union, overload +from typing import Any, Awaitable, Callable, Dict, Optional, Pattern, Union, cast, overload from microsoft_teams.api import ( ActivityBase, + AdaptiveCardInvokeActivity, + AdaptiveCardInvokeResponse, MessageActivity, + TaskFetchInvokeActivity, + TaskModuleInvokeResponse, + TaskSubmitInvokeActivity, ) from .activity_context import ActivityContext from .generated_handlers import GeneratedActivityHandlerMixin from .router import ActivityRouter +from .type_helpers import InvokeHandler, InvokeHandlerUnion from .type_validation import validate_handler_type @@ -112,3 +118,437 @@ def selector(ctx: ActivityBase) -> bool: if handler is not None: return decorator(handler) return decorator + + @overload + def on_dialog_open( + self, + ) -> Callable[ + [InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse]], + InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse], + ]: + """ + Register a global dialog open handler for all dialog open events. + + Usage: + + @app.on_dialog_open + async def handle_all_dialogs(ctx: ActivityContext[TaskFetchInvokeActivity]) -> TaskModuleInvokeResponse: + return InvokeResponse(...) + + """ + ... + + @overload + def on_dialog_open( + self, + dialog_id_or_handler: InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse], + ) -> InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse]: + """ + Register a global dialog open handler for all dialog open events. + + Usage: + + async def handle_all_dialogs(ctx: ActivityContext[TaskFetchInvokeActivity]) -> TaskModuleInvokeResponse: + return InvokeResponse(...) + app.on_dialog_open(handle_all_dialogs) + + """ + ... + + @overload + def on_dialog_open( + self, dialog_id_or_handler: str + ) -> Callable[ + [InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse]], + InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse], + ]: + """ + Register a dialog open handler that matches a specific dialog_id. + + Args: + dialog_id_or_handler: The dialog identifier to match against the 'dialog_id' field in activity data + + Usage: + + @app.on_dialog_open("simple_form") + async def handle_simple_form_open( + ctx: ActivityContext[TaskFetchInvokeActivity] + ) -> TaskModuleInvokeResponse: + return InvokeResponse(...) + + """ + ... + + @overload + def on_dialog_open( + self, + dialog_id_or_handler: str, + handler: InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse], + ) -> InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse]: + """ + Register a dialog open handler that matches a specific dialog_id. + + Args: + dialog_id_or_handler: The dialog identifier to match against the 'dialog_id' field in activity data + handler: The async function to call when the dialog_id matches + + Usage: + + async def handle_simple_form_open( + ctx: ActivityContext[TaskFetchInvokeActivity] + ) -> TaskModuleInvokeResponse: + return InvokeResponse(...) + app.on_dialog_open("simple_form", handle_simple_form_open) + + """ + ... + + def on_dialog_open( + self, + dialog_id_or_handler: Union[str, InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse], None] = None, + handler: Optional[InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse]] = None, + ) -> InvokeHandlerUnion[TaskFetchInvokeActivity, TaskModuleInvokeResponse]: + """ + Register a dialog open handler. + + Args: + dialog_id_or_handler: Optional dialog identifier to match against the 'dialog_id' field in activity data, + or a handler function to match all dialog open events. + handler: The async function to call when the event matches + + Returns: + Decorated function or decorator + """ + + # Handle case where first argument is actually a handler function (no dialog_id) + if callable(dialog_id_or_handler): + handler = dialog_id_or_handler + dialog_id_or_handler = None + + def decorator( + func: InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse], + ) -> InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse]: + validate_handler_type(func, TaskFetchInvokeActivity, "on_dialog_open", "TaskFetchInvokeActivity") + + def selector(ctx: ActivityBase) -> bool: + if not isinstance(ctx, TaskFetchInvokeActivity): + return False + # If no dialog_id specified, match all dialog open events + if dialog_id_or_handler is None: + return True + # Otherwise, match specific dialog_id + data = ctx.value.data if ctx.value else None + if not isinstance(data, dict): + return False + data = cast(Dict[str, Any], data) + dialog_id = data.get("dialog_id") + if dialog_id is not None and not isinstance(dialog_id, str): + return False + return dialog_id == dialog_id_or_handler + + self.router.add_handler(selector, func) + return func + + if handler is not None: + return decorator(handler) + return decorator + + @overload + def on_dialog_submit( + self, + ) -> Callable[ + [InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse]], + InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse], + ]: + """ + Register a global dialog submit handler for all dialog submit events. + + Usage: + + @app.on_dialog_submit + async def handle_all_submits(ctx: ActivityContext[TaskSubmitInvokeActivity]) -> TaskModuleInvokeResponse: + return InvokeResponse(...) + + """ + ... + + @overload + def on_dialog_submit( + self, + action_or_handler: InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse], + ) -> InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse]: + """ + Register a global dialog submit handler for all dialog submit events. + + Usage: + + async def handle_all_submits(ctx: ActivityContext[TaskSubmitInvokeActivity]) -> TaskModuleInvokeResponse: + return InvokeResponse(...) + app.on_dialog_submit(handle_all_submits) + + """ + ... + + @overload + def on_dialog_submit( + self, action_or_handler: str + ) -> Callable[ + [InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse]], + InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse], + ]: + """ + Register a dialog submit handler that matches a specific action. + + Args: + action_or_handler: The action identifier to match against the 'action' field in activity data + + Usage: + + @app.on_dialog_submit("submit_user_form") + async def handle_user_form_submit( + ctx: ActivityContext[TaskSubmitInvokeActivity] + ) -> TaskModuleInvokeResponse: + return InvokeResponse(...) + + """ + ... + + @overload + def on_dialog_submit( + self, + action_or_handler: str, + handler: InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse], + ) -> InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse]: + """ + Register a dialog submit handler that matches a specific action. + + Args: + action_or_handler: The action identifier to match against the 'action' field in activity data + handler: The async function to call when the action matches + + Usage: + + async def handle_user_form_submit( + ctx: ActivityContext[TaskSubmitInvokeActivity] + ) -> TaskModuleInvokeResponse: + return InvokeResponse(...) + app.on_dialog_submit("submit_user_form", handle_user_form_submit) + + """ + ... + + def on_dialog_submit( + self, + action_or_handler: Union[str, InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse], None] = None, + handler: Optional[InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse]] = None, + ) -> InvokeHandlerUnion[TaskSubmitInvokeActivity, TaskModuleInvokeResponse]: + """ + Register a dialog submit handler. + + Args: + action_or_handler: Optional action identifier to match against the 'action' field in activity data, + or a handler function to match all dialog submit events. + handler: The async function to call when the event matches + + Returns: + Decorated function or decorator + """ + + # Handle case where first argument is actually a handler function (no action) + if callable(action_or_handler): + handler = action_or_handler + action_or_handler = None + + def decorator( + func: InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse], + ) -> InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse]: + validate_handler_type(func, TaskSubmitInvokeActivity, "on_dialog_submit", "TaskSubmitInvokeActivity") + + def selector(ctx: ActivityBase) -> bool: + if not isinstance(ctx, TaskSubmitInvokeActivity): + return False + # If no action specified, match all dialog submit events + if action_or_handler is None: + return True + # Otherwise, match specific action + data = ctx.value.data if ctx.value else None + if not isinstance(data, dict): + return False + data = cast(Dict[str, Any], data) + action = data.get("action") + if action is not None and not isinstance(action, str): + return False + return action == action_or_handler + + self.router.add_handler(selector, func) + return func + + if handler is not None: + return decorator(handler) + return decorator + + @overload + def on_card_action_execute( + self, + ) -> Callable[ + [InvokeHandler[AdaptiveCardInvokeActivity, AdaptiveCardInvokeResponse]], + InvokeHandler[AdaptiveCardInvokeActivity, AdaptiveCardInvokeResponse], + ]: + """ + Register a global handler for all Action.Execute card actions. + + Usage: + + @app.on_card_action_execute + async def handle_all_execute_actions( + ctx: ActivityContext[AdaptiveCardInvokeActivity], + ) -> AdaptiveCardInvokeResponse: + return AdaptiveCardActionMessageResponse(...) + + """ + ... + + @overload + def on_card_action_execute( + self, + action_or_handler: InvokeHandler[AdaptiveCardInvokeActivity, AdaptiveCardInvokeResponse], + ) -> InvokeHandler[AdaptiveCardInvokeActivity, AdaptiveCardInvokeResponse]: + """ + Register a global handler for all Action.Execute card actions. + + Usage: + + async def handle_all_execute_actions( + ctx: ActivityContext[AdaptiveCardInvokeActivity], + ) -> AdaptiveCardInvokeResponse: + return AdaptiveCardActionMessageResponse(...) + app.on_card_action_execute(handle_all_execute_actions) + + """ + ... + + @overload + def on_card_action_execute( + self, action_or_handler: str + ) -> Callable[ + [InvokeHandler[AdaptiveCardInvokeActivity, AdaptiveCardInvokeResponse]], + InvokeHandler[AdaptiveCardInvokeActivity, AdaptiveCardInvokeResponse], + ]: + """ + Register a handler for Action.Execute card actions with a specific action identifier. + + Only Action.Execute is supported. Action.Submit and other action types do not trigger + AdaptiveCardInvokeActivity in modern Teams clients. + + Args: + action_or_handler: The action identifier to match against the 'action' field in activity data + + Usage: + + @app.on_card_action_execute("submit_basic") + async def handle_basic_submit( + ctx: ActivityContext[AdaptiveCardInvokeActivity] + ) -> AdaptiveCardInvokeResponse: + return AdaptiveCardActionMessageResponse(...) + + """ + ... + + @overload + def on_card_action_execute( + self, + action_or_handler: str, + handler: InvokeHandler[AdaptiveCardInvokeActivity, AdaptiveCardInvokeResponse], + ) -> InvokeHandler[AdaptiveCardInvokeActivity, AdaptiveCardInvokeResponse]: + """ + Register a handler for Action.Execute card actions with a specific action identifier. + + Only Action.Execute is supported. Action.Submit and other action types do not trigger + AdaptiveCardInvokeActivity in modern Teams clients. + + Args: + action_or_handler: The action identifier to match against the 'action' field in activity data + handler: The async function to call when the action matches + + Usage: + + async def handle_basic_submit( + ctx: ActivityContext[AdaptiveCardInvokeActivity] + ) -> AdaptiveCardInvokeResponse: + return AdaptiveCardActionMessageResponse(...) + app.on_card_action_execute("submit_basic", handle_basic_submit) + + """ + ... + + def on_card_action_execute( + self, + action_or_handler: Union[ + str, InvokeHandler[AdaptiveCardInvokeActivity, AdaptiveCardInvokeResponse], None + ] = None, + handler: Optional[InvokeHandler[AdaptiveCardInvokeActivity, AdaptiveCardInvokeResponse]] = None, + ) -> InvokeHandlerUnion[AdaptiveCardInvokeActivity, AdaptiveCardInvokeResponse]: + """ + Register a handler for Action.Execute card actions. + + This handler provides action-based routing for ExecuteAction (Action.Execute) buttons. + Only Action.Execute is supported - Action.Submit and other action types do not trigger + AdaptiveCardInvokeActivity in modern Teams clients. + + Args: + action_or_handler: Optional action identifier to match against the 'action' field in activity data, + or a handler function to match all Action.Execute events. + handler: The async function to call when the event matches + + Returns: + Decorated function or decorator + """ + + # Handle case where first argument is actually a handler function (no action) + if callable(action_or_handler): + handler = action_or_handler + action_or_handler = None + + def decorator( + func: InvokeHandler[AdaptiveCardInvokeActivity, AdaptiveCardInvokeResponse], + ) -> InvokeHandler[AdaptiveCardInvokeActivity, AdaptiveCardInvokeResponse]: + validate_handler_type( + func, + AdaptiveCardInvokeActivity, + "on_card_action_execute", + "AdaptiveCardInvokeActivity", + ) + + def selector(ctx: ActivityBase) -> bool: + if not isinstance(ctx, AdaptiveCardInvokeActivity): + return False + + # If no action specified, match all Action.Execute events + if action_or_handler is None: + # Still validate it's Action.Execute for global handler + if not ctx.value or not ctx.value.action: + return False + if ctx.value.action.type != "Action.Execute": + return False + return True + + # Otherwise, match specific action with Action.Execute validation + if not ctx.value or not ctx.value.action: + return False + + # Extract and match action field + if ctx.value.action.type != "Action.Execute": + return False + data = ctx.value.action.data + action = data.get("action") + if action is not None and not isinstance(action, str): + return False + + return action == action_or_handler + + self.router.add_handler(selector, func) + return func + + if handler is not None: + return decorator(handler) + return decorator diff --git a/packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py b/packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py index 2feecac4e..fe13c3c49 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py +++ b/packages/apps/src/microsoft_teams/apps/routing/activity_route_configs.py @@ -15,7 +15,6 @@ ConfigSubmitInvokeActivity, ConversationUpdateActivity, CustomBaseModel, - EndOfConversationActivity, EventActivity, ExecuteActionInvokeActivity, FileConsentInvokeActivity, @@ -50,9 +49,6 @@ TabFetchInvokeActivity, TabInvokeResponse, TabSubmitInvokeActivity, - TaskFetchInvokeActivity, - TaskModuleInvokeResponse, - TaskSubmitInvokeActivity, TraceActivity, TypingActivity, UninstalledActivity, @@ -244,13 +240,6 @@ class ActivityConfig: and activity.channel_data.event_type == "teamUnarchived", output_model=None, ), - "end_of_conversation": ActivityConfig( - name="end_of_conversation", - method_name="on_end_of_conversation", - input_model=EndOfConversationActivity, - selector=lambda activity: isinstance(activity, EndOfConversationActivity), - output_model=None, - ), # Complex Union Activities (discriminated by sub-fields) "event": ActivityConfig( name="event", @@ -424,24 +413,26 @@ class ActivityConfig: output_model=None, is_invoke=True, ), - "dialog.open": ActivityConfig( - name="dialog.open", - method_name="on_dialog_open", - input_model=TaskFetchInvokeActivity, - selector=lambda activity: activity.type == "invoke" and cast(InvokeActivity, activity).name == "task/fetch", - output_model=TaskModuleInvokeResponse, - output_type_name="TaskModuleInvokeResponse", - is_invoke=True, - ), - "dialog.submit": ActivityConfig( - name="dialog.submit", - method_name="on_dialog_submit", - input_model=TaskSubmitInvokeActivity, - selector=lambda activity: activity.type == "invoke" and cast(InvokeActivity, activity).name == "task/submit", - output_model=TaskModuleInvokeResponse, - output_type_name="TaskModuleInvokeResponse", - is_invoke=True, - ), + # Note: dialog.open and dialog.submit are manually implemented in activity_handlers.py + # They have overloaded versions that accept dialog_id/action parameters for routing + # "dialog.open": ActivityConfig( + # name="dialog.open", + # method_name="on_dialog_open", + # input_model=TaskFetchInvokeActivity, + # selector=lambda activity: activity.type == "invoke" and cast(InvokeActivity, activity).name == "task/fetch", + # output_model=TaskModuleInvokeResponse, + # output_type_name="TaskModuleInvokeResponse", + # is_invoke=True, + # ), + # "dialog.submit": ActivityConfig( + # name="dialog.submit", + # method_name="on_dialog_submit", + # input_model=TaskSubmitInvokeActivity, + # selector=lambda activity: activity.type == "invoke" and cast(InvokeActivity, activity).name == "task/submit", + # output_model=TaskModuleInvokeResponse, + # output_type_name="TaskModuleInvokeResponse", + # is_invoke=True, + # ), "tab.open": ActivityConfig( name="tab.open", method_name="on_tab_open", diff --git a/packages/apps/src/microsoft_teams/apps/routing/generated_handlers.py b/packages/apps/src/microsoft_teams/apps/routing/generated_handlers.py index c1a8b7104..803fc8aa8 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/generated_handlers.py +++ b/packages/apps/src/microsoft_teams/apps/routing/generated_handlers.py @@ -20,7 +20,6 @@ ConfigFetchInvokeActivity, ConfigSubmitInvokeActivity, ConversationUpdateActivity, - EndOfConversationActivity, EventActivity, ExecuteActionInvokeActivity, FileConsentInvokeActivity, @@ -53,8 +52,6 @@ SignInVerifyStateInvokeActivity, TabFetchInvokeActivity, TabSubmitInvokeActivity, - TaskFetchInvokeActivity, - TaskSubmitInvokeActivity, TraceActivity, TypingActivity, UninstalledActivity, @@ -65,7 +62,6 @@ MessagingExtensionActionInvokeResponse, MessagingExtensionInvokeResponse, TabInvokeResponse, - TaskModuleInvokeResponse, TokenExchangeInvokeResponseType, ) @@ -583,33 +579,6 @@ def decorator(func: BasicHandler[ConversationUpdateActivity]) -> BasicHandler[Co return decorator(handler) return decorator - @overload - def on_end_of_conversation( - self, handler: BasicHandler[EndOfConversationActivity] - ) -> BasicHandler[EndOfConversationActivity]: ... - - @overload - def on_end_of_conversation( - self, - ) -> Callable[[BasicHandler[EndOfConversationActivity]], BasicHandler[EndOfConversationActivity]]: ... - - def on_end_of_conversation( - self, handler: Optional[BasicHandler[EndOfConversationActivity]] = None - ) -> BasicHandlerUnion[EndOfConversationActivity]: - """Register a end_of_conversation activity handler.""" - - def decorator(func: BasicHandler[EndOfConversationActivity]) -> BasicHandler[EndOfConversationActivity]: - validate_handler_type( - func, EndOfConversationActivity, "on_end_of_conversation", "EndOfConversationActivity" - ) - config = ACTIVITY_ROUTES["end_of_conversation"] - self.router.add_handler(config.selector, func) - return func - - if handler is not None: - return decorator(handler) - return decorator - @overload def on_event(self, handler: BasicHandler[EventActivity]) -> BasicHandler[EventActivity]: ... @@ -1222,66 +1191,6 @@ def decorator( return decorator(handler) return decorator - @overload - def on_dialog_open( - self, handler: InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse] - ) -> InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse]: ... - - @overload - def on_dialog_open( - self, - ) -> Callable[ - [InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse]], - InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse], - ]: ... - - def on_dialog_open( - self, handler: Optional[InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse]] = None - ) -> InvokeHandlerUnion[TaskFetchInvokeActivity, TaskModuleInvokeResponse]: - """Register a dialog.open activity handler.""" - - def decorator( - func: InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse], - ) -> InvokeHandler[TaskFetchInvokeActivity, TaskModuleInvokeResponse]: - validate_handler_type(func, TaskFetchInvokeActivity, "on_dialog_open", "TaskFetchInvokeActivity") - config = ACTIVITY_ROUTES["dialog.open"] - self.router.add_handler(config.selector, func) - return func - - if handler is not None: - return decorator(handler) - return decorator - - @overload - def on_dialog_submit( - self, handler: InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse] - ) -> InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse]: ... - - @overload - def on_dialog_submit( - self, - ) -> Callable[ - [InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse]], - InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse], - ]: ... - - def on_dialog_submit( - self, handler: Optional[InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse]] = None - ) -> InvokeHandlerUnion[TaskSubmitInvokeActivity, TaskModuleInvokeResponse]: - """Register a dialog.submit activity handler.""" - - def decorator( - func: InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse], - ) -> InvokeHandler[TaskSubmitInvokeActivity, TaskModuleInvokeResponse]: - validate_handler_type(func, TaskSubmitInvokeActivity, "on_dialog_submit", "TaskSubmitInvokeActivity") - config = ACTIVITY_ROUTES["dialog.submit"] - self.router.add_handler(config.selector, func) - return func - - if handler is not None: - return decorator(handler) - return decorator - @overload def on_tab_open( self, handler: InvokeHandler[TabFetchInvokeActivity, TabInvokeResponse] diff --git a/packages/apps/tests/test_activity_context.py b/packages/apps/tests/test_activity_context.py index ab1346b68..96fdfbd9b 100644 --- a/packages/apps/tests/test_activity_context.py +++ b/packages/apps/tests/test_activity_context.py @@ -114,7 +114,7 @@ async def test_targeted_with_different_recipient(self) -> None: the recipient should not be overridden. """ incoming_sender = Account(id="user-123", name="Test User") - explicit_recipient = Account(id="other-user-456", name="Other User", role="user") + explicit_recipient = Account(id="other-user-456", name="Other User") ctx, mock_sender = self._create_activity_context(from_account=incoming_sender) # Create a targeted message with explicit recipient @@ -353,7 +353,6 @@ async def test_sign_in_returns_token_when_already_available(self) -> None: mock_activity = MagicMock() mock_activity.channel_id = "msteams" mock_activity.from_.id = "user-001" - mock_activity.relates_to = None mock_activity.conversation.is_group = False ctx, _ = _create_activity_context(activity=mock_activity) diff --git a/packages/apps/tests/test_card_action_routing.py b/packages/apps/tests/test_card_action_routing.py new file mode 100644 index 000000000..d7fc44c8d --- /dev/null +++ b/packages/apps/tests/test_card_action_routing.py @@ -0,0 +1,283 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" +# pyright: basic + +from unittest.mock import MagicMock + +import pytest +from microsoft_teams.api import ( + Account, + AdaptiveCardInvokeActivity, + AdaptiveCardInvokeResponse, + ConversationAccount, +) +from microsoft_teams.api.models.adaptive_card import ( + AdaptiveCardActionMessageResponse, + AdaptiveCardInvokeAction, + AdaptiveCardInvokeValue, +) +from microsoft_teams.apps import ActivityContext, App + + +class TestCardActionExecuteRouting: + """Test cases for card action execute routing functionality.""" + + @pytest.fixture + def mock_storage(self): + """Create a mock storage.""" + return MagicMock() + + @pytest.fixture(scope="function") + def app_with_options(self, mock_storage): + """Create an app with basic options.""" + return App( + storage=mock_storage, + client_id="test-client-id", + client_secret="test-secret", + ) + + def test_on_card_action_execute_with_action_id(self, app_with_options: App) -> None: + """Test on_card_action_execute with specific action matching.""" + + @app_with_options.on_card_action_execute("submit_form") + async def handle_submit_form(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse: + return AdaptiveCardActionMessageResponse( + status_code=200, type="application/vnd.microsoft.activity.message", value="Form submitted" + ) + + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + # Test matching action + matching_activity = AdaptiveCardInvokeActivity( + id="test-activity-id", + type="invoke", + name="adaptiveCard/action", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=AdaptiveCardInvokeValue( + action=AdaptiveCardInvokeAction(type="Action.Execute", data={"action": "submit_form"}) + ), + ) + + # Test non-matching action + non_matching_activity = AdaptiveCardInvokeActivity( + id="test-activity-id-2", + type="invoke", + name="adaptiveCard/action", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=AdaptiveCardInvokeValue( + action=AdaptiveCardInvokeAction(type="Action.Execute", data={"action": "other_action"}) + ), + ) + + # Verify handler was registered and can match + handlers = app_with_options.router.select_handlers(matching_activity) + assert len(handlers) == 1 + assert handlers[0] == handle_submit_form + + # Verify non-matching action doesn't match + non_matching_handlers = app_with_options.router.select_handlers(non_matching_activity) + assert len(non_matching_handlers) == 0 + + def test_on_card_action_execute_global_handler(self, app_with_options: App) -> None: + """Test on_card_action_execute without action matches all Action.Execute actions.""" + + @app_with_options.on_card_action_execute() + async def handle_all_actions(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse: + return AdaptiveCardActionMessageResponse( + status_code=200, type="application/vnd.microsoft.activity.message", value="Action received" + ) + + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + # Test with any action + activity1 = AdaptiveCardInvokeActivity( + id="test-activity-id-1", + type="invoke", + name="adaptiveCard/action", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=AdaptiveCardInvokeValue( + action=AdaptiveCardInvokeAction(type="Action.Execute", data={"action": "action1"}) + ), + ) + + activity2 = AdaptiveCardInvokeActivity( + id="test-activity-id-2", + type="invoke", + name="adaptiveCard/action", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=AdaptiveCardInvokeValue( + action=AdaptiveCardInvokeAction(type="Action.Execute", data={"action": "action2"}) + ), + ) + + # Both should match the global handler + handlers1 = app_with_options.router.select_handlers(activity1) + assert len(handlers1) == 1 + assert handlers1[0] == handle_all_actions + + handlers2 = app_with_options.router.select_handlers(activity2) + assert len(handlers2) == 1 + assert handlers2[0] == handle_all_actions + + def test_on_card_action_execute_multiple_specific_handlers(self, app_with_options: App) -> None: + """Test multiple specific action handlers coexist correctly.""" + + @app_with_options.on_card_action_execute("submit_form") + async def handle_submit_form(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse: + return AdaptiveCardActionMessageResponse( + status_code=200, type="application/vnd.microsoft.activity.message", value="Form submitted" + ) + + @app_with_options.on_card_action_execute("save_data") + async def handle_save_data(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse: + return AdaptiveCardActionMessageResponse( + status_code=200, type="application/vnd.microsoft.activity.message", value="Data saved" + ) + + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + submit_activity = AdaptiveCardInvokeActivity( + id="test-activity-id-1", + type="invoke", + name="adaptiveCard/action", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=AdaptiveCardInvokeValue( + action=AdaptiveCardInvokeAction(type="Action.Execute", data={"action": "submit_form"}) + ), + ) + + save_activity = AdaptiveCardInvokeActivity( + id="test-activity-id-2", + type="invoke", + name="adaptiveCard/action", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=AdaptiveCardInvokeValue( + action=AdaptiveCardInvokeAction(type="Action.Execute", data={"action": "save_data"}) + ), + ) + + # Each should match only its specific handler + submit_handlers = app_with_options.router.select_handlers(submit_activity) + assert len(submit_handlers) == 1 + assert submit_handlers[0] == handle_submit_form + + save_handlers = app_with_options.router.select_handlers(save_activity) + assert len(save_handlers) == 1 + assert save_handlers[0] == handle_save_data + + def test_on_card_action_execute_decorator_syntax(self, app_with_options: App) -> None: + """Test on_card_action_execute works with decorator syntax.""" + + @app_with_options.on_card_action_execute("test_action") + async def decorated_handler(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse: + return AdaptiveCardActionMessageResponse( + status_code=200, type="application/vnd.microsoft.activity.message", value="Decorated" + ) + + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + activity = AdaptiveCardInvokeActivity( + id="test-activity-id", + type="invoke", + name="adaptiveCard/action", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=AdaptiveCardInvokeValue( + action=AdaptiveCardInvokeAction(type="Action.Execute", data={"action": "test_action"}) + ), + ) + + handlers = app_with_options.router.select_handlers(activity) + assert len(handlers) == 1 + assert handlers[0] == decorated_handler + + def test_on_card_action_execute_non_decorator_syntax(self, app_with_options: App) -> None: + """Test on_card_action_execute works with non-decorator syntax.""" + + async def handler_function(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse: + return AdaptiveCardActionMessageResponse( + status_code=200, type="application/vnd.microsoft.activity.message", value="Non-decorated" + ) + + app_with_options.on_card_action_execute("non_decorated_action", handler_function) + + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + activity = AdaptiveCardInvokeActivity( + id="test-activity-id", + type="invoke", + name="adaptiveCard/action", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=AdaptiveCardInvokeValue( + action=AdaptiveCardInvokeAction(type="Action.Execute", data={"action": "non_decorated_action"}) + ), + ) + + handlers = app_with_options.router.select_handlers(activity) + assert len(handlers) == 1 + assert handlers[0] == handler_function + + def test_on_card_action_execute_missing_action_field(self, app_with_options: App) -> None: + """Test on_card_action_execute handler doesn't match when action field is missing.""" + + @app_with_options.on_card_action_execute("submit_form") + async def handle_submit_form(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse: + return AdaptiveCardActionMessageResponse( + status_code=200, type="application/vnd.microsoft.activity.message", value="Form submitted" + ) + + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + # Activity with no action field in data + activity = AdaptiveCardInvokeActivity( + id="test-activity-id", + type="invoke", + name="adaptiveCard/action", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=AdaptiveCardInvokeValue( + action=AdaptiveCardInvokeAction(type="Action.Execute", data={"other_field": "value"}) + ), + ) + + handlers = app_with_options.router.select_handlers(activity) + assert len(handlers) == 0 diff --git a/packages/apps/tests/test_dialog_routing.py b/packages/apps/tests/test_dialog_routing.py new file mode 100644 index 000000000..1850a4af4 --- /dev/null +++ b/packages/apps/tests/test_dialog_routing.py @@ -0,0 +1,498 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" +# pyright: basic + +from unittest.mock import MagicMock + +import pytest +from microsoft_teams.api import ( + Account, + AdaptiveCardAttachment, + CardTaskModuleTaskInfo, + ConversationAccount, + InvokeResponse, + TaskFetchInvokeActivity, + TaskModuleContinueResponse, + TaskModuleMessageResponse, + TaskModuleRequest, + TaskModuleResponse, + TaskSubmitInvokeActivity, + card_attachment, +) +from microsoft_teams.apps import ActivityContext, App +from microsoft_teams.cards import AdaptiveCard + + +class TestDialogRouting: + """Test cases for dialog routing functionality.""" + + @pytest.fixture + def mock_storage(self): + """Create a mock storage.""" + return MagicMock() + + @pytest.fixture(scope="function") + def app_with_options(self, mock_storage): + """Create an app with basic options.""" + return App( + storage=mock_storage, + client_id="test-client-id", + client_secret="test-secret", + ) + + def test_on_dialog_open_with_dialog_id(self, app_with_options: App) -> None: + """Test on_dialog_open with specific dialog_id matching.""" + + @app_with_options.on_dialog_open("test_dialog") + async def handle_test_dialog(ctx: ActivityContext[TaskFetchInvokeActivity]) -> TaskModuleResponse: + return TaskModuleResponse(task=TaskModuleMessageResponse(value="Test dialog opened")) + + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + # Test matching dialog_id + matching_activity = TaskFetchInvokeActivity( + id="test-activity-id", + type="invoke", + name="task/fetch", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data={"dialog_id": "test_dialog"}), + ) + + # Test non-matching dialog_id + non_matching_activity = TaskFetchInvokeActivity( + id="test-activity-id-2", + type="invoke", + name="task/fetch", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data={"dialog_id": "other_dialog"}), + ) + + # Verify handler was registered and can match + handlers = app_with_options.router.select_handlers(matching_activity) + assert len(handlers) == 1 + assert handlers[0] == handle_test_dialog + + # Verify non-matching dialog_id doesn't match + non_matching_handlers = app_with_options.router.select_handlers(non_matching_activity) + assert len(non_matching_handlers) == 0 + + def test_on_dialog_open_global_handler(self, app_with_options: App) -> None: + """Test on_dialog_open without dialog_id matches all dialog opens.""" + + @app_with_options.on_dialog_open() + async def handle_all_dialogs(ctx: ActivityContext[TaskFetchInvokeActivity]) -> TaskModuleResponse: + return TaskModuleResponse(task=TaskModuleMessageResponse(value="Any dialog opened")) + + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + # Test with dialog_id present + activity_with_id = TaskFetchInvokeActivity( + id="test-activity-id", + type="invoke", + name="task/fetch", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data={"dialog_id": "some_dialog"}), + ) + + # Test without dialog_id + activity_without_id = TaskFetchInvokeActivity( + id="test-activity-id-2", + type="invoke", + name="task/fetch", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data={}), + ) + + # Both should match global handler + handlers_with_id = app_with_options.router.select_handlers(activity_with_id) + assert len(handlers_with_id) == 1 + assert handlers_with_id[0] == handle_all_dialogs + + handlers_without_id = app_with_options.router.select_handlers(activity_without_id) + assert len(handlers_without_id) == 1 + assert handlers_without_id[0] == handle_all_dialogs + + def test_on_dialog_open_with_non_dict_data(self, app_with_options: App) -> None: + """Test on_dialog_open handles non-dict data gracefully.""" + + @app_with_options.on_dialog_open("test_dialog") + async def handle_test_dialog(ctx: ActivityContext[TaskFetchInvokeActivity]) -> TaskModuleResponse: + return TaskModuleResponse(task=TaskModuleMessageResponse(value="Test")) + + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + # Test with non-dict data (should not match) + activity = TaskFetchInvokeActivity( + id="test-activity-id", + type="invoke", + name="task/fetch", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data="not a dict"), + ) + + handlers = app_with_options.router.select_handlers(activity) + assert len(handlers) == 0 + + def test_on_dialog_submit_with_action(self, app_with_options: App) -> None: + """Test on_dialog_submit with specific action matching.""" + + @app_with_options.on_dialog_submit("submit_form") + async def handle_form_submit(ctx: ActivityContext[TaskSubmitInvokeActivity]) -> TaskModuleResponse: + return TaskModuleResponse(task=TaskModuleMessageResponse(value="Form submitted")) + + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + # Test matching action + matching_activity = TaskSubmitInvokeActivity( + id="test-activity-id", + type="invoke", + name="task/submit", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data={"action": "submit_form", "name": "John"}), + ) + + # Test non-matching action + non_matching_activity = TaskSubmitInvokeActivity( + id="test-activity-id-2", + type="invoke", + name="task/submit", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data={"action": "cancel_form"}), + ) + + # Verify handler was registered and can match + handlers = app_with_options.router.select_handlers(matching_activity) + assert len(handlers) == 1 + assert handlers[0] == handle_form_submit + + # Verify non-matching action doesn't match + non_matching_handlers = app_with_options.router.select_handlers(non_matching_activity) + assert len(non_matching_handlers) == 0 + + def test_on_dialog_submit_global_handler(self, app_with_options: App) -> None: + """Test on_dialog_submit without action matches all dialog submits.""" + + @app_with_options.on_dialog_submit() + async def handle_all_submits(ctx: ActivityContext[TaskSubmitInvokeActivity]) -> TaskModuleResponse: + return TaskModuleResponse(task=TaskModuleMessageResponse(value="Any submit")) + + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + # Test with action present + activity_with_action = TaskSubmitInvokeActivity( + id="test-activity-id", + type="invoke", + name="task/submit", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data={"action": "some_action"}), + ) + + # Test without action + activity_without_action = TaskSubmitInvokeActivity( + id="test-activity-id-2", + type="invoke", + name="task/submit", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data={"name": "John"}), + ) + + # Both should match global handler + handlers_with_action = app_with_options.router.select_handlers(activity_with_action) + assert len(handlers_with_action) == 1 + assert handlers_with_action[0] == handle_all_submits + + handlers_without_action = app_with_options.router.select_handlers(activity_without_action) + assert len(handlers_without_action) == 1 + assert handlers_without_action[0] == handle_all_submits + + def test_on_dialog_open_non_decorator_syntax(self, app_with_options: App) -> None: + """Test on_dialog_open using non-decorator syntax.""" + + async def handle_dialog(ctx: ActivityContext[TaskFetchInvokeActivity]) -> TaskModuleResponse: + return TaskModuleResponse(task=TaskModuleMessageResponse(value="Dialog opened")) + + app_with_options.on_dialog_open("my_dialog", handle_dialog) + + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + activity = TaskFetchInvokeActivity( + id="test-activity-id", + type="invoke", + name="task/fetch", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data={"dialog_id": "my_dialog"}), + ) + + handlers = app_with_options.router.select_handlers(activity) + assert len(handlers) == 1 + assert handlers[0] == handle_dialog + + def test_on_dialog_submit_non_decorator_syntax(self, app_with_options: App) -> None: + """Test on_dialog_submit using non-decorator syntax.""" + + async def handle_submit(ctx: ActivityContext[TaskSubmitInvokeActivity]) -> TaskModuleResponse: + return TaskModuleResponse(task=TaskModuleMessageResponse(value="Submitted")) + + app_with_options.on_dialog_submit("my_action", handle_submit) + + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + activity = TaskSubmitInvokeActivity( + id="test-activity-id", + type="invoke", + name="task/submit", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data={"action": "my_action"}), + ) + + handlers = app_with_options.router.select_handlers(activity) + assert len(handlers) == 1 + assert handlers[0] == handle_submit + + def test_on_dialog_open_handler_as_first_arg(self, app_with_options: App) -> None: + """Test on_dialog_open with handler as first argument (global handler).""" + + async def handle_all(ctx: ActivityContext[TaskFetchInvokeActivity]) -> TaskModuleResponse: + return TaskModuleResponse(task=TaskModuleMessageResponse(value="All")) + + app_with_options.on_dialog_open(handle_all) + + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + activity = TaskFetchInvokeActivity( + id="test-activity-id", + type="invoke", + name="task/fetch", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data={"dialog_id": "any"}), + ) + + handlers = app_with_options.router.select_handlers(activity) + assert len(handlers) == 1 + assert handlers[0] == handle_all + + def test_on_dialog_submit_handler_as_first_arg(self, app_with_options: App) -> None: + """Test on_dialog_submit with handler as first argument (global handler).""" + + async def handle_all(ctx: ActivityContext[TaskSubmitInvokeActivity]) -> TaskModuleResponse: + return TaskModuleResponse(task=TaskModuleMessageResponse(value="All")) + + app_with_options.on_dialog_submit(handle_all) + + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + activity = TaskSubmitInvokeActivity( + id="test-activity-id", + type="invoke", + name="task/submit", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data={"action": "any"}), + ) + + handlers = app_with_options.router.select_handlers(activity) + assert len(handlers) == 1 + assert handlers[0] == handle_all + + def test_multiple_dialog_handlers(self, app_with_options: App) -> None: + """Test multiple dialog handlers can coexist.""" + + @app_with_options.on_dialog_open("dialog_a") + async def handle_dialog_a(ctx: ActivityContext[TaskFetchInvokeActivity]) -> TaskModuleResponse: + return TaskModuleResponse(task=TaskModuleMessageResponse(value="Dialog A")) + + @app_with_options.on_dialog_open("dialog_b") + async def handle_dialog_b(ctx: ActivityContext[TaskFetchInvokeActivity]) -> TaskModuleResponse: + return TaskModuleResponse(task=TaskModuleMessageResponse(value="Dialog B")) + + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + activity_a = TaskFetchInvokeActivity( + id="test-activity-id", + type="invoke", + name="task/fetch", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data={"dialog_id": "dialog_a"}), + ) + + activity_b = TaskFetchInvokeActivity( + id="test-activity-id-2", + type="invoke", + name="task/fetch", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data={"dialog_id": "dialog_b"}), + ) + + # Verify each handler only matches its specific dialog_id + handlers_a = app_with_options.router.select_handlers(activity_a) + assert len(handlers_a) == 1 + assert handlers_a[0] == handle_dialog_a + + handlers_b = app_with_options.router.select_handlers(activity_b) + assert len(handlers_b) == 1 + assert handlers_b[0] == handle_dialog_b + + def test_on_dialog_open_returns_unwrapped_response(self, app_with_options: App) -> None: + """Test that handlers can return TaskModuleResponse directly (unwrapped from InvokeResponse).""" + + @app_with_options.on_dialog_open("test_dialog") + async def handle_dialog(ctx: ActivityContext[TaskFetchInvokeActivity]) -> TaskModuleResponse: + # Return unwrapped TaskModuleResponse (not InvokeResponse[TaskModuleResponse]) + card = AdaptiveCard(version="1.4", body=[]) + attachment = card_attachment(AdaptiveCardAttachment(content=card)) + return TaskModuleResponse( + task=TaskModuleContinueResponse(value=CardTaskModuleTaskInfo(title="Test", card=attachment)) + ) + + # The type system should accept this - this test verifies type compatibility + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + activity = TaskFetchInvokeActivity( + id="test-activity-id", + type="invoke", + name="task/fetch", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data={"dialog_id": "test_dialog"}), + ) + + handlers = app_with_options.router.select_handlers(activity) + assert len(handlers) == 1 + assert handlers[0] == handle_dialog + + def test_on_dialog_open_global_and_specific_both_fire(self, app_with_options: App) -> None: + """Both global and specific handlers fire when an activity matches the specific dialog_id. + Handlers run in registration order.""" + + @app_with_options.on_dialog_open() + async def handle_all(ctx: ActivityContext[TaskFetchInvokeActivity]) -> TaskModuleResponse: + return TaskModuleResponse(task=TaskModuleMessageResponse(value="global")) + + @app_with_options.on_dialog_open("my_form") + async def handle_my_form(ctx: ActivityContext[TaskFetchInvokeActivity]) -> TaskModuleResponse: + return TaskModuleResponse(task=TaskModuleMessageResponse(value="specific")) + + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + # Activity matching the specific dialog_id — both handlers should fire + matching_activity = TaskFetchInvokeActivity( + id="test-activity-id", + type="invoke", + name="task/fetch", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data={"dialog_id": "my_form"}), + ) + + handlers = app_with_options.router.select_handlers(matching_activity) + assert len(handlers) == 2 + assert handlers[0] == handle_all # global registered first + assert handlers[1] == handle_my_form + + def test_on_dialog_open_returns_wrapped_response(self, app_with_options: App) -> None: + """Test that handlers can also return InvokeResponse[TaskModuleResponse] (wrapped).""" + + @app_with_options.on_dialog_open("test_dialog") + async def handle_dialog(ctx: ActivityContext[TaskFetchInvokeActivity]): + # Return wrapped InvokeResponse[TaskModuleResponse] + card = AdaptiveCard(version="1.4", body=[]) + attachment = card_attachment(AdaptiveCardAttachment(content=card)) + return InvokeResponse( + body=TaskModuleResponse( + task=TaskModuleContinueResponse(value=CardTaskModuleTaskInfo(title="Test", card=attachment)) + ) + ) + + # The type system should accept this too - verifies backward compatibility + from_account = Account(id="user-123", name="Test User", role="user") + recipient = Account(id="bot-456", name="Test Bot", role="bot") + conversation = ConversationAccount(id="conv-789", conversation_type="personal") + + activity = TaskFetchInvokeActivity( + id="test-activity-id", + type="invoke", + name="task/fetch", + from_=from_account, + recipient=recipient, + conversation=conversation, + channel_id="msteams", + value=TaskModuleRequest(data={"dialog_id": "test_dialog"}), + ) + + handlers = app_with_options.router.select_handlers(activity) + assert len(handlers) == 1 + assert handlers[0] == handle_dialog diff --git a/packages/apps/tests/test_token_validator.py b/packages/apps/tests/test_token_validator.py index 54a84ad31..b7534012a 100644 --- a/packages/apps/tests/test_token_validator.py +++ b/packages/apps/tests/test_token_validator.py @@ -32,6 +32,13 @@ def mock_signing_key(self): mock_key.key = "mock-rsa-key" return mock_key + @pytest.fixture + def mock_jwks_client(self, mock_signing_key): + """Create mock PyJWKClient that returns the mock signing key.""" + client = MagicMock() + client.get_signing_key_from_jwt.return_value = mock_signing_key + return client + @pytest.fixture def valid_payload(self): """Create valid JWT payload.""" @@ -59,18 +66,20 @@ def test_init(self): validator = TokenValidator.for_service("test-app-id") assert validator.options.valid_issuers == ["https://api.botframework.com"] - assert validator.options.valid_audiences == ["test-app-id", "api://test-app-id"] + assert validator.options.valid_audiences == [ + "test-app-id", + "api://test-app-id", + "api://botid-test-app-id", + ] assert validator.options.jwks_uri == "https://login.botframework.com/v1/.well-known/keys" @pytest.mark.asyncio - async def test_validate_token_success(self, validator, mock_signing_key, valid_payload): + async def test_validate_token_success(self, validator, mock_jwks_client, valid_payload): """Test successful token validation.""" token = "valid.jwt.token" - with ( - patch("jwt.PyJWKClient", return_value=mock_signing_key), - patch("jwt.decode", return_value=valid_payload), - ): + validator._jwks_client = mock_jwks_client + with patch("jwt.decode", return_value=valid_payload): result = await validator.validate_token(token) assert isinstance(result, dict) @@ -78,15 +87,13 @@ async def test_validate_token_success(self, validator, mock_signing_key, valid_p assert result["aud"] == "test-app-id" @pytest.mark.asyncio - async def test_validate_token_with_service_url(self, validator, mock_signing_key, valid_payload): + async def test_validate_token_with_service_url(self, validator, mock_jwks_client, valid_payload): """Test successful token validation with service URL check.""" token = "valid.jwt.token" service_url = "https://smba.trafficmanager.net/teams" - with ( - patch("jwt.PyJWKClient", return_value=mock_signing_key), - patch("jwt.decode", return_value=valid_payload), - ): + validator._jwks_client = mock_jwks_client + with patch("jwt.decode", return_value=valid_payload): result = await validator.validate_token(token, service_url) assert isinstance(result, dict) @@ -110,51 +117,71 @@ async def test_validate_token_jwks_error(self, validator): """Test validation when JWKS client fails.""" token = "invalid.jwt.token" - with patch( - "jwt.PyJWKClient", - side_effect=jwt.DecodeError("Invalid token format"), - ): - with pytest.raises(jwt.InvalidTokenError): - await validator.validate_token(token) + mock_client = MagicMock() + mock_client.get_signing_key_from_jwt.side_effect = jwt.DecodeError("Invalid token format") + validator._jwks_client = mock_client + with pytest.raises(jwt.InvalidTokenError): + await validator.validate_token(token) @pytest.mark.asyncio - async def test_validate_token_decode_error(self, validator, mock_signing_key): + async def test_validate_token_decode_error(self, validator, mock_jwks_client): """Test validation when JWT decode fails.""" token = "invalid.jwt.token" - with ( - patch("jwt.PyJWKClient", return_value=mock_signing_key), - patch("jwt.decode", side_effect=jwt.ExpiredSignatureError("Token expired")), - ): + validator._jwks_client = mock_jwks_client + with patch("jwt.decode", side_effect=jwt.ExpiredSignatureError("Token expired")): with pytest.raises(jwt.InvalidTokenError): await validator.validate_token(token) @pytest.mark.asyncio - async def test_validate_token_invalid_audience(self, validator, mock_signing_key): + @pytest.mark.parametrize( + "audience", + [ + "test-app-id", + "api://test-app-id", + "api://botid-test-app-id", + ], + ids=["app_id", "api://app_id", "api://botid-app_id"], + ) + async def test_validate_token_accepts_all_audience_formats(self, mock_jwks_client, audience): + """Test that all three audience formats are accepted.""" + validator = TokenValidator.for_service("test-app-id") + validator._jwks_client = mock_jwks_client + token = "valid.jwt.token" + payload = { + "iss": "https://api.botframework.com", + "aud": audience, + "serviceurl": "https://smba.trafficmanager.net/teams", + "exp": 9999999999, + "iat": 1000000000, + } + + with patch("jwt.decode", return_value=payload): + result = await validator.validate_token(token) + assert result["aud"] == audience + + @pytest.mark.asyncio + async def test_validate_token_invalid_audience(self, validator, mock_jwks_client): """Test validation with invalid audience.""" token = "invalid.jwt.token" - with ( - patch("jwt.PyJWKClient", return_value=mock_signing_key), - patch("jwt.decode", side_effect=jwt.InvalidAudienceError("Invalid audience")), - ): + validator._jwks_client = mock_jwks_client + with patch("jwt.decode", side_effect=jwt.InvalidAudienceError("Invalid audience")): with pytest.raises(jwt.InvalidTokenError): await validator.validate_token(token) @pytest.mark.asyncio - async def test_validate_token_invalid_issuer(self, validator, mock_signing_key): + async def test_validate_token_invalid_issuer(self, validator, mock_jwks_client): """Test validation with invalid issuer.""" token = "invalid.jwt.token" - with ( - patch("jwt.PyJWKClient", return_value=mock_signing_key), - patch("jwt.decode", side_effect=jwt.InvalidIssuerError("Invalid issuer")), - ): + validator._jwks_client = mock_jwks_client + with patch("jwt.decode", side_effect=jwt.InvalidIssuerError("Invalid issuer")): with pytest.raises(jwt.InvalidTokenError): await validator.validate_token(token) @pytest.mark.asyncio - async def test_service_url_validation_missing_claim(self, validator, mock_signing_key): + async def test_service_url_validation_missing_claim(self, validator, mock_jwks_client): """Test service URL validation when token missing serviceurl claim.""" token = "valid.jwt.token" service_url = "https://smba.trafficmanager.net/teams" @@ -163,15 +190,13 @@ async def test_service_url_validation_missing_claim(self, validator, mock_signin "aud": "test-app-id", } - with ( - patch("jwt.PyJWKClient", return_value=mock_signing_key), - patch("jwt.decode", return_value=payload_without_service_url), - ): + validator._jwks_client = mock_jwks_client + with patch("jwt.decode", return_value=payload_without_service_url): with pytest.raises(jwt.InvalidTokenError, match="Token missing serviceurl claim"): await validator.validate_token(token, service_url) @pytest.mark.asyncio - async def test_service_url_validation_mismatch(self, validator, mock_signing_key): + async def test_service_url_validation_mismatch(self, validator, mock_jwks_client): """Test service URL validation when URLs don't match.""" token = "valid.jwt.token" service_url = "https://smba.trafficmanager.net/teams" @@ -181,15 +206,13 @@ async def test_service_url_validation_mismatch(self, validator, mock_signing_key "serviceurl": "https://different.service.url", } - with ( - patch("jwt.PyJWKClient", return_value=mock_signing_key), - patch("jwt.decode", return_value=payload_with_different_url), - ): + validator._jwks_client = mock_jwks_client + with patch("jwt.decode", return_value=payload_with_different_url): with pytest.raises(jwt.InvalidTokenError, match="Service URL mismatch"): await validator.validate_token(token, service_url) @pytest.mark.asyncio - async def test_service_url_validation_with_trailing_slashes(self, validator, mock_signing_key): + async def test_service_url_validation_with_trailing_slashes(self, validator, mock_jwks_client): """Test service URL validation normalizes trailing slashes.""" token = "valid.jwt.token" service_url = "https://smba.trafficmanager.net/teams/" # With trailing slash @@ -199,10 +222,8 @@ async def test_service_url_validation_with_trailing_slashes(self, validator, moc "serviceurl": "https://smba.trafficmanager.net/teams", # Without trailing slash } - with ( - patch("jwt.PyJWKClient", return_value=mock_signing_key), - patch("jwt.decode", return_value=payload_without_slash), - ): + validator._jwks_client = mock_jwks_client + with patch("jwt.decode", return_value=payload_without_slash): # Should succeed because URLs are normalized result = await validator.validate_token(token, service_url) assert isinstance(result, dict) @@ -230,25 +251,23 @@ def test_for_entra_initialization(self, validator_entra): """Check Entra-specific initialization.""" options = validator_entra.options assert options.valid_issuers == ["https://login.microsoftonline.com/test-tenant-id/v2.0"] - assert options.valid_audiences == ["test-app-id", "api://test-app-id"] + assert options.valid_audiences == ["test-app-id", "api://test-app-id", "api://botid-test-app-id"] assert options.jwks_uri == "https://login.microsoftonline.com/test-tenant-id/discovery/v2.0/keys" assert options.scope == "user.read" @pytest.mark.asyncio async def test_validate_entra_token_success_with_scope( - self, validator_entra, mock_signing_key, valid_payload_entra + self, validator_entra, mock_jwks_client, valid_payload_entra ): """Validate Entra token successfully with required scope.""" token = "entra.valid.token" - with ( - patch("jwt.PyJWKClient", return_value=mock_signing_key), - patch("jwt.decode", return_value=valid_payload_entra), - ): + validator_entra._jwks_client = mock_jwks_client + with patch("jwt.decode", return_value=valid_payload_entra): payload = await validator_entra.validate_token(token) assert payload["scp"] == "user.read mail.read" @pytest.mark.asyncio - async def test_validate_entra_token_missing_scope(self, validator_entra, mock_signing_key): + async def test_validate_entra_token_missing_scope(self, validator_entra, mock_jwks_client): """Fail validation if required scope is missing.""" token = "entra.missing.scope" payload_missing_scope = { @@ -259,15 +278,13 @@ async def test_validate_entra_token_missing_scope(self, validator_entra, mock_si "iat": 1000000000, } - with ( - patch("jwt.PyJWKClient", return_value=mock_signing_key), - patch("jwt.decode", return_value=payload_missing_scope), - ): + validator_entra._jwks_client = mock_jwks_client + with patch("jwt.decode", return_value=payload_missing_scope): with pytest.raises(jwt.InvalidTokenError, match="Token missing required scope: user.read"): await validator_entra.validate_token(token) @pytest.mark.asyncio - async def test_validate_entra_token_invalid_issuer(self, validator_entra, mock_signing_key): + async def test_validate_entra_token_invalid_issuer(self, validator_entra, mock_jwks_client): """Fail validation for invalid issuer.""" token = "entra.invalid.issuer" payload_invalid_issuer = { @@ -278,17 +295,31 @@ async def test_validate_entra_token_invalid_issuer(self, validator_entra, mock_s "iat": 1000000000, } - with ( - patch("jwt.PyJWKClient", return_value=mock_signing_key), - patch( - "jwt.decode", return_value=payload_invalid_issuer, side_effect=jwt.InvalidIssuerError("Invalid issuer") - ), + validator_entra._jwks_client = mock_jwks_client + with patch( + "jwt.decode", return_value=payload_invalid_issuer, side_effect=jwt.InvalidIssuerError("Invalid issuer") ): with pytest.raises(jwt.InvalidTokenError): await validator_entra.validate_token(token) + def test_for_entra_with_application_id_uri(self): + """Check that applicationIdUri is included in valid audiences.""" + validator = TokenValidator.for_entra( + app_id="test-app-id", + tenant_id="test-tenant-id", + application_id_uri="api://my-app.contoso.com/test-app-id", + ) + options = validator.options + assert "api://my-app.contoso.com/test-app-id" in options.valid_audiences + + def test_for_entra_without_application_id_uri(self): + """Check that audiences are default when applicationIdUri is not provided.""" + validator = TokenValidator.for_entra(app_id="test-app-id", tenant_id="test-tenant-id") + options = validator.options + assert options.valid_audiences == ["test-app-id", "api://test-app-id", "api://botid-test-app-id"] + @pytest.mark.asyncio - async def test_validate_entra_token_invalid_audience(self, validator_entra, mock_signing_key): + async def test_validate_entra_token_invalid_audience(self, validator_entra, mock_jwks_client): """Fail validation for invalid audience.""" token = "entra.invalid.aud" payload_invalid_aud = { @@ -299,11 +330,9 @@ async def test_validate_entra_token_invalid_audience(self, validator_entra, mock "iat": 1000000000, } - with ( - patch("jwt.PyJWKClient", return_value=mock_signing_key), - patch( - "jwt.decode", return_value=payload_invalid_aud, side_effect=jwt.InvalidAudienceError("Invalid audience") - ), + validator_entra._jwks_client = mock_jwks_client + with patch( + "jwt.decode", return_value=payload_invalid_aud, side_effect=jwt.InvalidAudienceError("Invalid audience") ): with pytest.raises(jwt.InvalidTokenError): await validator_entra.validate_token(token) diff --git a/packages/cards/src/microsoft/teams/cards/__init__.py b/packages/cards/src/microsoft/teams/cards/__init__.py index ff5b5dc15..c13881d19 100644 --- a/packages/cards/src/microsoft/teams/cards/__init__.py +++ b/packages/cards/src/microsoft/teams/cards/__init__.py @@ -22,7 +22,6 @@ # Import everything from the new namespace from microsoft_teams.cards import * # noqa: E402, F401, F403 -from microsoft_teams.cards import __all__ # noqa: E402, F401 # sys.modules trick to make submodule imports work # This ensures: from microsoft.teams.cards.submodule import X works diff --git a/packages/cards/src/microsoft_teams/cards/__init__.py b/packages/cards/src/microsoft_teams/cards/__init__.py index 98a1850b9..19bc15f5b 100644 --- a/packages/cards/src/microsoft_teams/cards/__init__.py +++ b/packages/cards/src/microsoft_teams/cards/__init__.py @@ -5,12 +5,12 @@ import logging -from . import actions -from .actions import * # noqa: F403 +from . import utilities from .core import * +from .utilities import * # noqa: F403 logging.getLogger(__name__).addHandler(logging.NullHandler()) # Combine all exports from submodules __all__: list[str] = [] -__all__.extend(actions.__all__) +__all__.extend(utilities.__all__) diff --git a/packages/cards/src/microsoft_teams/cards/actions/__init__.py b/packages/cards/src/microsoft_teams/cards/actions/__init__.py deleted file mode 100644 index a684abc0e..000000000 --- a/packages/cards/src/microsoft_teams/cards/actions/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -from .im_back_action import IMBackAction -from .invoke_action import InvokeAction -from .message_back_action import MessageBackAction -from .sign_in_action import SignInAction -from .task_fetch_action import TaskFetchAction - -__all__ = ["IMBackAction", "MessageBackAction", "SignInAction", "InvokeAction", "TaskFetchAction"] diff --git a/packages/cards/src/microsoft_teams/cards/actions/im_back_action.py b/packages/cards/src/microsoft_teams/cards/actions/im_back_action.py deleted file mode 100644 index d77cbe7e8..000000000 --- a/packages/cards/src/microsoft_teams/cards/actions/im_back_action.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -from ..core import ImBackSubmitActionData, SubmitAction, SubmitActionData - - -class IMBackAction(SubmitAction): - """Initial data that input fields will be combined with. These are essentially ‘hidden’ properties.""" - - def __init__(self, value: str): - super().__init__() - action_data = ImBackSubmitActionData().with_value(value) - self.data = SubmitActionData(ms_teams=action_data.model_dump()) diff --git a/packages/cards/src/microsoft_teams/cards/actions/invoke_action.py b/packages/cards/src/microsoft_teams/cards/actions/invoke_action.py deleted file mode 100644 index 08af726f3..000000000 --- a/packages/cards/src/microsoft_teams/cards/actions/invoke_action.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -from typing import Any, Dict - -from ..core import InvokeSubmitActionData, SubmitAction, SubmitActionData - - -class InvokeAction(SubmitAction): - def __init__(self, value: Dict[str, Any]): - super().__init__() - action_data = InvokeSubmitActionData().with_value(value) - self.data = SubmitActionData(ms_teams=action_data.model_dump()) diff --git a/packages/cards/src/microsoft_teams/cards/actions/message_back_action.py b/packages/cards/src/microsoft_teams/cards/actions/message_back_action.py deleted file mode 100644 index 627c70da5..000000000 --- a/packages/cards/src/microsoft_teams/cards/actions/message_back_action.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -from typing import Optional - -from ..core import MessageBackSubmitActionData, SubmitAction, SubmitActionData - - -class MessageBackAction(SubmitAction): - def __init__(self, text: str, value: str, display_text: Optional[str] = None): - super().__init__() - action_data = MessageBackSubmitActionData().with_value(value).with_text(text) - - if display_text: - action_data = action_data.with_display_text(display_text) - - self.data = SubmitActionData(ms_teams=action_data.model_dump()) diff --git a/packages/cards/src/microsoft_teams/cards/actions/sign_in_action.py b/packages/cards/src/microsoft_teams/cards/actions/sign_in_action.py deleted file mode 100644 index d81e85322..000000000 --- a/packages/cards/src/microsoft_teams/cards/actions/sign_in_action.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -from ..core import SigninSubmitActionData, SubmitAction, SubmitActionData - - -class SignInAction(SubmitAction): - def __init__(self, value: str): - super().__init__() - action_data = SigninSubmitActionData().with_value(value) - self.data = SubmitActionData(ms_teams=action_data.model_dump()) diff --git a/packages/cards/src/microsoft_teams/cards/actions/task_fetch_action.py b/packages/cards/src/microsoft_teams/cards/actions/task_fetch_action.py deleted file mode 100644 index fc2cbd2d6..000000000 --- a/packages/cards/src/microsoft_teams/cards/actions/task_fetch_action.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -from typing import Any, Dict - -from ..core import SubmitAction, SubmitActionData, TaskFetchSubmitActionData - - -class TaskFetchAction(SubmitAction): - def __init__(self, value: Dict[str, Any]): - super().__init__() - # For task/fetch, the action data actually goes in the SubmitActionData, not with - # msteams. msteams simply contains { type: 'task/fetch' } - self.data = SubmitActionData(**value).with_ms_teams(TaskFetchSubmitActionData().model_dump()) diff --git a/packages/cards/src/microsoft_teams/cards/core.py b/packages/cards/src/microsoft_teams/cards/core.py index 2677ee581..1ec829bf6 100644 --- a/packages/cards/src/microsoft_teams/cards/core.py +++ b/packages/cards/src/microsoft_teams/cards/core.py @@ -1,13 +1,7 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -# This file was automatically generated by a tool on 08/20/2025, 10:35 PM UTC. +# This file was automatically generated by a tool on 03/31/2026, 12:11 AM UTC. DO NOT UPDATE MANUALLY. # It includes declarations for Adaptive Card features available in Teams, Copilot, Outlook, Word, Excel, PowerPoint. -from typing import Any, Dict, List, Literal, Optional, Self, Union - +from typing import Dict, List, Any, Optional, Union, Literal, Self from pydantic import AliasGenerator, BaseModel, ConfigDict, Field, SerializeAsAny from pydantic.alias_generators import to_camel @@ -18,46 +12,46 @@ class SerializableObject(BaseModel): @staticmethod def validation_alias_generator(field: str) -> str: "Handles deserialization aliasing" - + # Handle parameters that start with "@" if field.startswith("at_"): return f"@{field[3:]}" - + # Handles from field which is a duplicate reserved internal name if field == "from_": return "from" - + # Handle ms_teams field which should deserialize from msteams if field == "ms_teams": return "msteams" - + # Handle choices_data field which should deserialize from choices.data if field == "choices_data": return "choices.data" - + # All other fields are converted to camelCase return to_camel(field) - + @staticmethod def serialization_alias_generator(field: str) -> str: "Handles serialization aliasing and casing" - + # Handle parameters that start with "@" if field.startswith("at_"): return f"@{field[3:]}" - + # Handles from field which is a duplicate reserved internal name if field == "from_": return "from" - + # Handle ms_teams field which should serialize to msteams if field == "ms_teams": return "msteams" - + # Handle choices_data field which should serialize to choices.data if field == "choices_data": return "choices.data" - + # All other fields are converted to camelCase return to_camel(field) @@ -70,24 +64,19 @@ def serialization_alias_generator(field: str) -> str: alias_generator=AliasGenerator( validation_alias=validation_alias_generator, serialization_alias=serialization_alias_generator ), - ) + ) class CardElement(SerializableObject): """Base class for CardElement.""" - pass - class Action(SerializableObject): """Base class for Action.""" - pass - class ContainerLayout(SerializableObject): """Base class for ContainerLayout.""" - pass @@ -95,47 +84,24 @@ class ContainerLayout(SerializableObject): ActionMode = Literal["primary", "secondary"] -AssociatedInputs = Literal["auto", "none"] +ThemeName = Literal["Light", "Dark"] -ImageInsertPosition = Literal["Selection", "Top", "Bottom"] +ElementHeight = Literal["auto", "stretch"] -FallbackAction = Literal["drop"] +HorizontalAlignment = Literal["Left", "Center", "Right"] -ContainerStyle = Literal["default", "emphasis", "accent", "good", "attention", "warning"] +Spacing = Literal["None", "ExtraSmall", "Small", "Default", "Medium", "Large", "ExtraLarge", "Padding"] -TargetWidth = Literal[ - "VeryNarrow", - "Narrow", - "Standard", - "Wide", - "atLeast:VeryNarrow", - "atMost:VeryNarrow", - "atLeast:Narrow", - "atMost:Narrow", - "atLeast:Standard", - "atMost:Standard", - "atLeast:Wide", - "atMost:Wide", -] +TargetWidth = Literal["VeryNarrow", "Narrow", "Standard", "Wide", "atLeast:VeryNarrow", "atMost:VeryNarrow", "atLeast:Narrow", "atMost:Narrow", "atLeast:Standard", "atMost:Standard", "atLeast:Wide", "atMost:Wide"] -HorizontalAlignment = Literal["Left", "Center", "Right"] +ContainerStyle = Literal["default", "emphasis", "accent", "good", "attention", "warning"] VerticalAlignment = Literal["Top", "Center", "Bottom"] FlowLayoutItemFit = Literal["Fit", "Fill"] -Spacing = Literal["None", "ExtraSmall", "Small", "Default", "Medium", "Large", "ExtraLarge", "Padding"] - FillMode = Literal["Cover", "RepeatHorizontally", "RepeatVertically", "Repeat"] -Version = Literal["1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6"] - -TeamsCardWidth = Literal["full"] - -MentionType = Literal["Person", "Tag"] - -ElementHeight = Literal["auto", "stretch"] - TextSize = Literal["Small", "Default", "Medium", "Large", "ExtraLarge"] TextWeight = Literal["Lighter", "Default", "Bolder"] @@ -144,14 +110,20 @@ class ContainerLayout(SerializableObject): FontType = Literal["Default", "Monospace"] -StyleEnum = Literal["compact", "expanded", "filtered"] +TextBlockStyle = Literal["default", "columnHeader", "heading"] ImageStyle = Literal["Default", "Person", "RoundedCorners"] Size = Literal["Auto", "Stretch", "Small", "Medium", "Large"] +ImageFitMode = Literal["Cover", "Contain", "Fill"] + InputTextStyle = Literal["Text", "Tel", "Url", "Email", "Password"] +AssociatedInputs = Literal["auto", "none"] + +ChoiceSetInputStyle = Literal["compact", "expanded", "filtered"] + RatingSize = Literal["Medium", "Large"] RatingColor = Literal["Neutral", "Marigold"] @@ -174,72 +146,27 @@ class ContainerLayout(SerializableObject): BadgeStyle = Literal["Default", "Subtle", "Informative", "Accent", "Good", "Attention", "Warning"] -ChartColorSet = Literal["categorical", "sequential", "diverging"] - -ChartColor = Literal[ - "good", - "warning", - "attention", - "neutral", - "categoricalRed", - "categoricalPurple", - "categoricalLavender", - "categoricalBlue", - "categoricalLightBlue", - "categoricalTeal", - "categoricalGreen", - "categoricalLime", - "categoricalMarigold", - "sequential1", - "sequential2", - "sequential3", - "sequential4", - "sequential5", - "sequential6", - "sequential7", - "sequential8", - "divergingBlue", - "divergingLightBlue", - "divergingCyan", - "divergingTeal", - "divergingYellow", - "divergingPeach", - "divergingLightRed", - "divergingRed", - "divergingMaroon", - "divergingGray", -] +ProgressRingLabelPosition = Literal["Before", "After", "Above", "Below"] + +ProgressRingSize = Literal["Tiny", "Small", "Medium", "Large"] + +ProgressBarColor = Literal["Accent", "Good", "Warning", "Attention"] + +ChartColorSet = Literal["categorical", "sequential", "sequentialred", "sequentialgreen", "sequentialyellow", "diverging"] + +ChartColor = Literal["good", "warning", "attention", "neutral", "categoricalRed", "categoricalPurple", "categoricalLavender", "categoricalBlue", "categoricalLightBlue", "categoricalTeal", "categoricalGreen", "categoricalLime", "categoricalMarigold", "sequential1", "sequential2", "sequential3", "sequential4", "sequential5", "sequential6", "sequential7", "sequential8", "divergingBlue", "divergingLightBlue", "divergingCyan", "divergingTeal", "divergingYellow", "divergingPeach", "divergingLightRed", "divergingRed", "divergingMaroon", "divergingGray", "sequentialRed1", "sequentialRed2", "sequentialRed3", "sequentialRed4", "sequentialRed5", "sequentialRed6", "sequentialRed7", "sequentialRed8", "sequentialGreen1", "sequentialGreen2", "sequentialGreen3", "sequentialGreen4", "sequentialGreen5", "sequentialGreen6", "sequentialGreen7", "sequentialGreen8", "sequentialYellow1", "sequentialYellow2", "sequentialYellow3", "sequentialYellow4", "sequentialYellow5", "sequentialYellow6", "sequentialYellow7", "sequentialYellow8"] + +DonutThickness = Literal["Thin", "Thick"] HorizontalBarChartDisplayMode = Literal["AbsoluteWithAxis", "AbsoluteNoAxis", "PartToWhole"] GaugeChartValueFormat = Literal["Percentage", "Fraction"] -CodeLanguage = Literal[ - "Bash", - "C", - "Cpp", - "CSharp", - "Css", - "Dos", - "Go", - "Graphql", - "Html", - "Java", - "JavaScript", - "Json", - "ObjectiveC", - "Perl", - "Php", - "PlainText", - "PowerShell", - "Python", - "Sql", - "TypeScript", - "VbNet", - "Verilog", - "Vhdl", - "Xml", -] +CodeLanguage = Literal["Bash", "C", "Cpp", "CSharp", "Css", "Dos", "Go", "Graphql", "Html", "Java", "JavaScript", "Json", "ObjectiveC", "Perl", "Php", "PlainText", "PowerShell", "Python", "Sql", "TypeScript", "VbNet", "Verilog", "Vhdl", "Xml"] + +PersonaIconStyle = Literal["profilePicture", "contactCard", "none"] + +PersonaDisplayStyle = Literal["iconAndName", "iconOnly", "nameOnly"] FallbackElement = Literal["drop"] @@ -247,27 +174,67 @@ class ContainerLayout(SerializableObject): SizeEnum = Literal["Small", "Default", "Medium", "Large", "ExtraLarge"] -ThemeName = Literal["Light", "Dark"] +PopoverPosition = Literal["Above", "Below", "Before", "After"] + +FallbackAction = Literal["drop"] + +ImageInsertPosition = Literal["Selection", "Top", "Bottom"] + +Version = Literal["1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6"] + +TeamsCardWidth = Literal["full"] + +MentionType = Literal["Person", "Tag"] class HostCapabilities(SerializableObject): """Represents a list of versioned capabilities a host application must support.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + + def with_key(self, value: str) -> Self: + self.key = value + return self + +class ThemedUrl(SerializableObject): + """Defines a theme-specific URL.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + theme: Optional[ThemeName] = "Light" + """ The theme this URL applies to. """ + url: Optional[str] = None + """ The URL to use for the associated theme. """ + + def with_key(self, value: str) -> Self: + self.key = value + return self + + def with_theme(self, value: ThemeName) -> Self: + self.theme = value + return self + def with_url(self, value: str) -> Self: + self.url = value + return self class BackgroundImage(SerializableObject): """Defines a container's background image and the way it should be rendered.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ url: Optional[str] = None """ The URL (or Base64-encoded Data URI) of the image. Acceptable formats are PNG, JPEG, GIF and SVG. """ - - fill_mode: Optional[FillMode] = None + fill_mode: Optional[FillMode] = "Cover" """ Controls how the image should fill the area. """ - - horizontal_alignment: Optional[HorizontalAlignment] = None + horizontal_alignment: Optional[HorizontalAlignment] = "Left" """ Controls how the image should be aligned if it must be cropped or if using repeat fill mode. """ - - vertical_alignment: Optional[VerticalAlignment] = None + vertical_alignment: Optional[VerticalAlignment] = "Top" """ Controls how the image should be aligned if it must be cropped or if using repeat fill mode. """ + themed_urls: Optional[List[ThemedUrl]] = None + """ A set of theme-specific image URLs. """ + + def with_key(self, value: str) -> Self: + self.key = value + return self def with_url(self, value: str) -> Self: self.url = value @@ -285,75 +252,73 @@ def with_vertical_alignment(self, value: VerticalAlignment) -> Self: self.vertical_alignment = value return self + def with_themed_urls(self, value: List[ThemedUrl]) -> Self: + self.themed_urls = value + return self class SubmitActionData(SerializableObject): - """Represents the data of an Action.Submit. This model can include arbitrary data""" - - ms_teams: Optional[Dict[str, Any]] = None + """Represents the data of an Action.Submit.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + msteams: Optional[Dict[str, Any]] = None """ Defines the optional Teams-specific portion of the action's data. """ - def with_ms_teams(self, value: Dict[str, Any]) -> Self: - self.ms_teams = value + def with_key(self, value: str) -> Self: + self.key = value return self - def with_data(self, value: Dict[str, Any]) -> Self: - for k, v in value.items(): - setattr(self, k, v) + def with_msteams(self, value: Dict[str, Any]) -> Self: + self.msteams = value return self - class ExecuteAction(Action): """Gathers input values, merges them with the data property if specified, and sends them to the Bot via an Invoke activity. The Bot can respond synchronously and return an updated Adaptive Card to be displayed by the client. Action.Execute works in all Adaptive Card hosts.""" - - type: Literal["Action.Execute"] = "Action.Execute" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Action.Execute'] = 'Action.Execute' """ Must be **Action.Execute**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - title: Optional[str] = None """ The title of the action, as it appears on buttons. """ - icon_url: Optional[str] = None """ A URL (or Base64-encoded Data URI) to a PNG, GIF, JPEG or SVG image to be displayed on the left of the action's title. `iconUrl` also accepts the `[,regular|filled]` format to display an icon from the vast [Adaptive Card icon catalog](topic:icon-catalog) instead of an image. """ - - style: Optional[ActionStyle] = None + style: Optional[ActionStyle] = "default" """ Control the style of the action, affecting its visual and spoken representations. """ - - mode: Optional[ActionMode] = None + mode: Optional[ActionMode] = "primary" """ Controls if the action is primary or secondary. Secondary actions appear in an overflow menu. """ - tooltip: Optional[str] = None """ The tooltip text to display when the action is hovered over. """ - is_enabled: Optional[bool] = True """ Controls the enabled state of the action. A disabled action cannot be clicked. If the action is represented as a button, the button's style will reflect this state. """ - - data: Optional[Union[str, Dict[str, Any], SubmitActionData]] = None + menu_actions: Optional[List[SerializeAsAny[Action]]] = None + """ The actions to display in the overflow menu of a Split action button. """ + themed_icon_urls: Optional[List[ThemedUrl]] = None + """ A set of theme-specific icon URLs. """ + data: Optional[Union[str, SubmitActionData, Dict[str, Any]]] = None """ The data to send to the Bot when the action is executed. When expressed as an object, `data` is sent back to the Bot when the action is executed, adorned with the values of the inputs expressed as key/value pairs, where the key is the Id of the input. If `data` is expressed as a string, input values are not sent to the Bot. """ - associated_inputs: Optional[AssociatedInputs] = None """ The Ids of the inputs associated with the Action.Submit. When the action is executed, the values of the associated inputs are sent to the Bot. See [Input validation](topic:input-validation) for more details. """ - conditionally_enabled: Optional[bool] = None """ Controls if the action is enabled only if at least one required input has been filled by the user. """ - verb: Optional[str] = None """ The verb of the action. """ - fallback: Optional[Union[SerializeAsAny[Action], FallbackAction]] = None """ An alternate action to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -381,7 +346,15 @@ def with_is_enabled(self, value: bool) -> Self: self.is_enabled = value return self - def with_data(self, value: Union[str, Dict[str, Any], SubmitActionData]) -> Self: + def with_menu_actions(self, value: List[Action]) -> Self: + self.menu_actions = value + return self + + def with_themed_icon_urls(self, value: List[ThemedUrl]) -> Self: + self.themed_icon_urls = value + return self + + def with_data(self, value: Union[str, SubmitActionData, Dict[str, Any]]) -> Self: self.data = value return self @@ -401,16 +374,19 @@ def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: self.fallback = value return self - class RefreshDefinition(SerializableObject): """Defines how a card can be refreshed by making a request to the target Bot.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ action: Optional[ExecuteAction] = None """ The Action.Execute action to invoke to refresh the card. """ - user_ids: Optional[List[str]] = None """ The list of user Ids for which the card will be automatically refreshed. In Teams, in chats or channels with more than 60 users, the card will automatically refresh only for users specified in the userIds list. Other users will have to manually click on a "refresh" button. In contexts with fewer than 60 users, the card will automatically refresh for all users. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_action(self, value: ExecuteAction) -> Self: self.action = value return self @@ -419,22 +395,23 @@ def with_user_ids(self, value: List[str]) -> Self: self.user_ids = value return self - class AuthCardButton(SerializableObject): """Defines a button as displayed when prompting a user to authenticate. For more information, refer to the [Bot Framework CardAction type](https://docs.microsoft.com/dotnet/api/microsoft.bot.schema.cardaction).""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ type: Optional[str] = None """ Must be **signin**. """ - title: Optional[str] = None """ The caption of the button. """ - image: Optional[str] = None """ A URL to an image to display alongside the button’s caption. """ - value: Optional[str] = None """ The value associated with the button. The meaning of value depends on the button’s type. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_type(self, value: str) -> Self: self.type = value return self @@ -451,19 +428,21 @@ def with_value(self, value: str) -> Self: self.value = value return self - class TokenExchangeResource(SerializableObject): """Defines information required to enable on-behalf-of single sign-on user authentication. For more information, refer to the [Bot Framework TokenExchangeResource type](https://docs.microsoft.com/dotnet/api/microsoft.bot.schema.tokenexchangeresource)""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ id: Optional[str] = None """ The unique identified of this token exchange instance. """ - uri: Optional[str] = None """ An application ID or resource identifier with which to exchange a token on behalf of. This property is identity provider- and application-specific. """ - provider_id: Optional[str] = None """ An identifier for the identity provider with which to attempt a token exchange. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self @@ -476,22 +455,23 @@ def with_provider_id(self, value: str) -> Self: self.provider_id = value return self - class Authentication(SerializableObject): """Defines authentication information associated with a card. For more information, refer to the [Bot Framework OAuthCard type](https://docs.microsoft.com/dotnet/api/microsoft.bot.schema.oauthcard)""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ text: Optional[str] = None """ The text that can be displayed to the end user when prompting them to authenticate. """ - connection_name: Optional[str] = None """ The identifier for registered OAuth connection setting information. """ - buttons: Optional[List[AuthCardButton]] = None """ The buttons that should be displayed to the user when prompting for authentication. The array MUST contain one button of type “signin”. Other button types are not currently supported. """ - token_exchange_resource: Optional[TokenExchangeResource] = None """ Provides information required to enable on-behalf-of single sign-on user authentication. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_text(self, value: str) -> Self: self.text = value return self @@ -508,19 +488,21 @@ def with_token_exchange_resource(self, value: TokenExchangeResource) -> Self: self.token_exchange_resource = value return self - class MentionedEntity(SerializableObject): """Represents a mentioned person or tag.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ id: Optional[str] = None """ The Id of a person (typically a Microsoft Entra user Id) or tag. """ - name: Optional[str] = None """ The name of the mentioned entity. """ - - mention_type: Optional[MentionType] = None + mention_type: Optional[MentionType] = "Person" """ The type of the mentioned entity. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self @@ -533,19 +515,21 @@ def with_mention_type(self, value: MentionType) -> Self: self.mention_type = value return self - class Mention(SerializableObject): """Represents a mention to a person.""" - - type: Literal["mention"] = "mention" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['mention'] = 'mention' """ Must be **mention**. """ - text: Optional[str] = None """ The text that will be substituted with the mention. """ - mentioned: Optional[MentionedEntity] = None """ Defines the entity being mentioned. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_text(self, value: str) -> Self: self.text = value return self @@ -554,18 +538,21 @@ def with_mentioned(self, value: MentionedEntity) -> Self: self.mentioned = value return self - class TeamsCardProperties(SerializableObject): """Represents a set of Teams-specific properties on a card.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ width: Optional[TeamsCardWidth] = None """ Controls the width of the card in a Teams chat. Note that setting `width` to "full" will not actually stretch the card to the "full width" of the chat pane. It will only make the card wider than when the `width` property isn't set. """ - entities: Optional[List[Mention]] = None """ The Teams-specific entities associated with the card. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_width(self, value: TeamsCardWidth) -> Self: self.width = value return self @@ -574,98 +561,121 @@ def with_entities(self, value: List[Mention]) -> Self: self.entities = value return self - class CardMetadata(SerializableObject): """Card-level metadata.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ web_url: Optional[str] = None """ The URL the card originates from. When `webUrl` is set, the card is dubbed an **Adaptive Card-based Loop Component** and, when pasted in Teams or other Loop Component-capable host applications, the URL will unfurl to the same exact card. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_web_url(self, value: str) -> Self: self.web_url = value return self +class StringResource(SerializableObject): + """Defines the replacement string values.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + default_value: Optional[str] = None + """ The default value of the string, which is used when no matching localized value is found. """ + localized_values: Optional[Dict[str, str]] = None + """ Localized values of the string, where keys represent the locale (e.g. `en-US`) in the `(-)` format. `` is the 2-letter language code and `` is the optional 2-letter country code. """ + + def with_key(self, value: str) -> Self: + self.key = value + return self + + def with_default_value(self, value: str) -> Self: + self.default_value = value + return self + + def with_localized_values(self, value: Dict[str, str]) -> Self: + self.localized_values = value + return self + +class Resources(SerializableObject): + """The resources that can be used in the body of the card.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + strings: Optional[Dict[str, StringResource]] = None + """ String resources that can provide translations in multiple languages. String resources make it possible to craft cards that are automatically localized according to the language settings of the application that displays the card. """ + + def with_key(self, value: str) -> Self: + self.key = value + return self + + def with_strings(self, value: Dict[str, StringResource]) -> Self: + self.strings = value + return self class AdaptiveCard(CardElement): """An Adaptive Card, containing a free-form body of card elements, and an optional set of actions.""" - - type: Literal["AdaptiveCard"] = "AdaptiveCard" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['AdaptiveCard'] = 'AdaptiveCard' """ Must be **AdaptiveCard**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - - select_action: Optional[Action] = None + select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the element is tapped or clicked. Action.ShowCard is not supported. """ - style: Optional[ContainerStyle] = None """ The style of the container. Container styles control the colors of the background, border and text inside the container, in such a way that contrast requirements are always met. """ - layouts: Optional[List[SerializeAsAny[ContainerLayout]]] = None """ The layouts associated with the container. The container can dynamically switch from one layout to another as the card's width changes. See [Container layouts](topic:container-layouts) for more details. """ - min_height: Optional[str] = None """ The minimum height, in pixels, of the container, in the `px` format. """ - background_image: Optional[Union[str, BackgroundImage]] = None """ Defines the container's background image. """ - vertical_content_alignment: Optional[VerticalAlignment] = None """ Controls how the container's content should be vertically aligned. """ - rtl: Optional[bool] = None """ Controls if the content of the card is to be rendered left-to-right or right-to-left. """ - ac_schema: Optional[str] = Field(default=None, alias="schema") """ A URL to the Adaptive Card schema the card is authored against. """ - - version: Optional[Version] = None + version: Optional[Version] = "1.5" """ The Adaptive Card schema version the card is authored against. """ - fallback_text: Optional[str] = None """ The text that should be displayed if the client is not able to render the card. """ - speak: Optional[str] = None """ The text that should be spoken for the entire card. """ - refresh: Optional[RefreshDefinition] = None """ Defines how the card can be refreshed by making a request to the target Bot. """ - authentication: Optional[Authentication] = None """ Defines authentication information to enable on-behalf-of single-sign-on or just-in-time OAuth. This information is used in conjunction with the refresh property and Action.Execute in general. """ - - ms_teams: Optional[TeamsCardProperties] = None + msteams: Optional[TeamsCardProperties] = None """ Teams-specific metadata associated with the card. """ - metadata: Optional[CardMetadata] = None """ Metadata associated with the card. """ - + resources: Optional[Resources] = None + """ Resources card elements can reference. """ grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ - body: Optional[List[SerializeAsAny[CardElement]]] = None """ The body of the card, comprised of a list of elements displayed according to the layouts property. If the layouts property is not specified, a Layout.Stack is used. """ - actions: Optional[List[SerializeAsAny[Action]]] = None """ The card level actions, which always appear at the bottom of the card. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -705,7 +715,7 @@ def with_rtl(self, value: bool) -> Self: self.rtl = value return self - def withac_schema(self, value: str) -> Self: + def with_schema(self, value: str) -> Self: self.ac_schema = value return self @@ -729,14 +739,18 @@ def with_authentication(self, value: Authentication) -> Self: self.authentication = value return self - def with_ms_teams(self, value: TeamsCardProperties) -> Self: - self.ms_teams = value + def with_msteams(self, value: TeamsCardProperties) -> Self: + self.msteams = value return self def with_metadata(self, value: CardMetadata) -> Self: self.metadata = value return self + def with_resources(self, value: Resources) -> Self: + self.resources = value + return self + def with_grid_area(self, value: str) -> Self: self.grid_area = value return self @@ -753,165 +767,145 @@ def with_actions(self, value: List[Action]) -> Self: self.actions = value return self +class InsertImageAction(Action): + """Inserts an image into the host application's canvas.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Action.InsertImage'] = 'Action.InsertImage' + """ Must be **Action.InsertImage**. """ + id: Optional[str] = None + """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) + """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ + title: Optional[str] = None + """ The title of the action, as it appears on buttons. """ + icon_url: Optional[str] = None + """ A URL (or Base64-encoded Data URI) to a PNG, GIF, JPEG or SVG image to be displayed on the left of the action's title. -class ImBackSubmitActionData(SerializableObject): - """Represents Teams-specific data in an Action.Submit to send an Instant Message back to the Bot.""" +`iconUrl` also accepts the `[,regular|filled]` format to display an icon from the vast [Adaptive Card icon catalog](topic:icon-catalog) instead of an image. """ + style: Optional[ActionStyle] = "default" + """ Control the style of the action, affecting its visual and spoken representations. """ + mode: Optional[ActionMode] = "primary" + """ Controls if the action is primary or secondary. Secondary actions appear in an overflow menu. """ + tooltip: Optional[str] = None + """ The tooltip text to display when the action is hovered over. """ + is_enabled: Optional[bool] = True + """ Controls the enabled state of the action. A disabled action cannot be clicked. If the action is represented as a button, the button's style will reflect this state. """ + menu_actions: Optional[List[SerializeAsAny[Action]]] = None + """ The actions to display in the overflow menu of a Split action button. """ + themed_icon_urls: Optional[List[ThemedUrl]] = None + """ A set of theme-specific icon URLs. """ + url: Optional[str] = None + """ The URL of the image to insert. """ + alt_text: Optional[str] = None + """ The alternate text for the image. """ + insert_position: Optional[ImageInsertPosition] = "Selection" + """ The position at which to insert the image. """ + fallback: Optional[Union[SerializeAsAny[Action], FallbackAction]] = None + """ An alternate action to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ - type: Literal["imBack"] = "imBack" - """ Must be **imBack**. """ + def with_key(self, value: str) -> Self: + self.key = value + return self - value: Optional[str] = None - """ The value that will be sent to the Bot. """ + def with_id(self, value: str) -> Self: + self.id = value + return self - def with_value(self, value: str) -> Self: - self.value = value + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: + self.requires = value return self + def with_title(self, value: str) -> Self: + self.title = value + return self -class InvokeSubmitActionData(SerializableObject): - """Represents Teams-specific data in an Action.Submit to make an Invoke request to the Bot.""" + def with_icon_url(self, value: str) -> Self: + self.icon_url = value + return self - type: Literal["invoke"] = "invoke" - """ Must be **invoke**. """ + def with_style(self, value: ActionStyle) -> Self: + self.style = value + return self - value: Optional[Dict[str, Any]] = None - """ The object to send to the Bot with the Invoke request. """ + def with_mode(self, value: ActionMode) -> Self: + self.mode = value + return self - def with_value(self, value: Dict[str, Any]) -> Self: - self.value = value + def with_tooltip(self, value: str) -> Self: + self.tooltip = value return self + def with_is_enabled(self, value: bool) -> Self: + self.is_enabled = value + return self -class MessageBackSubmitActionData(SerializableObject): - """Represents Teams-specific data in an Action.Submit to send a message back to the Bot.""" - - type: Literal["messageBack"] = "messageBack" - """ Must be **messageBack**. """ - - text: Optional[str] = None - """ The text that will be sent to the Bot. """ - - display_text: Optional[str] = None - """ The optional text that will be displayed as a new message in the conversation, as if the end-user sent it. `displayText` is not sent to the Bot. """ - - value: Optional[str] = None - """ Optional additional value that will be sent to the Bot. For instance, `value` can encode specific context for the action, such as unique identifiers or a JSON object. """ - - def with_text(self, value: str) -> Self: - self.text = value - return self - - def with_display_text(self, value: str) -> Self: - self.display_text = value + def with_menu_actions(self, value: List[Action]) -> Self: + self.menu_actions = value return self - def with_value(self, value: str) -> Self: - self.value = value + def with_themed_icon_urls(self, value: List[ThemedUrl]) -> Self: + self.themed_icon_urls = value return self - -class SigninSubmitActionData(SerializableObject): - """Represents Teams-specific data in an Action.Submit to sign in a user.""" - - type: Literal["signin"] = "signin" - """ Must be **signin**. """ - - value: Optional[str] = None - """ The URL to redirect the end-user for signing in. """ - - def with_value(self, value: str) -> Self: - self.value = value + def with_url(self, value: str) -> Self: + self.url = value return self - -class TaskFetchSubmitActionData(SerializableObject): - """Represents Teams-specific data in an Action.Submit to open a task module.""" - - type: Literal["task/fetch"] = "task/fetch" - """ Must be **task/fetch**. """ - - value: Optional[Dict[str, Any]] = None - """ The contextual data sent to the Bot to specify which task module to open. """ - - def with_value(self, value: Dict[str, Any]) -> Self: - self.value = value + def with_alt_text(self, value: str) -> Self: + self.alt_text = value return self - -class TeamsSubmitActionFeedback(SerializableObject): - """Represents feedback options for an [Action.Submit](https://adaptivecards.microsoft.com/?topic=Action.Submit).""" - - hide: Optional[bool] = None - """ Defines if a feedback message should be displayed after the action is executed. """ - - def with_hide(self, value: bool) -> Self: - self.hide = value + def with_insert_position(self, value: ImageInsertPosition) -> Self: + self.insert_position = value return self - -class TeamsSubmitActionProperties(SerializableObject): - """Teams-specific properties associated with the action.""" - - feedback: Optional[TeamsSubmitActionFeedback] = None - """ Defines how feedback is provided to the end-user when the action is executed. """ - - def with_feedback(self, value: TeamsSubmitActionFeedback) -> Self: - self.feedback = value + def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: + self.fallback = value return self - -class SubmitAction(Action): - """Gathers input values, merges them with the data property if specified, and sends them to the Bot via an Invoke activity. The Bot can only acknowledge is has received the request.""" - - type: Literal["Action.Submit"] = "Action.Submit" - """ Must be **Action.Submit**. """ - +class OpenUrlAction(Action): + """Opens the provided URL in either a separate browser tab or within the host application.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Action.OpenUrl'] = 'Action.OpenUrl' + """ Must be **Action.OpenUrl**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - title: Optional[str] = None """ The title of the action, as it appears on buttons. """ - icon_url: Optional[str] = None """ A URL (or Base64-encoded Data URI) to a PNG, GIF, JPEG or SVG image to be displayed on the left of the action's title. `iconUrl` also accepts the `[,regular|filled]` format to display an icon from the vast [Adaptive Card icon catalog](topic:icon-catalog) instead of an image. """ - - style: Optional[ActionStyle] = None + style: Optional[ActionStyle] = "default" """ Control the style of the action, affecting its visual and spoken representations. """ - - mode: Optional[ActionMode] = None + mode: Optional[ActionMode] = "primary" """ Controls if the action is primary or secondary. Secondary actions appear in an overflow menu. """ - tooltip: Optional[str] = None """ The tooltip text to display when the action is hovered over. """ - is_enabled: Optional[bool] = True """ Controls the enabled state of the action. A disabled action cannot be clicked. If the action is represented as a button, the button's style will reflect this state. """ - - data: Optional[Union[str, SubmitActionData]] = None - """ The data to send to the Bot when the action is executed. When expressed as an object, `data` is sent back to the Bot when the action is executed, adorned with the values of the inputs expressed as key/value pairs, where the key is the Id of the input. If `data` is expressed as a string, input values are not sent to the Bot. """ - - associated_inputs: Optional[AssociatedInputs] = None - """ The Ids of the inputs associated with the Action.Submit. When the action is executed, the values of the associated inputs are sent to the Bot. See [Input validation](topic:input-validation) for more details. """ - - conditionally_enabled: Optional[bool] = None - """ Controls if the action is enabled only if at least one required input has been filled by the user. """ - - ms_teams: Optional[TeamsSubmitActionProperties] = None - """ Teams-specific metadata associated with the action. """ - + menu_actions: Optional[List[SerializeAsAny[Action]]] = None + """ The actions to display in the overflow menu of a Split action button. """ + themed_icon_urls: Optional[List[ThemedUrl]] = None + """ A set of theme-specific icon URLs. """ + url: Optional[str] = None + """ The URL to open. """ fallback: Optional[Union[SerializeAsAny[Action], FallbackAction]] = None """ An alternate action to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -939,70 +933,70 @@ def with_is_enabled(self, value: bool) -> Self: self.is_enabled = value return self - def with_data(self, value: Union[str, SubmitActionData]) -> Self: - self.data = value - return self - - def with_associated_inputs(self, value: AssociatedInputs) -> Self: - self.associated_inputs = value + def with_menu_actions(self, value: List[Action]) -> Self: + self.menu_actions = value return self - def with_conditionally_enabled(self, value: bool) -> Self: - self.conditionally_enabled = value + def with_themed_icon_urls(self, value: List[ThemedUrl]) -> Self: + self.themed_icon_urls = value return self - def with_ms_teams(self, value: TeamsSubmitActionProperties) -> Self: - self.ms_teams = value + def with_url(self, value: str) -> Self: + self.url = value return self def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: self.fallback = value return self - -class OpenUrlAction(Action): - """Opens the provided URL in either a separate browser tab or within the host application.""" - - type: Literal["Action.OpenUrl"] = "Action.OpenUrl" - """ Must be **Action.OpenUrl**. """ - +class OpenUrlDialogAction(Action): + """Opens a task module in a modal dialog hosting the content at a provided URL.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Action.OpenUrlDialog'] = 'Action.OpenUrlDialog' + """ Must be **Action.OpenUrlDialog**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - title: Optional[str] = None """ The title of the action, as it appears on buttons. """ - icon_url: Optional[str] = None """ A URL (or Base64-encoded Data URI) to a PNG, GIF, JPEG or SVG image to be displayed on the left of the action's title. `iconUrl` also accepts the `[,regular|filled]` format to display an icon from the vast [Adaptive Card icon catalog](topic:icon-catalog) instead of an image. """ - - style: Optional[ActionStyle] = None + style: Optional[ActionStyle] = "default" """ Control the style of the action, affecting its visual and spoken representations. """ - - mode: Optional[ActionMode] = None + mode: Optional[ActionMode] = "primary" """ Controls if the action is primary or secondary. Secondary actions appear in an overflow menu. """ - tooltip: Optional[str] = None """ The tooltip text to display when the action is hovered over. """ - is_enabled: Optional[bool] = True """ Controls the enabled state of the action. A disabled action cannot be clicked. If the action is represented as a button, the button's style will reflect this state. """ - + menu_actions: Optional[List[SerializeAsAny[Action]]] = None + """ The actions to display in the overflow menu of a Split action button. """ + themed_icon_urls: Optional[List[ThemedUrl]] = None + """ A set of theme-specific icon URLs. """ + dialog_title: Optional[str] = None + """ The title of the dialog to be displayed in the dialog header. """ + dialog_height: Optional[str] = None + """ The height of the dialog. To define height as a number of pixels, use the px format. """ + dialog_width: Optional[str] = None + """ The width of the dialog. To define width as a number of pixels, use the px format. """ url: Optional[str] = None """ The URL to open. """ - fallback: Optional[Union[SerializeAsAny[Action], FallbackAction]] = None """ An alternate action to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -1030,76 +1024,76 @@ def with_is_enabled(self, value: bool) -> Self: self.is_enabled = value return self - def with_url(self, value: str) -> Self: - self.url = value + def with_menu_actions(self, value: List[Action]) -> Self: + self.menu_actions = value return self - def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: - self.fallback = value + def with_themed_icon_urls(self, value: List[ThemedUrl]) -> Self: + self.themed_icon_urls = value return self - -class TargetElement(SerializableObject): - """Defines a target element in an Action.ToggleVisibility.""" - - element_id: Optional[str] = None - """ The Id of the element to change the visibility of. """ - - is_visible: Optional[bool] = None - """ The new visibility state of the element. """ - - def with_element_id(self, value: str) -> Self: - self.element_id = value + def with_dialog_title(self, value: str) -> Self: + self.dialog_title = value return self - def with_is_visible(self, value: bool) -> Self: - self.is_visible = value + def with_dialog_height(self, value: str) -> Self: + self.dialog_height = value return self + def with_dialog_width(self, value: str) -> Self: + self.dialog_width = value + return self -class ToggleVisibilityAction(Action): - """Toggles the visibility of a set of elements. Action.ToggleVisibility is useful for creating "Show more" type UI patterns.""" + def with_url(self, value: str) -> Self: + self.url = value + return self - type: Literal["Action.ToggleVisibility"] = "Action.ToggleVisibility" - """ Must be **Action.ToggleVisibility**. """ + def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: + self.fallback = value + return self +class ResetInputsAction(Action): + """Resets the values of the inputs in the card.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Action.ResetInputs'] = 'Action.ResetInputs' + """ Must be **Action.ResetInputs**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - title: Optional[str] = None """ The title of the action, as it appears on buttons. """ - icon_url: Optional[str] = None """ A URL (or Base64-encoded Data URI) to a PNG, GIF, JPEG or SVG image to be displayed on the left of the action's title. `iconUrl` also accepts the `[,regular|filled]` format to display an icon from the vast [Adaptive Card icon catalog](topic:icon-catalog) instead of an image. """ - - style: Optional[ActionStyle] = None + style: Optional[ActionStyle] = "default" """ Control the style of the action, affecting its visual and spoken representations. """ - - mode: Optional[ActionMode] = None + mode: Optional[ActionMode] = "primary" """ Controls if the action is primary or secondary. Secondary actions appear in an overflow menu. """ - tooltip: Optional[str] = None """ The tooltip text to display when the action is hovered over. """ - is_enabled: Optional[bool] = True """ Controls the enabled state of the action. A disabled action cannot be clicked. If the action is represented as a button, the button's style will reflect this state. """ - - target_elements: Optional[Union[List[str], List[TargetElement]]] = None - """ The Ids of the elements to toggle the visibility of. """ - + menu_actions: Optional[List[SerializeAsAny[Action]]] = None + """ The actions to display in the overflow menu of a Split action button. """ + themed_icon_urls: Optional[List[ThemedUrl]] = None + """ A set of theme-specific icon URLs. """ + target_input_ids: Optional[List[str]] = None + """ The Ids of the inputs that should be reset. """ fallback: Optional[Union[SerializeAsAny[Action], FallbackAction]] = None """ An alternate action to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -1127,58 +1121,100 @@ def with_is_enabled(self, value: bool) -> Self: self.is_enabled = value return self - def with_target_elements(self, value: Union[List[str], List[TargetElement]]) -> Self: - self.target_elements = value + def with_menu_actions(self, value: List[Action]) -> Self: + self.menu_actions = value + return self + + def with_themed_icon_urls(self, value: List[ThemedUrl]) -> Self: + self.themed_icon_urls = value + return self + + def with_target_input_ids(self, value: List[str]) -> Self: + self.target_input_ids = value return self def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: self.fallback = value return self +class TeamsSubmitActionFeedback(SerializableObject): + """Represents feedback options for an [Action.Submit](https://adaptivecards.microsoft.com/?topic=Action.Submit).""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + hide: Optional[bool] = None + """ Defines if a feedback message should be displayed after the action is executed. """ -class ShowCardAction(Action): - """Expands or collapses an embedded card within the main card.""" + def with_key(self, value: str) -> Self: + self.key = value + return self - type: Literal["Action.ShowCard"] = "Action.ShowCard" - """ Must be **Action.ShowCard**. """ + def with_hide(self, value: bool) -> Self: + self.hide = value + return self + +class TeamsSubmitActionProperties(SerializableObject): + """Teams-specific properties associated with the action.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + feedback: Optional[TeamsSubmitActionFeedback] = None + """ Defines how feedback is provided to the end-user when the action is executed. """ + + def with_key(self, value: str) -> Self: + self.key = value + return self + + def with_feedback(self, value: TeamsSubmitActionFeedback) -> Self: + self.feedback = value + return self +class SubmitAction(Action): + """Gathers input values, merges them with the data property if specified, and sends them to the Bot via an Invoke activity. The Bot can only acknowledge is has received the request.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Action.Submit'] = 'Action.Submit' + """ Must be **Action.Submit**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - title: Optional[str] = None """ The title of the action, as it appears on buttons. """ - icon_url: Optional[str] = None """ A URL (or Base64-encoded Data URI) to a PNG, GIF, JPEG or SVG image to be displayed on the left of the action's title. `iconUrl` also accepts the `[,regular|filled]` format to display an icon from the vast [Adaptive Card icon catalog](topic:icon-catalog) instead of an image. """ - - style: Optional[ActionStyle] = None + style: Optional[ActionStyle] = "default" """ Control the style of the action, affecting its visual and spoken representations. """ - - mode: Optional[ActionMode] = None + mode: Optional[ActionMode] = "primary" """ Controls if the action is primary or secondary. Secondary actions appear in an overflow menu. """ - tooltip: Optional[str] = None """ The tooltip text to display when the action is hovered over. """ - is_enabled: Optional[bool] = True """ Controls the enabled state of the action. A disabled action cannot be clicked. If the action is represented as a button, the button's style will reflect this state. """ - + menu_actions: Optional[List[SerializeAsAny[Action]]] = None + """ The actions to display in the overflow menu of a Split action button. """ + themed_icon_urls: Optional[List[ThemedUrl]] = None + """ A set of theme-specific icon URLs. """ + data: Optional[Union[str, SubmitActionData, Dict[str, Any]]] = None + """ The data to send to the Bot when the action is executed. When expressed as an object, `data` is sent back to the Bot when the action is executed, adorned with the values of the inputs expressed as key/value pairs, where the key is the Id of the input. If `data` is expressed as a string, input values are not sent to the Bot. """ + associated_inputs: Optional[AssociatedInputs] = None + """ The Ids of the inputs associated with the Action.Submit. When the action is executed, the values of the associated inputs are sent to the Bot. See [Input validation](topic:input-validation) for more details. """ + conditionally_enabled: Optional[bool] = None + """ Controls if the action is enabled only if at least one required input has been filled by the user. """ + msteams: Optional[TeamsSubmitActionProperties] = None + """ Teams-specific metadata associated with the action. """ fallback: Optional[Union[SerializeAsAny[Action], FallbackAction]] = None """ An alternate action to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ - card: Optional[AdaptiveCard] = None - """ The card that should be displayed when the action is executed. """ + def with_key(self, value: str) -> Self: + self.key = value + return self def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -1206,58 +1242,91 @@ def with_is_enabled(self, value: bool) -> Self: self.is_enabled = value return self - def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: - self.fallback = value + def with_menu_actions(self, value: List[Action]) -> Self: + self.menu_actions = value return self - def with_card(self, value: AdaptiveCard) -> Self: - self.card = value + def with_themed_icon_urls(self, value: List[ThemedUrl]) -> Self: + self.themed_icon_urls = value return self + def with_data(self, value: Union[str, SubmitActionData, Dict[str, Any]]) -> Self: + self.data = value + return self -class ResetInputsAction(Action): - """Resets the values of the inputs in the card.""" + def with_associated_inputs(self, value: AssociatedInputs) -> Self: + self.associated_inputs = value + return self - type: Literal["Action.ResetInputs"] = "Action.ResetInputs" - """ Must be **Action.ResetInputs**. """ + def with_conditionally_enabled(self, value: bool) -> Self: + self.conditionally_enabled = value + return self - id: Optional[str] = None - """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ + def with_msteams(self, value: TeamsSubmitActionProperties) -> Self: + self.msteams = value + return self - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) - """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ + def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: + self.fallback = value + return self - title: Optional[str] = None - """ The title of the action, as it appears on buttons. """ +class TargetElement(SerializableObject): + """Defines a target element in an Action.ToggleVisibility.""" + element_id: Optional[str] = None + """ The Id of the element to change the visibility of. """ + is_visible: Optional[bool] = None + """ The new visibility state of the element. """ - icon_url: Optional[str] = None - """ A URL (or Base64-encoded Data URI) to a PNG, GIF, JPEG or SVG image to be displayed on the left of the action's title. + def with_element_id(self, value: str) -> Self: + self.element_id = value + return self -`iconUrl` also accepts the `[,regular|filled]` format to display an icon from the vast [Adaptive Card icon catalog](topic:icon-catalog) instead of an image. """ + def with_is_visible(self, value: bool) -> Self: + self.is_visible = value + return self - style: Optional[ActionStyle] = None - """ Control the style of the action, affecting its visual and spoken representations. """ +class ToggleVisibilityAction(Action): + """Toggles the visibility of a set of elements. Action.ToggleVisibility is useful for creating "Show more" type UI patterns.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Action.ToggleVisibility'] = 'Action.ToggleVisibility' + """ Must be **Action.ToggleVisibility**. """ + id: Optional[str] = None + """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) + """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ + title: Optional[str] = None + """ The title of the action, as it appears on buttons. """ + icon_url: Optional[str] = None + """ A URL (or Base64-encoded Data URI) to a PNG, GIF, JPEG or SVG image to be displayed on the left of the action's title. - mode: Optional[ActionMode] = None +`iconUrl` also accepts the `[,regular|filled]` format to display an icon from the vast [Adaptive Card icon catalog](topic:icon-catalog) instead of an image. """ + style: Optional[ActionStyle] = "default" + """ Control the style of the action, affecting its visual and spoken representations. """ + mode: Optional[ActionMode] = "primary" """ Controls if the action is primary or secondary. Secondary actions appear in an overflow menu. """ - tooltip: Optional[str] = None """ The tooltip text to display when the action is hovered over. """ - is_enabled: Optional[bool] = True """ Controls the enabled state of the action. A disabled action cannot be clicked. If the action is represented as a button, the button's style will reflect this state. """ - - target_input_ids: Optional[List[str]] = None - """ The Ids of the inputs that should be reset. """ - + menu_actions: Optional[List[SerializeAsAny[Action]]] = None + """ The actions to display in the overflow menu of a Split action button. """ + themed_icon_urls: Optional[List[ThemedUrl]] = None + """ A set of theme-specific icon URLs. """ + target_elements: Optional[Union[List[str], List[TargetElement]]] = None + """ The Ids of the elements to toggle the visibility of. """ fallback: Optional[Union[SerializeAsAny[Action], FallbackAction]] = None """ An alternate action to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -1285,64 +1354,64 @@ def with_is_enabled(self, value: bool) -> Self: self.is_enabled = value return self - def with_target_input_ids(self, value: List[str]) -> Self: - self.target_input_ids = value + def with_menu_actions(self, value: List[Action]) -> Self: + self.menu_actions = value return self - def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: - self.fallback = value + def with_themed_icon_urls(self, value: List[ThemedUrl]) -> Self: + self.themed_icon_urls = value return self + def with_target_elements(self, value: Union[List[str], List[TargetElement]]) -> Self: + self.target_elements = value + return self -class InsertImageAction(Action): - """Inserts an image into the host application's canvas.""" - - type: Literal["Action.InsertImage"] = "Action.InsertImage" - """ Must be **Action.InsertImage**. """ + def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: + self.fallback = value + return self +class ShowCardAction(Action): + """Expands or collapses an embedded card within the main card.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Action.ShowCard'] = 'Action.ShowCard' + """ Must be **Action.ShowCard**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - title: Optional[str] = None """ The title of the action, as it appears on buttons. """ - icon_url: Optional[str] = None """ A URL (or Base64-encoded Data URI) to a PNG, GIF, JPEG or SVG image to be displayed on the left of the action's title. `iconUrl` also accepts the `[,regular|filled]` format to display an icon from the vast [Adaptive Card icon catalog](topic:icon-catalog) instead of an image. """ - - style: Optional[ActionStyle] = None + style: Optional[ActionStyle] = "default" """ Control the style of the action, affecting its visual and spoken representations. """ - - mode: Optional[ActionMode] = None + mode: Optional[ActionMode] = "primary" """ Controls if the action is primary or secondary. Secondary actions appear in an overflow menu. """ - tooltip: Optional[str] = None """ The tooltip text to display when the action is hovered over. """ - is_enabled: Optional[bool] = True """ Controls the enabled state of the action. A disabled action cannot be clicked. If the action is represented as a button, the button's style will reflect this state. """ - - url: Optional[str] = None - """ The URL of the image to insert. """ - - alt_text: Optional[str] = None - """ The alternate text for the image. """ - - insert_position: Optional[ImageInsertPosition] = None - """ The position at which to insert the image. """ - + menu_actions: Optional[List[SerializeAsAny[Action]]] = None + """ The actions to display in the overflow menu of a Split action button. """ + themed_icon_urls: Optional[List[ThemedUrl]] = None + """ A set of theme-specific icon URLs. """ fallback: Optional[Union[SerializeAsAny[Action], FallbackAction]] = None """ An alternate action to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + card: Optional[AdaptiveCard] = None + """ The card that should be displayed when the action is executed. """ + + def with_key(self, value: str) -> Self: + self.key = value + return self def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -1370,271 +1439,272 @@ def with_is_enabled(self, value: bool) -> Self: self.is_enabled = value return self - def with_url(self, value: str) -> Self: - self.url = value + def with_menu_actions(self, value: List[Action]) -> Self: + self.menu_actions = value return self - def with_alt_text(self, value: str) -> Self: - self.alt_text = value - return self - - def with_insert_position(self, value: ImageInsertPosition) -> Self: - self.insert_position = value + def with_themed_icon_urls(self, value: List[ThemedUrl]) -> Self: + self.themed_icon_urls = value return self def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: self.fallback = value return self - -class StackLayout(ContainerLayout): - """A layout that stacks elements on top of each other. Layout.Stack is the default layout used by AdaptiveCard and all containers.""" - - type: Literal["Layout.Stack"] = "Layout.Stack" - """ Must be **Layout.Stack**. """ - - target_width: Optional[TargetWidth] = None - """ Controls for which card width the layout should be used. """ - - def with_target_width(self, value: TargetWidth) -> Self: - self.target_width = value + def with_card(self, value: AdaptiveCard) -> Self: + self.card = value return self +class PopoverAction(Action): + """Shows a popover to display more information to the user.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Action.Popover'] = 'Action.Popover' + """ Must be **Action.Popover**. """ + id: Optional[str] = None + """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) + """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ + title: Optional[str] = None + """ The title of the action, as it appears on buttons. """ + icon_url: Optional[str] = None + """ A URL (or Base64-encoded Data URI) to a PNG, GIF, JPEG or SVG image to be displayed on the left of the action's title. -class FlowLayout(ContainerLayout): - """A layout that spreads elements horizontally and wraps them across multiple rows, as needed.""" - - type: Literal["Layout.Flow"] = "Layout.Flow" - """ Must be **Layout.Flow**. """ - - target_width: Optional[TargetWidth] = None - """ Controls for which card width the layout should be used. """ - - horizontal_items_alignment: Optional[HorizontalAlignment] = None - """ Controls how the content of the container should be horizontally aligned. """ - - vertical_items_alignment: Optional[VerticalAlignment] = None - """ Controls how the content of the container should be vertically aligned. """ - - item_fit: Optional[FlowLayoutItemFit] = None - """ Controls how item should fit inside the container. """ - - min_item_width: Optional[str] = None - """ The minimum width, in pixels, of each item, in the `px` format. Should not be used if itemWidth is set. """ - - max_item_width: Optional[str] = None - """ The maximum width, in pixels, of each item, in the `px` format. Should not be used if itemWidth is set. """ - - item_width: Optional[str] = None - """ The width, in pixels, of each item, in the `px` format. Should not be used if maxItemWidth and/or minItemWidth are set. """ - - column_spacing: Optional[Spacing] = None - """ The space between items. """ - - row_spacing: Optional[Spacing] = None - """ The space between rows of items. """ +`iconUrl` also accepts the `[,regular|filled]` format to display an icon from the vast [Adaptive Card icon catalog](topic:icon-catalog) instead of an image. """ + style: Optional[ActionStyle] = "default" + """ Control the style of the action, affecting its visual and spoken representations. """ + mode: Optional[ActionMode] = "primary" + """ Controls if the action is primary or secondary. Secondary actions appear in an overflow menu. """ + tooltip: Optional[str] = None + """ The tooltip text to display when the action is hovered over. """ + is_enabled: Optional[bool] = True + """ Controls the enabled state of the action. A disabled action cannot be clicked. If the action is represented as a button, the button's style will reflect this state. """ + themed_icon_urls: Optional[List[ThemedUrl]] = None + """ A set of theme-specific icon URLs. """ + content: Optional[SerializeAsAny[CardElement]] = None + """ The content of the popover, which can be any element. """ + display_arrow: Optional[bool] = True + """ Controls if an arrow should be displayed towards the element that triggered the popover. """ + position: Optional[PopoverPosition] = "Above" + """ Controls where the popover should be displayed with regards to the element that triggered it. """ + max_popover_width: Optional[str] = None + """ The maximum width of the popover in pixels, in the `px` format """ + fallback: Optional[Union[SerializeAsAny[Action], FallbackAction]] = None + """ An alternate action to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ - def with_target_width(self, value: TargetWidth) -> Self: - self.target_width = value + def with_key(self, value: str) -> Self: + self.key = value return self - def with_horizontal_items_alignment(self, value: HorizontalAlignment) -> Self: - self.horizontal_items_alignment = value + def with_id(self, value: str) -> Self: + self.id = value return self - def with_vertical_items_alignment(self, value: VerticalAlignment) -> Self: - self.vertical_items_alignment = value + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: + self.requires = value return self - def with_item_fit(self, value: FlowLayoutItemFit) -> Self: - self.item_fit = value + def with_title(self, value: str) -> Self: + self.title = value return self - def with_min_item_width(self, value: str) -> Self: - self.min_item_width = value + def with_icon_url(self, value: str) -> Self: + self.icon_url = value return self - def with_max_item_width(self, value: str) -> Self: - self.max_item_width = value + def with_style(self, value: ActionStyle) -> Self: + self.style = value return self - def with_item_width(self, value: str) -> Self: - self.item_width = value + def with_mode(self, value: ActionMode) -> Self: + self.mode = value return self - def with_column_spacing(self, value: Spacing) -> Self: - self.column_spacing = value + def with_tooltip(self, value: str) -> Self: + self.tooltip = value return self - def with_row_spacing(self, value: Spacing) -> Self: - self.row_spacing = value + def with_is_enabled(self, value: bool) -> Self: + self.is_enabled = value return self + def with_themed_icon_urls(self, value: List[ThemedUrl]) -> Self: + self.themed_icon_urls = value + return self -class GridArea(SerializableObject): - """Defines an area in a Layout.AreaGrid layout.""" + def with_content(self, value: CardElement) -> Self: + self.content = value + return self - name: Optional[str] = None - """ The name of the area. To place an element in this area, set its `grid.area` property to match the name of the area. """ + def with_display_arrow(self, value: bool) -> Self: + self.display_arrow = value + return self - column: Optional[float] = 1 - """ The start column index of the area. Column indices start at 1. """ + def with_position(self, value: PopoverPosition) -> Self: + self.position = value + return self - column_span: Optional[float] = 1 - """ Defines how many columns the area should span. """ + def with_max_popover_width(self, value: str) -> Self: + self.max_popover_width = value + return self - row: Optional[float] = 1 - """ The start row index of the area. Row indices start at 1. """ + def with_fallback(self, value: Union[Action, FallbackAction]) -> Self: + self.fallback = value + return self - row_span: Optional[float] = 1 - """ Defines how many rows the area should span. """ +class ActionSet(CardElement): + """Displays a set of action, which can be placed anywhere in the card.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['ActionSet'] = 'ActionSet' + """ Must be **ActionSet**. """ + id: Optional[str] = None + """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) + """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ + lang: Optional[str] = None + """ The locale associated with the element. """ + is_visible: Optional[bool] = True + """ Controls the visibility of the element. """ + separator: Optional[bool] = None + """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ + height: Optional[ElementHeight] = "auto" + """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ + horizontal_alignment: Optional[HorizontalAlignment] = None + """ Controls how the element should be horizontally aligned. """ + spacing: Optional[Spacing] = "Default" + """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ + target_width: Optional[TargetWidth] = None + """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ + is_sort_key: Optional[bool] = None + """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ + grid_area: Optional[str] = None + """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ + fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None + """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + actions: Optional[List[SerializeAsAny[Action]]] = None + """ The actions in the set. """ - def with_name(self, value: str) -> Self: - self.name = value + def with_key(self, value: str) -> Self: + self.key = value return self - def with_column(self, value: float) -> Self: - self.column = value + def with_id(self, value: str) -> Self: + self.id = value return self - def with_column_span(self, value: float) -> Self: - self.column_span = value + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: + self.requires = value return self - def with_row(self, value: float) -> Self: - self.row = value + def with_lang(self, value: str) -> Self: + self.lang = value return self - def with_row_span(self, value: float) -> Self: - self.row_span = value + def with_is_visible(self, value: bool) -> Self: + self.is_visible = value return self + def with_separator(self, value: bool) -> Self: + self.separator = value + return self -class AreaGridLayout(ContainerLayout): - """A layout that divides a container into named areas into which elements can be placed.""" - - type: Literal["Layout.AreaGrid"] = "Layout.AreaGrid" - """ Must be **Layout.AreaGrid**. """ - - target_width: Optional[TargetWidth] = None - """ Controls for which card width the layout should be used. """ - - columns: Optional[Union[List[float], List[str]]] = None - """ The columns in the grid layout, defined as a percentage of the available width or in pixels using the `px` format. """ - - areas: Optional[List[GridArea]] = None - """ The areas in the grid layout. """ + def with_height(self, value: ElementHeight) -> Self: + self.height = value + return self - column_spacing: Optional[Spacing] = None - """ The space between columns. """ + def with_horizontal_alignment(self, value: HorizontalAlignment) -> Self: + self.horizontal_alignment = value + return self - row_spacing: Optional[Spacing] = None - """ The space between rows. """ + def with_spacing(self, value: Spacing) -> Self: + self.spacing = value + return self def with_target_width(self, value: TargetWidth) -> Self: self.target_width = value return self - def with_columns(self, value: Union[List[float], List[str]]) -> Self: - self.columns = value + def with_is_sort_key(self, value: bool) -> Self: + self.is_sort_key = value return self - def with_areas(self, value: List[GridArea]) -> Self: - self.areas = value + def with_grid_area(self, value: str) -> Self: + self.grid_area = value return self - def with_column_spacing(self, value: Spacing) -> Self: - self.column_spacing = value + def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: + self.fallback = value return self - def with_row_spacing(self, value: Spacing) -> Self: - self.row_spacing = value + def with_actions(self, value: List[Action]) -> Self: + self.actions = value return self - class Container(CardElement): """A container for other elements. Use containers for styling purposes and/or to logically group a set of elements together, which can be especially useful when used with Action.ToggleVisibility.""" - - type: Literal["Container"] = "Container" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Container'] = 'Container' """ Must be **Container**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - - select_action: Optional[Action] = None + select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the element is tapped or clicked. Action.ShowCard is not supported. """ - style: Optional[ContainerStyle] = None """ The style of the container. Container styles control the colors of the background, border and text inside the container, in such a way that contrast requirements are always met. """ - show_border: Optional[bool] = None """ Controls if a border should be displayed around the container. """ - rounded_corners: Optional[bool] = None """ Controls if the container should have rounded corners. """ - layouts: Optional[List[SerializeAsAny[ContainerLayout]]] = None """ The layouts associated with the container. The container can dynamically switch from one layout to another as the card's width changes. See [Container layouts](topic:container-layouts) for more details. """ - bleed: Optional[bool] = None """ Controls if the container should bleed into its parent. A bleeding container extends into its parent's padding. """ - min_height: Optional[str] = None """ The minimum height, in pixels, of the container, in the `px` format. """ - background_image: Optional[Union[str, BackgroundImage]] = None """ Defines the container's background image. """ - vertical_content_alignment: Optional[VerticalAlignment] = None """ Controls how the container's content should be vertically aligned. """ - rtl: Optional[bool] = None """ Controls if the content of the card is to be rendered left-to-right or right-to-left. """ - max_height: Optional[str] = None """ The maximum height, in pixels, of the container, in the `px` format. When the content of a container exceeds the container's maximum height, a vertical scrollbar is displayed. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ - items: Optional[List[SerializeAsAny[CardElement]]] = None """ The elements in the container. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -1726,191 +1796,234 @@ def with_items(self, value: List[CardElement]) -> Self: self.items = value return self +class StackLayout(ContainerLayout): + """A layout that stacks elements on top of each other. Layout.Stack is the default layout used by AdaptiveCard and all containers.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Layout.Stack'] = 'Layout.Stack' + """ Must be **Layout.Stack**. """ + target_width: Optional[TargetWidth] = None + """ Controls for which card width the layout should be used. """ -class ActionSet(CardElement): - """Displays a set of action, which can be placed anywhere in the card.""" - - type: Literal["ActionSet"] = "ActionSet" - """ Must be **ActionSet**. """ + def with_key(self, value: str) -> Self: + self.key = value + return self - id: Optional[str] = None - """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ + def with_target_width(self, value: TargetWidth) -> Self: + self.target_width = value + return self - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) - """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ +class FlowLayout(ContainerLayout): + """A layout that spreads elements horizontally and wraps them across multiple rows, as needed.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Layout.Flow'] = 'Layout.Flow' + """ Must be **Layout.Flow**. """ + target_width: Optional[TargetWidth] = None + """ Controls for which card width the layout should be used. """ + horizontal_items_alignment: Optional[HorizontalAlignment] = "Center" + """ Controls how the content of the container should be horizontally aligned. """ + vertical_items_alignment: Optional[VerticalAlignment] = "Top" + """ Controls how the content of the container should be vertically aligned. """ + item_fit: Optional[FlowLayoutItemFit] = "Fit" + """ Controls how item should fit inside the container. """ + min_item_width: Optional[str] = None + """ The minimum width, in pixels, of each item, in the `px` format. Should not be used if itemWidth is set. """ + max_item_width: Optional[str] = None + """ The maximum width, in pixels, of each item, in the `px` format. Should not be used if itemWidth is set. """ + item_width: Optional[str] = None + """ The width, in pixels, of each item, in the `px` format. Should not be used if maxItemWidth and/or minItemWidth are set. """ + column_spacing: Optional[Spacing] = "Default" + """ The space between items. """ + row_spacing: Optional[Spacing] = "Default" + """ The space between rows of items. """ - lang: Optional[str] = None - """ The locale associated with the element. """ + def with_key(self, value: str) -> Self: + self.key = value + return self - is_visible: Optional[bool] = True - """ Controls the visibility of the element. """ + def with_target_width(self, value: TargetWidth) -> Self: + self.target_width = value + return self - separator: Optional[bool] = None - """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ + def with_horizontal_items_alignment(self, value: HorizontalAlignment) -> Self: + self.horizontal_items_alignment = value + return self - height: Optional[ElementHeight] = None - """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ + def with_vertical_items_alignment(self, value: VerticalAlignment) -> Self: + self.vertical_items_alignment = value + return self - horizontal_alignment: Optional[HorizontalAlignment] = None - """ Controls how the element should be horizontally aligned. """ + def with_item_fit(self, value: FlowLayoutItemFit) -> Self: + self.item_fit = value + return self - spacing: Optional[Spacing] = None - """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ + def with_min_item_width(self, value: str) -> Self: + self.min_item_width = value + return self - target_width: Optional[TargetWidth] = None - """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ + def with_max_item_width(self, value: str) -> Self: + self.max_item_width = value + return self - is_sort_key: Optional[bool] = None - """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ + def with_item_width(self, value: str) -> Self: + self.item_width = value + return self - grid_area: Optional[str] = None - """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ + def with_column_spacing(self, value: Spacing) -> Self: + self.column_spacing = value + return self - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None - """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_row_spacing(self, value: Spacing) -> Self: + self.row_spacing = value + return self - actions: Optional[List[SerializeAsAny[Action]]] = None - """ The actions in the set. """ +class GridArea(SerializableObject): + """Defines an area in a Layout.AreaGrid layout.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + name: Optional[str] = None + """ The name of the area. To place an element in this area, set its `grid.area` property to match the name of the area. """ + column: Optional[float] = 1 + """ The start column index of the area. Column indices start at 1. """ + column_span: Optional[float] = 1 + """ Defines how many columns the area should span. """ + row: Optional[float] = 1 + """ The start row index of the area. Row indices start at 1. """ + row_span: Optional[float] = 1 + """ Defines how many rows the area should span. """ - def with_id(self, value: str) -> Self: - self.id = value + def with_key(self, value: str) -> Self: + self.key = value return self - def with_requires(self, value: HostCapabilities) -> Self: - self.requires = value + def with_name(self, value: str) -> Self: + self.name = value return self - def with_lang(self, value: str) -> Self: - self.lang = value + def with_column(self, value: float) -> Self: + self.column = value return self - def with_is_visible(self, value: bool) -> Self: - self.is_visible = value + def with_column_span(self, value: float) -> Self: + self.column_span = value return self - def with_separator(self, value: bool) -> Self: - self.separator = value + def with_row(self, value: float) -> Self: + self.row = value return self - def with_height(self, value: ElementHeight) -> Self: - self.height = value + def with_row_span(self, value: float) -> Self: + self.row_span = value return self - def with_horizontal_alignment(self, value: HorizontalAlignment) -> Self: - self.horizontal_alignment = value - return self +class AreaGridLayout(ContainerLayout): + """A layout that divides a container into named areas into which elements can be placed.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Layout.AreaGrid'] = 'Layout.AreaGrid' + """ Must be **Layout.AreaGrid**. """ + target_width: Optional[TargetWidth] = None + """ Controls for which card width the layout should be used. """ + columns: Optional[Union[List[float], List[str]]] = None + """ The columns in the grid layout, defined as a percentage of the available width or in pixels using the `px` format. """ + areas: Optional[List[GridArea]] = None + """ The areas in the grid layout. """ + column_spacing: Optional[Spacing] = "Default" + """ The space between columns. """ + row_spacing: Optional[Spacing] = "Default" + """ The space between rows. """ - def with_spacing(self, value: Spacing) -> Self: - self.spacing = value + def with_key(self, value: str) -> Self: + self.key = value return self def with_target_width(self, value: TargetWidth) -> Self: self.target_width = value return self - def with_is_sort_key(self, value: bool) -> Self: - self.is_sort_key = value + def with_columns(self, value: Union[List[float], List[str]]) -> Self: + self.columns = value return self - def with_grid_area(self, value: str) -> Self: - self.grid_area = value + def with_areas(self, value: List[GridArea]) -> Self: + self.areas = value return self - def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: - self.fallback = value + def with_column_spacing(self, value: Spacing) -> Self: + self.column_spacing = value return self - def with_actions(self, value: List[Action]) -> Self: - self.actions = value + def with_row_spacing(self, value: Spacing) -> Self: + self.row_spacing = value return self - class Column(CardElement): """A column in a ColumnSet element.""" - - type: Literal["Column"] = "Column" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Column'] = 'Column' """ Optional. If specified, must be **Column**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - - select_action: Optional[Action] = None + select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the element is tapped or clicked. Action.ShowCard is not supported. """ - style: Optional[ContainerStyle] = None """ The style of the container. Container styles control the colors of the background, border and text inside the container, in such a way that contrast requirements are always met. """ - show_border: Optional[bool] = None """ Controls if a border should be displayed around the container. """ - rounded_corners: Optional[bool] = None """ Controls if the container should have rounded corners. """ - layouts: Optional[List[SerializeAsAny[ContainerLayout]]] = None """ The layouts associated with the container. The container can dynamically switch from one layout to another as the card's width changes. See [Container layouts](topic:container-layouts) for more details. """ - bleed: Optional[bool] = None """ Controls if the container should bleed into its parent. A bleeding container extends into its parent's padding. """ - min_height: Optional[str] = None """ The minimum height, in pixels, of the container, in the `px` format. """ - background_image: Optional[Union[str, BackgroundImage]] = None """ Defines the container's background image. """ - vertical_content_alignment: Optional[VerticalAlignment] = None """ Controls how the container's content should be vertically aligned. """ - rtl: Optional[bool] = None """ Controls if the content of the card is to be rendered left-to-right or right-to-left. """ - max_height: Optional[str] = None """ The maximum height, in pixels, of the container, in the `px` format. When the content of a container exceeds the container's maximum height, a vertical scrollbar is displayed. """ - - width: Optional[Union[str, float]] = "stretch" + width: Optional[Union[str, float]] = None """ The width of the column. If expressed as a number, represents the relative weight of the column in the set. If expressed as a string, `auto` will automatically adjust the column's width according to its content, `stretch` will make the column use the remaining horizontal space (shared with other columns with width set to `stretch`) and using the `px` format will give the column an explicit width in pixels. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ - items: Optional[List[SerializeAsAny[CardElement]]] = None """ The elements in the column. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -2006,75 +2119,62 @@ def with_items(self, value: List[CardElement]) -> Self: self.items = value return self - class ColumnSet(CardElement): """Splits the available horizontal space into separate columns, so elements can be organized in a row.""" - - type: Literal["ColumnSet"] = "ColumnSet" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['ColumnSet'] = 'ColumnSet' """ Must be **ColumnSet**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - - select_action: Optional[Action] = None + select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the element is tapped or clicked. Action.ShowCard is not supported. """ - style: Optional[ContainerStyle] = None """ The style of the container. Container styles control the colors of the background, border and text inside the container, in such a way that contrast requirements are always met. """ - show_border: Optional[bool] = None """ Controls if a border should be displayed around the container. """ - rounded_corners: Optional[bool] = None """ Controls if the container should have rounded corners. """ - bleed: Optional[bool] = None """ Controls if the container should bleed into its parent. A bleeding container extends into its parent's padding. """ - min_height: Optional[str] = None """ The minimum height, in pixels, of the container, in the `px` format. """ - + min_width: Optional[str] = None + """ The minimum width of the column set. `auto` will automatically adjust the column set's minimum width according to its content and using the `px` format will give the column set an explicit minimum width in pixels. A scrollbar will be displayed if the available width is less than the specified minimum width. """ grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ - columns: Optional[List[Column]] = None """ The columns in the set. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -2134,6 +2234,10 @@ def with_min_height(self, value: str) -> Self: self.min_height = value return self + def with_min_width(self, value: str) -> Self: + self.min_width = value + return self + def with_grid_area(self, value: str) -> Self: self.grid_area = value return self @@ -2146,16 +2250,19 @@ def with_columns(self, value: List[Column]) -> Self: self.columns = value return self - class MediaSource(SerializableObject): """Defines the source URL of a media stream. YouTube, Dailymotion, Vimeo and Microsoft Stream URLs are supported.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ mime_type: Optional[str] = None """ The MIME type of the source. """ - url: Optional[str] = None """ The URL of the source. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_mime_type(self, value: str) -> Self: self.mime_type = value return self @@ -2164,19 +2271,21 @@ def with_url(self, value: str) -> Self: self.url = value return self - class CaptionSource(SerializableObject): """Defines a source URL for a video captions.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ mime_type: Optional[str] = None """ The MIME type of the source. """ - url: Optional[str] = None """ The URL of the source. """ - label: Optional[str] = None """ The label of this caption source. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_mime_type(self, value: str) -> Self: self.mime_type = value return self @@ -2189,63 +2298,52 @@ def with_label(self, value: str) -> Self: self.label = value return self - class Media(CardElement): """A media element, that makes it possible to embed videos inside a card.""" - - type: Literal["Media"] = "Media" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Media'] = 'Media' """ Must be **Media**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - sources: Optional[List[MediaSource]] = None """ The sources for the media. For YouTube, Dailymotion and Vimeo, only one source can be specified. """ - caption_sources: Optional[List[CaptionSource]] = None """ The caption sources for the media. Caption sources are not used for YouTube, Dailymotion or Vimeo sources. """ - poster: Optional[str] = None """ The URL of the poster image to display. """ - alt_text: Optional[str] = None """ The alternate text for the media, used for accessibility purposes. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -2301,57 +2399,50 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class RichTextBlock(CardElement): """A rich text block that displays formatted text.""" - - type: Literal["RichTextBlock"] = "RichTextBlock" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['RichTextBlock'] = 'RichTextBlock' """ Must be **RichTextBlock**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - + label_for: Optional[str] = None + """ The Id of the input the RichTextBlock should act as the label of. """ grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ - inlines: Optional[Union[List[SerializeAsAny[CardElement]], List[str]]] = None """ The inlines making up the rich text block. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -2387,6 +2478,10 @@ def with_is_sort_key(self, value: bool) -> Self: self.is_sort_key = value return self + def with_label_for(self, value: str) -> Self: + self.label_for = value + return self + def with_grid_area(self, value: str) -> Self: self.grid_area = value return self @@ -2399,18 +2494,20 @@ def with_inlines(self, value: Union[List[CardElement], List[str]]) -> Self: self.inlines = value return self - class ColumnDefinition(SerializableObject): """Defines a column in a Table element.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ horizontal_cell_content_alignment: Optional[HorizontalAlignment] = None """ Controls how the content of every cell in the table should be horizontally aligned by default. This property overrides the horizontalCellContentAlignment property of the table. """ - vertical_cell_content_alignment: Optional[VerticalAlignment] = None """ Controls how the content of every cell in the column should be vertically aligned by default. This property overrides the verticalCellContentAlignment property of the table. """ - width: Optional[Union[str, float]] = None - """ The width of the column in the table, expressed as either a percentage of the available width or in pixels, using the `px` format. """ + """ The width of the column in the table. If expressed as a number, represents the relative weight of the column in the table. If expressed as a string, `auto` will automatically adjust the column's width according to its content and using the `px` format will give the column an explicit width in pixels. """ + + def with_key(self, value: str) -> Self: + self.key = value + return self def with_horizontal_cell_content_alignment(self, value: HorizontalAlignment) -> Self: self.horizontal_cell_content_alignment = value @@ -2424,81 +2521,64 @@ def with_width(self, value: Union[str, float]) -> Self: self.width = value return self - class TableCell(CardElement): """Represents a cell in a table row.""" - - type: Literal["TableCell"] = "TableCell" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['TableCell'] = 'TableCell' """ Must be **TableCell**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - - select_action: Optional[Action] = None + select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the element is tapped or clicked. Action.ShowCard is not supported. """ - style: Optional[ContainerStyle] = None """ The style of the container. Container styles control the colors of the background, border and text inside the container, in such a way that contrast requirements are always met. """ - layouts: Optional[List[SerializeAsAny[ContainerLayout]]] = None """ The layouts associated with the container. The container can dynamically switch from one layout to another as the card's width changes. See [Container layouts](topic:container-layouts) for more details. """ - bleed: Optional[bool] = None """ Controls if the container should bleed into its parent. A bleeding container extends into its parent's padding. """ - min_height: Optional[str] = None """ The minimum height, in pixels, of the container, in the `px` format. """ - background_image: Optional[Union[str, BackgroundImage]] = None """ Defines the container's background image. """ - vertical_content_alignment: Optional[VerticalAlignment] = None """ Controls how the container's content should be vertically aligned. """ - rtl: Optional[bool] = None """ Controls if the content of the card is to be rendered left-to-right or right-to-left. """ - max_height: Optional[str] = None """ The maximum height, in pixels, of the container, in the `px` format. When the content of a container exceeds the container's maximum height, a vertical scrollbar is displayed. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ - items: Optional[List[SerializeAsAny[CardElement]]] = None """ The items (elements) in the cell. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -2578,72 +2658,58 @@ def with_items(self, value: List[CardElement]) -> Self: self.items = value return self - class TableRow(CardElement): """Represents a row of cells in a table.""" - - type: Literal["TableRow"] = "TableRow" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['TableRow'] = 'TableRow' """ Must be **TableRow**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - show_border: Optional[bool] = None """ Controls if a border should be displayed around the container. """ - rounded_corners: Optional[bool] = None """ Controls if the container should have rounded corners. """ - style: Optional[ContainerStyle] = None """ The style of the container. Container styles control the colors of the background, border and text inside the container, in such a way that contrast requirements are always met. """ - horizontal_cell_content_alignment: Optional[HorizontalAlignment] = None """ Controls how the content of every cell in the row should be horizontally aligned by default. This property overrides the horizontalCellContentAlignment property of the table and columns. """ - vertical_cell_content_alignment: Optional[VerticalAlignment] = None """ Controls how the content of every cell in the row should be vertically aligned by default. This property overrides the verticalCellContentAlignment property of the table and columns. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ - cells: Optional[List[TableCell]] = None """ The cells in the row. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -2711,84 +2777,68 @@ def with_cells(self, value: List[TableCell]) -> Self: self.cells = value return self - class Table(CardElement): """Use tables to display data in a tabular way, with rows, columns and cells.""" - - type: Literal["Table"] = "Table" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Table'] = 'Table' """ Must be **Table**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - style: Optional[ContainerStyle] = None """ The style of the container. Container styles control the colors of the background, border and text inside the container, in such a way that contrast requirements are always met. """ - show_border: Optional[bool] = None """ Controls if a border should be displayed around the container. """ - rounded_corners: Optional[bool] = None """ Controls if the container should have rounded corners. """ - columns: Optional[List[ColumnDefinition]] = None """ The columns in the table. """ - + min_width: Optional[str] = None + """ The minimum width of the table in pixels. `auto` will automatically adjust the table's minimum width according to its content and using the `px` format will give the table an explicit minimum width in pixels. A scrollbar will be displayed if the available width is less than the specified minimum width. """ first_row_as_headers: Optional[bool] = True """ Controls whether the first row of the table should be treated as a header. """ - show_grid_lines: Optional[bool] = True """ Controls if grid lines should be displayed. """ - grid_style: Optional[ContainerStyle] = None """ The style of the grid lines between cells. """ - horizontal_cell_content_alignment: Optional[HorizontalAlignment] = None """ Controls how the content of every cell in the table should be horizontally aligned by default. """ - vertical_cell_content_alignment: Optional[VerticalAlignment] = None """ Controls how the content of every cell in the table should be vertically aligned by default. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ - rows: Optional[List[TableRow]] = None """ The rows of the table. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -2840,6 +2890,10 @@ def with_columns(self, value: List[ColumnDefinition]) -> Self: self.columns = value return self + def with_min_width(self, value: str) -> Self: + self.min_width = value + return self + def with_first_row_as_headers(self, value: bool) -> Self: self.first_row_as_headers = value return self @@ -2872,81 +2926,66 @@ def with_rows(self, value: List[TableRow]) -> Self: self.rows = value return self - class TextBlock(CardElement): """A block of text, optionally formatted using Markdown.""" - - type: Literal["TextBlock"] = "TextBlock" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['TextBlock'] = 'TextBlock' """ Must be **TextBlock**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - text: Optional[str] = None """ The text to display. A subset of markdown is supported. """ - size: Optional[TextSize] = None """ The size of the text. """ - weight: Optional[TextWeight] = None """ The weight of the text. """ - color: Optional[TextColor] = None """ The color of the text. """ - is_subtle: Optional[bool] = None """ Controls whether the text should be renderer using a subtler variant of the select color. """ - font_type: Optional[FontType] = None """ The type of font to use for rendering. """ - wrap: Optional[bool] = None """ Controls if the text should wrap. """ - max_lines: Optional[float] = None """ The maximum number of lines to display. """ - - style: Optional[StyleEnum] = None + style: Optional[TextBlockStyle] = None """ The style of the text. """ - + label_for: Optional[str] = None + """ The Id of the input the TextBlock should act as the label of. """ grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -3014,10 +3053,14 @@ def with_max_lines(self, value: float) -> Self: self.max_lines = value return self - def with_style(self, value: StyleEnum) -> Self: + def with_style(self, value: TextBlockStyle) -> Self: self.style = value return self + def with_label_for(self, value: str) -> Self: + self.label_for = value + return self + def with_grid_area(self, value: str) -> Self: self.grid_area = value return self @@ -3026,16 +3069,19 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class Fact(SerializableObject): """A fact in a FactSet element.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ title: Optional[str] = None """ The fact's title. """ - value: Optional[str] = None """ The fact's value. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_title(self, value: str) -> Self: self.title = value return self @@ -3044,54 +3090,46 @@ def with_value(self, value: str) -> Self: self.value = value return self - class FactSet(CardElement): """A set of facts, displayed as a table or a vertical list when horizontal space is constrained.""" - - type: Literal["FactSet"] = "FactSet" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['FactSet'] = 'FactSet' """ Must be **FactSet**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - facts: Optional[List[Fact]] = None """ The facts in the set. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -3135,92 +3173,87 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class TeamsImageProperties(SerializableObject): """Represents a set of Teams-specific properties on an image.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ allow_expand: Optional[bool] = None """ Controls if the image is expandable in Teams. This property is equivalent to the Image.allowExpand property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_allow_expand(self, value: bool) -> Self: self.allow_expand = value return self - class Image(CardElement): """A standalone image element.""" - - type: Literal["Image"] = "Image" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Image'] = 'Image' """ Must be **Image**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - url: Optional[str] = None """ The URL (or Base64-encoded Data URI) of the image. Acceptable formats are PNG, JPEG, GIF and SVG. """ - alt_text: Optional[str] = None """ The alternate text for the image, used for accessibility purposes. """ - background_color: Optional[str] = None """ The background color of the image. """ - - style: Optional[ImageStyle] = None + style: Optional[ImageStyle] = "Default" """ The style of the image. """ - - size: Optional[Size] = None + size: Optional[Size] = "Auto" """ The size of the image. """ - width: Optional[str] = "auto" """ The width of the image. """ - - select_action: Optional[Action] = None + select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the image is tapped or clicked. Action.ShowCard is not supported. """ - allow_expand: Optional[bool] = None """ Controls if the image can be expanded to full screen. """ - - ms_teams: Optional[TeamsImageProperties] = None + msteams: Optional[TeamsImageProperties] = None """ Teams-specific metadata associated with the image. """ - + themed_urls: Optional[List[ThemedUrl]] = None + """ A set of theme-specific image URLs. """ + fit_mode: Optional[ImageFitMode] = "Fill" + """ Controls how the image should be fitted inside its bounding box. imageFit is only meaningful when both the width and height properties are set. When fitMode is set to contain, the default style is always used. """ + horizontal_content_alignment: Optional[HorizontalAlignment] = "Left" + """ Controls the horizontal position of the image within its bounding box. horizontalContentAlignment is only meaningful when both the width and height properties are set and fitMode is set to either cover or contain. """ + vertical_content_alignment: Optional[VerticalAlignment] = "Top" + """ Controls the vertical position of the image within its bounding box. verticalContentAlignment is only meaningful when both the width and height properties are set and fitMode is set to either cover or contain. """ height: Optional[str] = "auto" """ The height of the image. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -3284,8 +3317,24 @@ def with_allow_expand(self, value: bool) -> Self: self.allow_expand = value return self - def with_ms_teams(self, value: TeamsImageProperties) -> Self: - self.ms_teams = value + def with_msteams(self, value: TeamsImageProperties) -> Self: + self.msteams = value + return self + + def with_themed_urls(self, value: List[ThemedUrl]) -> Self: + self.themed_urls = value + return self + + def with_fit_mode(self, value: ImageFitMode) -> Self: + self.fit_mode = value + return self + + def with_horizontal_content_alignment(self, value: HorizontalAlignment) -> Self: + self.horizontal_content_alignment = value + return self + + def with_vertical_content_alignment(self, value: VerticalAlignment) -> Self: + self.vertical_content_alignment = value return self def with_height(self, value: str) -> Self: @@ -3300,60 +3349,50 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class ImageSet(CardElement): """A set of images, displayed side-by-side and wrapped across multiple rows as needed.""" - - type: Literal["ImageSet"] = "ImageSet" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['ImageSet'] = 'ImageSet' """ Must be **ImageSet**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - images: Optional[List[Image]] = None """ The images in the set. """ - - image_size: Optional[ImageSize] = None + image_size: Optional[ImageSize] = "Medium" """ The size to use to render all images in the set. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -3405,86 +3444,68 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class TextInput(CardElement): """An input to allow the user to enter text.""" - - type: Literal["Input.Text"] = "Input.Text" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Input.Text'] = 'Input.Text' """ Must be **Input.Text**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - label: Optional[str] = None """ The label of the input. A label should **always** be provided to ensure the best user experience especially for users of assistive technology. """ - is_required: Optional[bool] = None """ Controls whether the input is required. See [Input validation](topic:input-validation) for more details. """ - error_message: Optional[str] = None """ The error message to display when the input fails validation. See [Input validation](topic:input-validation) for more details. """ - - value_changed_action: Optional[ResetInputsAction] = None + value_changed_action: Optional[SerializeAsAny[Action]] = None """ An Action.ResetInputs action that will be executed when the value of the input changes. """ - value: Optional[str] = None """ The default value of the input. """ - max_length: Optional[float] = None """ The maximum length of the text in the input. """ - is_multiline: Optional[bool] = None """ Controls if the input should allow multiple lines of text. """ - placeholder: Optional[str] = None """ The text to display as a placeholder when the user hasn't entered a value. """ - - style: Optional[InputTextStyle] = None + style: Optional[InputTextStyle] = "Text" """ The style of the input. """ - - inline_action: Optional[Action] = None + inline_action: Optional[SerializeAsAny[Action]] = None """ The action that should be displayed as a button alongside the input. Action.ShowCard is not supported. """ - regex: Optional[str] = None """ The regular expression to validate the input. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -3528,7 +3549,7 @@ def with_error_message(self, value: str) -> Self: self.error_message = value return self - def with_value_changed_action(self, value: ResetInputsAction) -> Self: + def with_value_changed_action(self, value: Action) -> Self: self.value_changed_action = value return self @@ -3568,77 +3589,62 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class DateInput(CardElement): """An input to allow the user to select a date.""" - - type: Literal["Input.Date"] = "Input.Date" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Input.Date'] = 'Input.Date' """ Must be **Input.Date**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - label: Optional[str] = None """ The label of the input. A label should **always** be provided to ensure the best user experience especially for users of assistive technology. """ - is_required: Optional[bool] = None """ Controls whether the input is required. See [Input validation](topic:input-validation) for more details. """ - error_message: Optional[str] = None """ The error message to display when the input fails validation. See [Input validation](topic:input-validation) for more details. """ - - value_changed_action: Optional[ResetInputsAction] = None + value_changed_action: Optional[SerializeAsAny[Action]] = None """ An Action.ResetInputs action that will be executed when the value of the input changes. """ - value: Optional[str] = None """ The default value of the input, in the `YYYY-MM-DD` format. """ - placeholder: Optional[str] = None """ The text to display as a placeholder when the user has not selected a date. """ - min: Optional[str] = None """ The minimum date that can be selected. """ - max: Optional[str] = None """ The maximum date that can be selected. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -3682,7 +3688,7 @@ def with_error_message(self, value: str) -> Self: self.error_message = value return self - def with_value_changed_action(self, value: ResetInputsAction) -> Self: + def with_value_changed_action(self, value: Action) -> Self: self.value_changed_action = value return self @@ -3710,77 +3716,62 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class TimeInput(CardElement): """An input to allow the user to select a time.""" - - type: Literal["Input.Time"] = "Input.Time" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Input.Time'] = 'Input.Time' """ Must be **Input.Time**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - label: Optional[str] = None """ The label of the input. A label should **always** be provided to ensure the best user experience especially for users of assistive technology. """ - is_required: Optional[bool] = None """ Controls whether the input is required. See [Input validation](topic:input-validation) for more details. """ - error_message: Optional[str] = None """ The error message to display when the input fails validation. See [Input validation](topic:input-validation) for more details. """ - - value_changed_action: Optional[ResetInputsAction] = None + value_changed_action: Optional[SerializeAsAny[Action]] = None """ An Action.ResetInputs action that will be executed when the value of the input changes. """ - value: Optional[str] = None """ The default value of the input, in the `HH:MM` format. """ - placeholder: Optional[str] = None """ The text to display as a placeholder when the user hasn't entered a value. """ - min: Optional[str] = None """ The minimum time that can be selected, in the `HH:MM` format. """ - max: Optional[str] = None """ The maximum time that can be selected, in the `HH:MM` format. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -3824,7 +3815,7 @@ def with_error_message(self, value: str) -> Self: self.error_message = value return self - def with_value_changed_action(self, value: ResetInputsAction) -> Self: + def with_value_changed_action(self, value: Action) -> Self: self.value_changed_action = value return self @@ -3852,77 +3843,62 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class NumberInput(CardElement): """An input to allow the user to enter a number.""" - - type: Literal["Input.Number"] = "Input.Number" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Input.Number'] = 'Input.Number' """ Must be **Input.Number**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - label: Optional[str] = None """ The label of the input. A label should **always** be provided to ensure the best user experience especially for users of assistive technology. """ - is_required: Optional[bool] = None """ Controls whether the input is required. See [Input validation](topic:input-validation) for more details. """ - error_message: Optional[str] = None """ The error message to display when the input fails validation. See [Input validation](topic:input-validation) for more details. """ - - value_changed_action: Optional[ResetInputsAction] = None + value_changed_action: Optional[SerializeAsAny[Action]] = None """ An Action.ResetInputs action that will be executed when the value of the input changes. """ - value: Optional[float] = None """ The default value of the input. """ - placeholder: Optional[str] = None """ The text to display as a placeholder when the user hasn't entered a value. """ - min: Optional[float] = None """ The minimum value that can be entered. """ - max: Optional[float] = None """ The maximum value that can be entered. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -3966,7 +3942,7 @@ def with_error_message(self, value: str) -> Self: self.error_message = value return self - def with_value_changed_action(self, value: ResetInputsAction) -> Self: + def with_value_changed_action(self, value: Action) -> Self: self.value_changed_action = value return self @@ -3994,80 +3970,66 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class ToggleInput(CardElement): """An input to allow the user to select between on/off states.""" - - type: Literal["Input.Toggle"] = "Input.Toggle" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Input.Toggle'] = 'Input.Toggle' """ Must be **Input.Toggle**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - label: Optional[str] = None """ The label of the input. A label should **always** be provided to ensure the best user experience especially for users of assistive technology. """ - is_required: Optional[bool] = None """ Controls whether the input is required. See [Input validation](topic:input-validation) for more details. """ - error_message: Optional[str] = None """ The error message to display when the input fails validation. See [Input validation](topic:input-validation) for more details. """ - - value_changed_action: Optional[ResetInputsAction] = None + value_changed_action: Optional[SerializeAsAny[Action]] = None """ An Action.ResetInputs action that will be executed when the value of the input changes. """ - value: Optional[str] = "false" """ The default value of the input. """ - title: Optional[str] = None """ The title (caption) to display next to the toggle. """ - value_on: Optional[str] = "true" """ The value to send to the Bot when the toggle is on. """ - value_off: Optional[str] = "false" """ The value to send to the Bot when the toggle is off. """ - wrap: Optional[bool] = True """ Controls if the title should wrap. """ - + show_title: Optional[bool] = True + """ Controls whether the title is visually displayed. When set to false, the title is hidden from view but remains accessible to screen readers for accessibility purposes. """ grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -4111,7 +4073,7 @@ def with_error_message(self, value: str) -> Self: self.error_message = value return self - def with_value_changed_action(self, value: ResetInputsAction) -> Self: + def with_value_changed_action(self, value: Action) -> Self: self.value_changed_action = value return self @@ -4135,6 +4097,10 @@ def with_wrap(self, value: bool) -> Self: self.wrap = value return self + def with_show_title(self, value: bool) -> Self: + self.show_title = value + return self + def with_grid_area(self, value: str) -> Self: self.grid_area = value return self @@ -4143,16 +4109,19 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class Choice(SerializableObject): """A choice as used by the Input.ChoiceSet input.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ title: Optional[str] = None """ The text to display for the choice. """ - value: Optional[str] = None """ The value associated with the choice, as sent to the Bot when an Action.Submit or Action.Execute is invoked """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_title(self, value: str) -> Self: self.title = value return self @@ -4161,25 +4130,25 @@ def with_value(self, value: str) -> Self: self.value = value return self - class QueryData(SerializableObject): """Defines a query to dynamically fetch data from a Bot.""" - - type: Literal["Data.Query"] = "Data.Query" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Data.Query'] = 'Data.Query' """ Must be **Data.Query**. """ - dataset: Optional[str] = None """ The dataset from which to fetch the data. """ - associated_inputs: Optional[AssociatedInputs] = None """ Controls which inputs are associated with the Data.Query. When a Data.Query is executed, the values of the associated inputs are sent to the Bot, allowing it to perform filtering operations based on the user's input. """ - count: Optional[float] = None """ The maximum number of data items that should be returned by the query. Card authors should not specify this property in their card payload. It is determined by the client and sent to the Bot to enable pagination. """ - skip: Optional[float] = None """ The number of data items to be skipped by the query. Card authors should not specify this property in their card payload. It is determined by the client and sent to the Bot to enable pagination. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_dataset(self, value: str) -> Self: self.dataset = value return self @@ -4196,86 +4165,72 @@ def with_skip(self, value: float) -> Self: self.skip = value return self - class ChoiceSetInput(CardElement): """An input to allow the user to select one or more values.""" - - type: Literal["Input.ChoiceSet"] = "Input.ChoiceSet" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Input.ChoiceSet'] = 'Input.ChoiceSet' """ Must be **Input.ChoiceSet**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - label: Optional[str] = None """ The label of the input. A label should **always** be provided to ensure the best user experience especially for users of assistive technology. """ - is_required: Optional[bool] = None """ Controls whether the input is required. See [Input validation](topic:input-validation) for more details. """ - error_message: Optional[str] = None """ The error message to display when the input fails validation. See [Input validation](topic:input-validation) for more details. """ - - value_changed_action: Optional[ResetInputsAction] = None + value_changed_action: Optional[SerializeAsAny[Action]] = None """ An Action.ResetInputs action that will be executed when the value of the input changes. """ - value: Optional[str] = None """ The default value of the input. """ - choices: Optional[List[Choice]] = None """ The choices associated with the input. """ - choices_data: Optional[QueryData] = None """ A Data.Query object that defines the dataset from which to dynamically fetch the choices for the input. """ - - style: Optional[StyleEnum] = None + style: Optional[ChoiceSetInputStyle] = "compact" """ Controls whether the input should be displayed as a dropdown (compact) or a list of radio buttons or checkboxes (expanded). """ - is_multi_select: Optional[bool] = None """ Controls whether multiple choices can be selected. """ - placeholder: Optional[str] = None """ The text to display as a placeholder when the user has not entered any value. """ - wrap: Optional[bool] = True """ Controls if choice titles should wrap. """ - + use_multiple_columns: Optional[bool] = None + """ Controls whether choice items are arranged in multiple columns in expanded mode, or in a single column. Default is false. """ + min_column_width: Optional[str] = None + """ The minimum width, in pixels, for each column when using a multi-column layout. This ensures that choice items remain readable even when horizontal space is limited. Default is 100 pixels. """ grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -4319,7 +4274,7 @@ def with_error_message(self, value: str) -> Self: self.error_message = value return self - def with_value_changed_action(self, value: ResetInputsAction) -> Self: + def with_value_changed_action(self, value: Action) -> Self: self.value_changed_action = value return self @@ -4335,7 +4290,7 @@ def with_choices_data(self, value: QueryData) -> Self: self.choices_data = value return self - def with_style(self, value: StyleEnum) -> Self: + def with_style(self, value: ChoiceSetInputStyle) -> Self: self.style = value return self @@ -4351,6 +4306,14 @@ def with_wrap(self, value: bool) -> Self: self.wrap = value return self + def with_use_multiple_columns(self, value: bool) -> Self: + self.use_multiple_columns = value + return self + + def with_min_column_width(self, value: str) -> Self: + self.min_column_width = value + return self + def with_grid_area(self, value: str) -> Self: self.grid_area = value return self @@ -4359,80 +4322,64 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class RatingInput(CardElement): """An input to allow the user to rate something using stars.""" - - type: Literal["Input.Rating"] = "Input.Rating" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Input.Rating'] = 'Input.Rating' """ Must be **Input.Rating**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - label: Optional[str] = None """ The label of the input. A label should **always** be provided to ensure the best user experience especially for users of assistive technology. """ - is_required: Optional[bool] = None """ Controls whether the input is required. See [Input validation](topic:input-validation) for more details. """ - error_message: Optional[str] = None """ The error message to display when the input fails validation. See [Input validation](topic:input-validation) for more details. """ - - value_changed_action: Optional[ResetInputsAction] = None + value_changed_action: Optional[SerializeAsAny[Action]] = None """ An Action.ResetInputs action that will be executed when the value of the input changes. """ - value: Optional[float] = None """ The default value of the input. """ - max: Optional[float] = 5 """ The number of stars to display. """ - allow_half_steps: Optional[bool] = None """ Controls if the user can select half stars. """ - - size: Optional[RatingSize] = None + size: Optional[RatingSize] = "Large" """ The size of the stars. """ - - color: Optional[RatingColor] = None + color: Optional[RatingColor] = "Neutral" """ The color of the stars. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -4476,7 +4423,7 @@ def with_error_message(self, value: str) -> Self: self.error_message = value return self - def with_value_changed_action(self, value: ResetInputsAction) -> Self: + def with_value_changed_action(self, value: Action) -> Self: self.value_changed_action = value return self @@ -4508,72 +4455,58 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class Rating(CardElement): """A read-only star rating element, to display the rating of something.""" - - type: Literal["Rating"] = "Rating" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Rating'] = 'Rating' """ Must be **Rating**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - value: Optional[float] = None """ The value of the rating. Must be between 0 and max. """ - count: Optional[float] = None """ The number of "votes" associated with the rating. """ - max: Optional[float] = 5 """ The number of stars to display. """ - - size: Optional[RatingSize] = None + size: Optional[RatingSize] = "Large" """ The size of the stars. """ - - color: Optional[RatingColor] = None + color: Optional[RatingColor] = "Neutral" """ The color of the stars. """ - - style: Optional[RatingStyle] = None + style: Optional[RatingStyle] = "Default" """ The style of the stars. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -4641,22 +4574,23 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class IconInfo(SerializableObject): """Defines information about a Fluent icon and how it should be rendered.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ name: Optional[str] = None """ The name of the icon to display. """ - - size: Optional[IconSize] = None + size: Optional[IconSize] = "xSmall" """ The size of the icon. """ - - style: Optional[IconStyle] = None + style: Optional[IconStyle] = "Regular" """ The style of the icon. """ - - color: Optional[TextColor] = None + color: Optional[TextColor] = "Default" """ The color of the icon. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_name(self, value: str) -> Self: self.name = value return self @@ -4673,69 +4607,56 @@ def with_color(self, value: TextColor) -> Self: self.color = value return self - class CompoundButton(CardElement): """A special type of button with an icon, title and description.""" - - type: Literal["CompoundButton"] = "CompoundButton" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['CompoundButton'] = 'CompoundButton' """ Must be **CompoundButton**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - icon: Optional[IconInfo] = None """ The icon to show on the button. """ - badge: Optional[str] = None """ The badge to show on the button. """ - title: Optional[str] = None """ The title of the button. """ - description: Optional[str] = None """ The description text of the button. """ - - select_action: Optional[Action] = None + select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the button is tapped or clicked. Action.ShowCard is not supported. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -4799,66 +4720,54 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class Icon(CardElement): """A standalone icon element. Icons can be picked from the vast [Adaptive Card icon catalog](https://adaptivecards.microsoft.com/?topic=icon-catalog).""" - - type: Literal["Icon"] = "Icon" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Icon'] = 'Icon' """ Must be **Icon**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - name: Optional[str] = None """ The name of the icon to display. """ - - size: Optional[IconSize] = None + size: Optional[IconSize] = "Standard" """ The size of the icon. """ - - style: Optional[IconStyle] = None + style: Optional[IconStyle] = "Regular" """ The style of the icon. """ - - color: Optional[TextColor] = None + color: Optional[TextColor] = "Default" """ The color of the icon. """ - - select_action: Optional[Action] = None + select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the icon is tapped or clicked. Action.ShowCard is not supported. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -4918,78 +4827,62 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class CarouselPage(CardElement): """A page inside a Carousel element.""" - - type: Literal["CarouselPage"] = "CarouselPage" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['CarouselPage'] = 'CarouselPage' """ Must be **CarouselPage**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - - select_action: Optional[Action] = None + select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the element is tapped or clicked. Action.ShowCard is not supported. """ - style: Optional[ContainerStyle] = None """ The style of the container. Container styles control the colors of the background, border and text inside the container, in such a way that contrast requirements are always met. """ - show_border: Optional[bool] = None """ Controls if a border should be displayed around the container. """ - rounded_corners: Optional[bool] = None """ Controls if the container should have rounded corners. """ - layouts: Optional[List[SerializeAsAny[ContainerLayout]]] = None """ The layouts associated with the container. The container can dynamically switch from one layout to another as the card's width changes. See [Container layouts](topic:container-layouts) for more details. """ - min_height: Optional[str] = None """ The minimum height, in pixels, of the container, in the `px` format. """ - background_image: Optional[Union[str, BackgroundImage]] = None """ Defines the container's background image. """ - vertical_content_alignment: Optional[VerticalAlignment] = None """ Controls how the container's content should be vertically aligned. """ - rtl: Optional[bool] = None """ Controls if the content of the card is to be rendered left-to-right or right-to-left. """ - max_height: Optional[str] = None """ The maximum height, in pixels, of the container, in the `px` format. When the content of a container exceeds the container's maximum height, a vertical scrollbar is displayed. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ - items: Optional[List[SerializeAsAny[CardElement]]] = None """ The elements in the page. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -5065,63 +4958,284 @@ def with_items(self, value: List[CardElement]) -> Self: self.items = value return self - class Carousel(CardElement): """A carousel with sliding pages.""" - - type: Literal["Carousel"] = "Carousel" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Carousel'] = 'Carousel' """ Must be **Carousel**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - bleed: Optional[bool] = None """ Controls if the container should bleed into its parent. A bleeding container extends into its parent's padding. """ - min_height: Optional[str] = None """ The minimum height, in pixels, of the container, in the `px` format. """ - - page_animation: Optional[CarouselPageAnimation] = None + page_animation: Optional[CarouselPageAnimation] = "Slide" """ Controls the type of animation to use to navigate between pages. """ + grid_area: Optional[str] = None + """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ + fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None + """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + pages: Optional[List[CarouselPage]] = None + """ The pages in the carousel. """ + + def with_key(self, value: str) -> Self: + self.key = value + return self + + def with_id(self, value: str) -> Self: + self.id = value + return self + + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: + self.requires = value + return self + + def with_lang(self, value: str) -> Self: + self.lang = value + return self + + def with_is_visible(self, value: bool) -> Self: + self.is_visible = value + return self + + def with_separator(self, value: bool) -> Self: + self.separator = value + return self + + def with_height(self, value: ElementHeight) -> Self: + self.height = value + return self + + def with_spacing(self, value: Spacing) -> Self: + self.spacing = value + return self + + def with_target_width(self, value: TargetWidth) -> Self: + self.target_width = value + return self + + def with_is_sort_key(self, value: bool) -> Self: + self.is_sort_key = value + return self + + def with_bleed(self, value: bool) -> Self: + self.bleed = value + return self + + def with_min_height(self, value: str) -> Self: + self.min_height = value + return self + + def with_page_animation(self, value: CarouselPageAnimation) -> Self: + self.page_animation = value + return self + + def with_grid_area(self, value: str) -> Self: + self.grid_area = value + return self + + def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: + self.fallback = value + return self + + def with_pages(self, value: List[CarouselPage]) -> Self: + self.pages = value + return self +class Badge(CardElement): + """A badge element to show an icon and/or text in a compact form over a colored background.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Badge'] = 'Badge' + """ Must be **Badge**. """ + id: Optional[str] = None + """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) + """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ + lang: Optional[str] = None + """ The locale associated with the element. """ + is_visible: Optional[bool] = True + """ Controls the visibility of the element. """ + separator: Optional[bool] = None + """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ + height: Optional[ElementHeight] = "auto" + """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ + horizontal_alignment: Optional[HorizontalAlignment] = None + """ Controls how the element should be horizontally aligned. """ + spacing: Optional[Spacing] = "Default" + """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ + target_width: Optional[TargetWidth] = None + """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ + is_sort_key: Optional[bool] = None + """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ + text: Optional[str] = None + """ The text to display. """ + icon: Optional[str] = None + """ The name of an icon from the [Adaptive Card icon catalog](topic:icon-catalog) to display, in the `[,regular|filled]` format. If the style is not specified, the regular style is used. """ + icon_position: Optional[BadgeIconPosition] = "Before" + """ Controls the position of the icon. """ + appearance: Optional[BadgeAppearance] = "Filled" + """ Controls the strength of the background color. """ + size: Optional[BadgeSize] = "Medium" + """ The size of the badge. """ + shape: Optional[BadgeShape] = "Circular" + """ Controls the shape of the badge. """ + style: Optional[BadgeStyle] = "Default" + """ The style of the badge. """ + tooltip: Optional[str] = None + """ Controls the tooltip text to display when the badge is hovered over. """ + grid_area: Optional[str] = None + """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ + fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None + """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + + def with_key(self, value: str) -> Self: + self.key = value + return self + + def with_id(self, value: str) -> Self: + self.id = value + return self + + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: + self.requires = value + return self + + def with_lang(self, value: str) -> Self: + self.lang = value + return self + + def with_is_visible(self, value: bool) -> Self: + self.is_visible = value + return self + + def with_separator(self, value: bool) -> Self: + self.separator = value + return self + + def with_height(self, value: ElementHeight) -> Self: + self.height = value + return self + + def with_horizontal_alignment(self, value: HorizontalAlignment) -> Self: + self.horizontal_alignment = value + return self + + def with_spacing(self, value: Spacing) -> Self: + self.spacing = value + return self + + def with_target_width(self, value: TargetWidth) -> Self: + self.target_width = value + return self + + def with_is_sort_key(self, value: bool) -> Self: + self.is_sort_key = value + return self + + def with_text(self, value: str) -> Self: + self.text = value + return self + + def with_icon(self, value: str) -> Self: + self.icon = value + return self + + def with_icon_position(self, value: BadgeIconPosition) -> Self: + self.icon_position = value + return self + + def with_appearance(self, value: BadgeAppearance) -> Self: + self.appearance = value + return self + + def with_size(self, value: BadgeSize) -> Self: + self.size = value + return self + + def with_shape(self, value: BadgeShape) -> Self: + self.shape = value + return self + + def with_style(self, value: BadgeStyle) -> Self: + self.style = value + return self + + def with_tooltip(self, value: str) -> Self: + self.tooltip = value + return self + + def with_grid_area(self, value: str) -> Self: + self.grid_area = value + return self + + def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: + self.fallback = value + return self + +class ProgressRing(CardElement): + """A spinning ring element, to indicate progress.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['ProgressRing'] = 'ProgressRing' + """ Must be **ProgressRing**. """ + id: Optional[str] = None + """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) + """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ + lang: Optional[str] = None + """ The locale associated with the element. """ + is_visible: Optional[bool] = True + """ Controls the visibility of the element. """ + separator: Optional[bool] = None + """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ + height: Optional[ElementHeight] = "auto" + """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ + horizontal_alignment: Optional[HorizontalAlignment] = None + """ Controls how the element should be horizontally aligned. """ + spacing: Optional[Spacing] = "Default" + """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ + target_width: Optional[TargetWidth] = None + """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ + is_sort_key: Optional[bool] = None + """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ + label: Optional[str] = None + """ The label of the progress ring. """ + label_position: Optional[ProgressRingLabelPosition] = "Below" + """ Controls the relative position of the label to the progress ring. """ + size: Optional[ProgressRingSize] = "Medium" + """ The size of the progress ring. """ grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ - pages: Optional[List[CarouselPage]] = None - """ The pages in the carousel. """ + def with_key(self, value: str) -> Self: + self.key = value + return self def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -5141,6 +5255,10 @@ def with_height(self, value: ElementHeight) -> Self: self.height = value return self + def with_horizontal_alignment(self, value: HorizontalAlignment) -> Self: + self.horizontal_alignment = value + return self + def with_spacing(self, value: Spacing) -> Self: self.spacing = value return self @@ -5153,16 +5271,16 @@ def with_is_sort_key(self, value: bool) -> Self: self.is_sort_key = value return self - def with_bleed(self, value: bool) -> Self: - self.bleed = value + def with_label(self, value: str) -> Self: + self.label = value return self - def with_min_height(self, value: str) -> Self: - self.min_height = value + def with_label_position(self, value: ProgressRingLabelPosition) -> Self: + self.label_position = value return self - def with_page_animation(self, value: CarouselPageAnimation) -> Self: - self.page_animation = value + def with_size(self, value: ProgressRingSize) -> Self: + self.size = value return self def with_grid_area(self, value: str) -> Self: @@ -5173,82 +5291,52 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - def with_pages(self, value: List[CarouselPage]) -> Self: - self.pages = value - return self - - -class Badge(CardElement): - """A badge element to show an icon and/or text in a compact form over a colored background.""" - - type: Literal["Badge"] = "Badge" - """ Must be **Badge**. """ - +class ProgressBar(CardElement): + """A progress bar element, to represent a value within a range.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['ProgressBar'] = 'ProgressBar' + """ Must be **ProgressBar**. """ id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - - text: Optional[str] = None - """ The text to display. """ - - icon: Optional[str] = None - """ The name of an icon from the [Adaptive Card icon catalog](topic:icon-catalog) to display, in the `[,regular|filled]` format. If the style is not specified, the regular style is used. """ - - icon_position: Optional[BadgeIconPosition] = None - """ Controls the position of the icon. """ - - appearance: Optional[BadgeAppearance] = None - """ Controls the strength of the background color. """ - - size: Optional[BadgeSize] = None - """ The size of the badge. """ - - shape: Optional[BadgeShape] = None - """ Controls the shape of the badge. """ - - style: Optional[BadgeStyle] = None - """ The style of the badge. """ - - tooltip: Optional[str] = None - """ Controls the tooltip text to display when the badge is hovered over. """ - + value: Optional[float] = None + """ The value of the progress bar. Must be between 0 and max. """ + max: Optional[float] = 100 + """ The maximum value of the progress bar. """ + color: Optional[ProgressBarColor] = "Accent" + """ The color of the progress bar. `color` has no effect when the `ProgressBar` is in indeterminate mode, in which case the "accent" color is always used. """ grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -5284,36 +5372,16 @@ def with_is_sort_key(self, value: bool) -> Self: self.is_sort_key = value return self - def with_text(self, value: str) -> Self: - self.text = value - return self - - def with_icon(self, value: str) -> Self: - self.icon = value - return self - - def with_icon_position(self, value: BadgeIconPosition) -> Self: - self.icon_position = value - return self - - def with_appearance(self, value: BadgeAppearance) -> Self: - self.appearance = value - return self - - def with_size(self, value: BadgeSize) -> Self: - self.size = value - return self - - def with_shape(self, value: BadgeShape) -> Self: - self.shape = value + def with_value(self, value: float) -> Self: + self.value = value return self - def with_style(self, value: BadgeStyle) -> Self: - self.style = value + def with_max(self, value: float) -> Self: + self.max = value return self - def with_tooltip(self, value: str) -> Self: - self.tooltip = value + def with_color(self, value: ProgressBarColor) -> Self: + self.color = value return self def with_grid_area(self, value: str) -> Self: @@ -5324,19 +5392,21 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class DonutChartData(SerializableObject): """A data point in a Donut chart.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ legend: Optional[str] = None """ The legend of the chart. """ - value: Optional[float] = None """ The value associated with the data point. """ - color: Optional[ChartColor] = None """ The color to use for the data point. See [Chart colors reference](topic:chart-colors-reference). """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_legend(self, value: str) -> Self: self.legend = value return self @@ -5349,63 +5419,66 @@ def with_color(self, value: ChartColor) -> Self: self.color = value return self - class DonutChart(CardElement): """A donut chart.""" - - type: Literal["Chart.Donut"] = "Chart.Donut" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Chart.Donut'] = 'Chart.Donut' """ Must be **Chart.Donut**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - title: Optional[str] = None """ The title of the chart. """ - + show_title: Optional[bool] = None + """ Controls whether the chart's title should be displayed. Defaults to `false`. """ color_set: Optional[ChartColorSet] = None """ The name of the set of colors to use to render the chart. See [Chart colors reference](topic:chart-colors-reference). """ - + max_width: Optional[str] = None + """ The maximum width, in pixels, of the chart, in the `px` format. """ + show_legend: Optional[bool] = True + """ Controls whether the chart's legend should be displayed. """ data: Optional[List[DonutChartData]] = None """ The data to display in the chart. """ - + value: Optional[str] = None + """ The value that should be displayed in the center of a Donut chart. `value` is ignored for Pie charts. """ + value_color: Optional[ChartColor] = None + """ Controls the color of the value displayed in the center of a Donut chart. """ + thickness: Optional[DonutThickness] = None + """ Controls the thickness of the donut segments. Default is **Thick**. """ + show_outlines: Optional[bool] = True + """ Controls whether the outlines of the donut segments are displayed. """ grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -5445,14 +5518,42 @@ def with_title(self, value: str) -> Self: self.title = value return self + def with_show_title(self, value: bool) -> Self: + self.show_title = value + return self + def with_color_set(self, value: ChartColorSet) -> Self: self.color_set = value return self + def with_max_width(self, value: str) -> Self: + self.max_width = value + return self + + def with_show_legend(self, value: bool) -> Self: + self.show_legend = value + return self + def with_data(self, value: List[DonutChartData]) -> Self: self.data = value return self + def with_value(self, value: str) -> Self: + self.value = value + return self + + def with_value_color(self, value: ChartColor) -> Self: + self.value_color = value + return self + + def with_thickness(self, value: DonutThickness) -> Self: + self.thickness = value + return self + + def with_show_outlines(self, value: bool) -> Self: + self.show_outlines = value + return self + def with_grid_area(self, value: str) -> Self: self.grid_area = value return self @@ -5461,63 +5562,66 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class PieChart(CardElement): """A pie chart.""" - - type: Literal["Chart.Pie"] = "Chart.Pie" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Chart.Pie'] = 'Chart.Pie' """ Must be **Chart.Pie**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - title: Optional[str] = None """ The title of the chart. """ - + show_title: Optional[bool] = None + """ Controls whether the chart's title should be displayed. Defaults to `false`. """ color_set: Optional[ChartColorSet] = None """ The name of the set of colors to use to render the chart. See [Chart colors reference](topic:chart-colors-reference). """ - + max_width: Optional[str] = None + """ The maximum width, in pixels, of the chart, in the `px` format. """ + show_legend: Optional[bool] = True + """ Controls whether the chart's legend should be displayed. """ data: Optional[List[DonutChartData]] = None """ The data to display in the chart. """ - + value: Optional[str] = None + """ The value that should be displayed in the center of a Donut chart. `value` is ignored for Pie charts. """ + value_color: Optional[ChartColor] = None + """ Controls the color of the value displayed in the center of a Donut chart. """ + thickness: Optional[DonutThickness] = None + """ Controls the thickness of the donut segments. Default is **Thick**. """ + show_outlines: Optional[bool] = True + """ Controls whether the outlines of the donut segments are displayed. """ grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -5557,14 +5661,42 @@ def with_title(self, value: str) -> Self: self.title = value return self + def with_show_title(self, value: bool) -> Self: + self.show_title = value + return self + def with_color_set(self, value: ChartColorSet) -> Self: self.color_set = value return self + def with_max_width(self, value: str) -> Self: + self.max_width = value + return self + + def with_show_legend(self, value: bool) -> Self: + self.show_legend = value + return self + def with_data(self, value: List[DonutChartData]) -> Self: self.data = value return self + def with_value(self, value: str) -> Self: + self.value = value + return self + + def with_value_color(self, value: ChartColor) -> Self: + self.value_color = value + return self + + def with_thickness(self, value: DonutThickness) -> Self: + self.thickness = value + return self + + def with_show_outlines(self, value: bool) -> Self: + self.show_outlines = value + return self + def with_grid_area(self, value: str) -> Self: self.grid_area = value return self @@ -5573,16 +5705,19 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class BarChartDataValue(SerializableObject): """A single data point in a bar chart.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ x: Optional[str] = None """ The x axis value of the data point. """ - y: Optional[float] = None """ The y axis value of the data point. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_x(self, value: str) -> Self: self.x = value return self @@ -5591,19 +5726,21 @@ def with_y(self, value: float) -> Self: self.y = value return self - class GroupedVerticalBarChartData(SerializableObject): """Represents a series of data points.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ legend: Optional[str] = None """ The legend of the chart. """ - values: Optional[List[BarChartDataValue]] = None """ The data points in the series. """ - color: Optional[ChartColor] = None """ The color to use for all data points in the series. See [Chart colors reference](topic:chart-colors-reference). """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_legend(self, value: str) -> Self: self.legend = value return self @@ -5616,78 +5753,78 @@ def with_color(self, value: ChartColor) -> Self: self.color = value return self - class GroupedVerticalBarChart(CardElement): """A grouped vertical bar chart.""" - - type: Literal["Chart.VerticalBar.Grouped"] = "Chart.VerticalBar.Grouped" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Chart.VerticalBar.Grouped'] = 'Chart.VerticalBar.Grouped' """ Must be **Chart.VerticalBar.Grouped**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - title: Optional[str] = None """ The title of the chart. """ - + show_title: Optional[bool] = None + """ Controls whether the chart's title should be displayed. Defaults to `false`. """ color_set: Optional[ChartColorSet] = None """ The name of the set of colors to use to render the chart. See [Chart colors reference](topic:chart-colors-reference). """ - + max_width: Optional[str] = None + """ The maximum width, in pixels, of the chart, in the `px` format. """ + show_legend: Optional[bool] = True + """ Controls whether the chart's legend should be displayed. """ x_axis_title: Optional[str] = None """ The title of the x axis. """ - y_axis_title: Optional[str] = None """ The title of the y axis. """ - color: Optional[ChartColor] = None """ The color to use for all data points. See [Chart colors reference](topic:chart-colors-reference). """ - stacked: Optional[bool] = None - """ Controls if bars in the chart should be displayed as stacked instead of grouped. """ + """ Controls if bars in the chart should be displayed as stacks instead of groups. +**Note:** stacked vertical bar charts do not support custom Y ranges nor negative Y values. """ data: Optional[List[GroupedVerticalBarChartData]] = None """ The data points in a series. """ - show_bar_values: Optional[bool] = None """ Controls if values should be displayed on each bar. """ + y_min: Optional[float] = None + """ The requested minimum for the Y axis range. The value used at runtime may be different to optimize visual presentation. + +`yMin` is ignored if `stacked` is set to `true`. """ + y_max: Optional[float] = None + """ The requested maximum for the Y axis range. The value used at runtime may be different to optimize visual presentation. +`yMax` is ignored if `stacked` is set to `true`. """ grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -5727,10 +5864,22 @@ def with_title(self, value: str) -> Self: self.title = value return self + def with_show_title(self, value: bool) -> Self: + self.show_title = value + return self + def with_color_set(self, value: ChartColorSet) -> Self: self.color_set = value return self + def with_max_width(self, value: str) -> Self: + self.max_width = value + return self + + def with_show_legend(self, value: bool) -> Self: + self.show_legend = value + return self + def with_x_axis_title(self, value: str) -> Self: self.x_axis_title = value return self @@ -5755,6 +5904,14 @@ def with_show_bar_values(self, value: bool) -> Self: self.show_bar_values = value return self + def with_y_min(self, value: float) -> Self: + self.y_min = value + return self + + def with_y_max(self, value: float) -> Self: + self.y_max = value + return self + def with_grid_area(self, value: str) -> Self: self.grid_area = value return self @@ -5763,19 +5920,21 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class VerticalBarChartDataValue(SerializableObject): """Represents a data point in a vertical bar chart.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ x: Optional[Union[str, float]] = None """ The x axis value of the data point. """ - y: Optional[float] = None """ The y axis value of the data point. """ - color: Optional[ChartColor] = None """ The color to use for the bar associated with the data point. See [Chart colors reference](topic:chart-colors-reference). """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_x(self, value: Union[str, float]) -> Self: self.x = value return self @@ -5788,75 +5947,70 @@ def with_color(self, value: ChartColor) -> Self: self.color = value return self - class VerticalBarChart(CardElement): """A vertical bar chart.""" - - type: Literal["Chart.VerticalBar"] = "Chart.VerticalBar" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Chart.VerticalBar'] = 'Chart.VerticalBar' """ Must be **Chart.VerticalBar**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - title: Optional[str] = None """ The title of the chart. """ - + show_title: Optional[bool] = None + """ Controls whether the chart's title should be displayed. Defaults to `false`. """ color_set: Optional[ChartColorSet] = None """ The name of the set of colors to use to render the chart. See [Chart colors reference](topic:chart-colors-reference). """ - + max_width: Optional[str] = None + """ The maximum width, in pixels, of the chart, in the `px` format. """ + show_legend: Optional[bool] = True + """ Controls whether the chart's legend should be displayed. """ x_axis_title: Optional[str] = None """ The title of the x axis. """ - y_axis_title: Optional[str] = None """ The title of the y axis. """ - data: Optional[List[VerticalBarChartDataValue]] = None """ The data to display in the chart. """ - color: Optional[ChartColor] = None """ The color to use for all data points. See [Chart colors reference](topic:chart-colors-reference). """ - show_bar_values: Optional[bool] = None """ Controls if the bar values should be displayed. """ - + y_min: Optional[float] = None + """ The requested minimum for the Y axis range. The value used at runtime may be different to optimize visual presentation. """ + y_max: Optional[float] = None + """ The requested maximum for the Y axis range. The value used at runtime may be different to optimize visual presentation. """ grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -5896,10 +6050,22 @@ def with_title(self, value: str) -> Self: self.title = value return self + def with_show_title(self, value: bool) -> Self: + self.show_title = value + return self + def with_color_set(self, value: ChartColorSet) -> Self: self.color_set = value return self + def with_max_width(self, value: str) -> Self: + self.max_width = value + return self + + def with_show_legend(self, value: bool) -> Self: + self.show_legend = value + return self + def with_x_axis_title(self, value: str) -> Self: self.x_axis_title = value return self @@ -5920,6 +6086,14 @@ def with_show_bar_values(self, value: bool) -> Self: self.show_bar_values = value return self + def with_y_min(self, value: float) -> Self: + self.y_min = value + return self + + def with_y_max(self, value: float) -> Self: + self.y_max = value + return self + def with_grid_area(self, value: str) -> Self: self.grid_area = value return self @@ -5928,19 +6102,21 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class HorizontalBarChartDataValue(SerializableObject): """Represents a single data point in a horizontal bar chart.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ x: Optional[str] = None """ The x axis value of the data point. """ - y: Optional[float] = None """ The y axis value of the data point. """ - color: Optional[ChartColor] = None """ The color of the bar associated with the data point. See [Chart colors reference](topic:chart-colors-reference). """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_x(self, value: str) -> Self: self.x = value return self @@ -5953,75 +6129,66 @@ def with_color(self, value: ChartColor) -> Self: self.color = value return self - class HorizontalBarChart(CardElement): """A horizontal bar chart.""" - - type: Literal["Chart.HorizontalBar"] = "Chart.HorizontalBar" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Chart.HorizontalBar'] = 'Chart.HorizontalBar' """ Must be **Chart.HorizontalBar**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - title: Optional[str] = None """ The title of the chart. """ - + show_title: Optional[bool] = None + """ Controls whether the chart's title should be displayed. Defaults to `false`. """ color_set: Optional[ChartColorSet] = None """ The name of the set of colors to use to render the chart. See [Chart colors reference](topic:chart-colors-reference). """ - + max_width: Optional[str] = None + """ The maximum width, in pixels, of the chart, in the `px` format. """ + show_legend: Optional[bool] = True + """ Controls whether the chart's legend should be displayed. """ x_axis_title: Optional[str] = None """ The title of the x axis. """ - y_axis_title: Optional[str] = None """ The title of the y axis. """ - color: Optional[ChartColor] = None """ The color to use for all data points. See [Chart colors reference](topic:chart-colors-reference). """ - data: Optional[List[HorizontalBarChartDataValue]] = None """ The data points in the chart. """ - - display_mode: Optional[HorizontalBarChartDisplayMode] = None + display_mode: Optional[HorizontalBarChartDisplayMode] = "AbsoluteWithAxis" """ Controls how the chart should be visually laid out. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -6061,10 +6228,22 @@ def with_title(self, value: str) -> Self: self.title = value return self + def with_show_title(self, value: bool) -> Self: + self.show_title = value + return self + def with_color_set(self, value: ChartColorSet) -> Self: self.color_set = value return self + def with_max_width(self, value: str) -> Self: + self.max_width = value + return self + + def with_show_legend(self, value: bool) -> Self: + self.show_legend = value + return self + def with_x_axis_title(self, value: str) -> Self: self.x_axis_title = value return self @@ -6093,19 +6272,21 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class StackedHorizontalBarChartDataPoint(SerializableObject): """A data point in a series.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ legend: Optional[str] = None """ The legend associated with the data point. """ - value: Optional[float] = None """ The value of the data point. """ - color: Optional[ChartColor] = None """ The color to use to render the bar associated with the data point. See [Chart colors reference](topic:chart-colors-reference). """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_legend(self, value: str) -> Self: self.legend = value return self @@ -6118,16 +6299,19 @@ def with_color(self, value: ChartColor) -> Self: self.color = value return self - class StackedHorizontalBarChartData(SerializableObject): """Defines the collection of data series to display in as a stacked horizontal bar chart.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ title: Optional[str] = None """ The title of the series. """ - data: Optional[List[StackedHorizontalBarChartDataPoint]] = None """ The data points in the series. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_title(self, value: str) -> Self: self.title = value return self @@ -6136,72 +6320,64 @@ def with_data(self, value: List[StackedHorizontalBarChartDataPoint]) -> Self: self.data = value return self - class StackedHorizontalBarChart(CardElement): """A stacked horizontal bar chart.""" - - type: Literal["Chart.HorizontalBar.Stacked"] = "Chart.HorizontalBar.Stacked" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Chart.HorizontalBar.Stacked'] = 'Chart.HorizontalBar.Stacked' """ Must be **Chart.HorizontalBar.Stacked**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - title: Optional[str] = None """ The title of the chart. """ - + show_title: Optional[bool] = None + """ Controls whether the chart's title should be displayed. Defaults to `false`. """ color_set: Optional[ChartColorSet] = None """ The name of the set of colors to use to render the chart. See [Chart colors reference](topic:chart-colors-reference). """ - + max_width: Optional[str] = None + """ The maximum width, in pixels, of the chart, in the `px` format. """ + show_legend: Optional[bool] = True + """ Controls whether the chart's legend should be displayed. """ x_axis_title: Optional[str] = None """ The title of the x axis. """ - y_axis_title: Optional[str] = None """ The title of the y axis. """ - color: Optional[ChartColor] = None """ The color to use for all data points. See [Chart colors reference](topic:chart-colors-reference). """ - data: Optional[List[StackedHorizontalBarChartData]] = None """ The data to display in the chart. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -6241,10 +6417,22 @@ def with_title(self, value: str) -> Self: self.title = value return self + def with_show_title(self, value: bool) -> Self: + self.show_title = value + return self + def with_color_set(self, value: ChartColorSet) -> Self: self.color_set = value return self + def with_max_width(self, value: str) -> Self: + self.max_width = value + return self + + def with_show_legend(self, value: bool) -> Self: + self.show_legend = value + return self + def with_x_axis_title(self, value: str) -> Self: self.x_axis_title = value return self @@ -6269,20 +6457,23 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class LineChartValue(SerializableObject): """Represents a single data point in a line chart.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ x: Optional[Union[float, str]] = None """ The x axis value of the data point. If all x values of the x [Chart.Line](topic:Chart.Line) are expressed as a number, or if all x values are expressed as a date string in the `YYYY-MM-DD` format, the chart will be rendered as a time series chart, i.e. x axis values will span across the minimum x value to maximum x value range. Otherwise, if x values are represented as a mix of numbers and strings or if at least one x value isn't in the `YYYY-MM-DD` format, the chart will be rendered as a categorical chart, i.e. x axis values will be displayed as categories. """ - y: Optional[float] = None """ The y axis value of the data point. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_x(self, value: Union[float, str]) -> Self: self.x = value return self @@ -6291,19 +6482,21 @@ def with_y(self, value: float) -> Self: self.y = value return self - class LineChartData(SerializableObject): """Represents a collection of data points series in a line chart.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ legend: Optional[str] = None """ The legend of the chart. """ - values: Optional[List[LineChartValue]] = None """ The data points in the series. """ - color: Optional[ChartColor] = None """ The color all data points in the series. See [Chart colors reference](topic:chart-colors-reference). """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_legend(self, value: str) -> Self: self.legend = value return self @@ -6316,78 +6509,68 @@ def with_color(self, value: ChartColor) -> Self: self.color = value return self - class LineChart(CardElement): """A line chart.""" - - type: Literal["Chart.Line"] = "Chart.Line" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Chart.Line'] = 'Chart.Line' """ Must be **Chart.Line**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - title: Optional[str] = None """ The title of the chart. """ - + show_title: Optional[bool] = None + """ Controls whether the chart's title should be displayed. Defaults to `false`. """ color_set: Optional[ChartColorSet] = None """ The name of the set of colors to use to render the chart. See [Chart colors reference](topic:chart-colors-reference). """ - + max_width: Optional[str] = None + """ The maximum width, in pixels, of the chart, in the `px` format. """ + show_legend: Optional[bool] = True + """ Controls whether the chart's legend should be displayed. """ x_axis_title: Optional[str] = None """ The title of the x axis. """ - y_axis_title: Optional[str] = None """ The title of the y axis. """ - color: Optional[ChartColor] = None """ The color to use for all data points. See [Chart colors reference](topic:chart-colors-reference). """ - data: Optional[List[LineChartData]] = None """ The data point series in the line chart. """ - y_min: Optional[float] = None """ The maximum y range. """ - y_max: Optional[float] = None """ The minimum y range. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -6427,10 +6610,22 @@ def with_title(self, value: str) -> Self: self.title = value return self + def with_show_title(self, value: bool) -> Self: + self.show_title = value + return self + def with_color_set(self, value: ChartColorSet) -> Self: self.color_set = value return self + def with_max_width(self, value: str) -> Self: + self.max_width = value + return self + + def with_show_legend(self, value: bool) -> Self: + self.show_legend = value + return self + def with_x_axis_title(self, value: str) -> Self: self.x_axis_title = value return self @@ -6463,19 +6658,21 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class GaugeChartLegend(SerializableObject): """The legend of the chart.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ size: Optional[float] = None """ The size of the segment. """ - legend: Optional[str] = None """ The legend text associated with the segment. """ - color: Optional[ChartColor] = None """ The color to use for the segment. See [Chart colors reference](topic:chart-colors-reference). """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_size(self, value: float) -> Self: self.size = value return self @@ -6488,84 +6685,74 @@ def with_color(self, value: ChartColor) -> Self: self.color = value return self - class GaugeChart(CardElement): """A gauge chart.""" - - type: Literal["Chart.Gauge"] = "Chart.Gauge" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Chart.Gauge'] = 'Chart.Gauge' """ Must be **Chart.Gauge**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - title: Optional[str] = None """ The title of the chart. """ - + show_title: Optional[bool] = None + """ Controls whether the chart's title should be displayed. Defaults to `false`. """ color_set: Optional[ChartColorSet] = None """ The name of the set of colors to use to render the chart. See [Chart colors reference](topic:chart-colors-reference). """ - + max_width: Optional[str] = None + """ The maximum width, in pixels, of the chart, in the `px` format. """ + show_legend: Optional[bool] = True + """ Controls whether the chart's legend should be displayed. """ min: Optional[float] = None """ The minimum value of the gauge. """ - max: Optional[float] = None """ The maximum value of the gauge. """ - sub_label: Optional[str] = None """ The sub-label of the gauge. """ - show_min_max: Optional[bool] = True - """ Controls if the min/max values should be displayed. """ - - show_legend: Optional[bool] = True - """ Controls if the legend should be displayed. """ - + """ Controls whether the min/max values should be displayed. """ + show_needle: Optional[bool] = True + """ Controls whether the gauge's needle is displayed. Default is **true**. """ + show_outlines: Optional[bool] = True + """ Controls whether the outlines of the gauge segments are displayed. """ segments: Optional[List[GaugeChartLegend]] = None """ The segments to display in the gauge. """ - value: Optional[float] = None """ The value of the gauge. """ - - value_format: Optional[GaugeChartValueFormat] = None + value_format: Optional[GaugeChartValueFormat] = "Percentage" """ The format used to display the gauge's value. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -6605,10 +6792,22 @@ def with_title(self, value: str) -> Self: self.title = value return self + def with_show_title(self, value: bool) -> Self: + self.show_title = value + return self + def with_color_set(self, value: ChartColorSet) -> Self: self.color_set = value return self + def with_max_width(self, value: str) -> Self: + self.max_width = value + return self + + def with_show_legend(self, value: bool) -> Self: + self.show_legend = value + return self + def with_min(self, value: float) -> Self: self.min = value return self @@ -6625,8 +6824,12 @@ def with_show_min_max(self, value: bool) -> Self: self.show_min_max = value return self - def with_show_legend(self, value: bool) -> Self: - self.show_legend = value + def with_show_needle(self, value: bool) -> Self: + self.show_needle = value + return self + + def with_show_outlines(self, value: bool) -> Self: + self.show_outlines = value return self def with_segments(self, value: List[GaugeChartLegend]) -> Self: @@ -6649,63 +6852,52 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class CodeBlock(CardElement): """A formatted and syntax-colored code block.""" - - type: Literal["CodeBlock"] = "CodeBlock" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['CodeBlock'] = 'CodeBlock' """ Must be **CodeBlock**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - code_snippet: Optional[str] = None """ The code snippet to display. """ - - language: Optional[CodeLanguage] = None + language: Optional[CodeLanguage] = "PlainText" """ The language the code snippet is expressed in. """ - start_line_number: Optional[float] = 1 """ A number that represents the line in the file from where the code snippet was extracted. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -6761,15 +6953,28 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class PersonaProperties(SerializableObject): """Represents the properties of a Persona component.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + id: Optional[str] = None + """ The Id of the persona. """ user_principal_name: Optional[str] = None """ The UPN of the persona. """ - display_name: Optional[str] = None """ The display name of the persona. """ + icon_style: Optional[PersonaIconStyle] = None + """ Defines the style of the icon for the persona. """ + style: Optional[PersonaDisplayStyle] = None + """ Defines how the persona should be displayed. """ + + def with_key(self, value: str) -> Self: + self.key = value + return self + + def with_id(self, value: str) -> Self: + self.id = value + return self def with_user_principal_name(self, value: str) -> Self: self.user_principal_name = value @@ -6779,60 +6984,58 @@ def with_display_name(self, value: str) -> Self: self.display_name = value return self + def with_icon_style(self, value: PersonaIconStyle) -> Self: + self.icon_style = value + return self + + def with_style(self, value: PersonaDisplayStyle) -> Self: + self.style = value + return self class ComUserMicrosoftGraphComponent(CardElement): """Displays a user's information, including their profile picture.""" - - type: Literal["Component"] = "Component" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Component'] = 'Component' """ Must be **Component**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - - name: Literal["graph.microsoft.com/user"] = "graph.microsoft.com/user" + name: Literal['graph.microsoft.com/user'] = 'graph.microsoft.com/user' """ Must be **graph.microsoft.com/user**. """ - properties: Optional[PersonaProperties] = None - """ The properties of the user. """ - + """ The properties of the Persona component. """ grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -6880,71 +7083,77 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class PersonaSetProperties(SerializableObject): """Represents the properties of a PersonaSet component.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ users: Optional[List[PersonaProperties]] = None """ The users a PersonaSet component should display. """ + icon_style: Optional[PersonaIconStyle] = None + """ Defines the style of the icon for the personas in the set. """ + style: Optional[PersonaDisplayStyle] = None + """ Defines how each persona in the set should be displayed. """ + + def with_key(self, value: str) -> Self: + self.key = value + return self def with_users(self, value: List[PersonaProperties]) -> Self: self.users = value return self + def with_icon_style(self, value: PersonaIconStyle) -> Self: + self.icon_style = value + return self + + def with_style(self, value: PersonaDisplayStyle) -> Self: + self.style = value + return self class ComUsersMicrosoftGraphComponent(CardElement): """Displays multiple users' information, including their profile pictures.""" - - type: Literal["Component"] = "Component" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Component'] = 'Component' """ Must be **Component**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - - name: Literal["graph.microsoft.com/users"] = "graph.microsoft.com/users" + name: Literal['graph.microsoft.com/users'] = 'graph.microsoft.com/users' """ Must be **graph.microsoft.com/users**. """ - properties: Optional[PersonaSetProperties] = None - """ The properties of the set. """ - + """ The properties of the PersonaSet component. """ grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -6992,30 +7201,36 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class ResourceVisualization(SerializableObject): """Represents a visualization of a resource.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ media: Optional[str] = None """ The media associated with the resource. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_media(self, value: str) -> Self: self.media = value return self - class ResourceProperties(SerializableObject): """Represents the properties of a resource component.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ id: Optional[str] = None """ The Id of the resource. """ - resource_reference: Optional[Dict[str, str]] = None """ The reference to the resource. """ - resource_visualization: Optional[ResourceVisualization] = None """ The visualization of the resource. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self @@ -7028,60 +7243,50 @@ def with_resource_visualization(self, value: ResourceVisualization) -> Self: self.resource_visualization = value return self - class ComResourceMicrosoftGraphComponent(CardElement): """Displays information about a generic graph resource.""" - - type: Literal["Component"] = "Component" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Component'] = 'Component' """ Must be **Component**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - - name: Literal["graph.microsoft.com/resource"] = "graph.microsoft.com/resource" + name: Literal['graph.microsoft.com/resource'] = 'graph.microsoft.com/resource' """ Must be **graph.microsoft.com/resource**. """ - properties: Optional[ResourceProperties] = None """ The properties of the resource. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -7129,19 +7334,21 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class FileProperties(SerializableObject): """Represents the properties of a file component.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ name: Optional[str] = None """ The name of the file. """ - extension: Optional[str] = None """ The file extension. """ - url: Optional[str] = None """ The URL of the file. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_name(self, value: str) -> Self: self.name = value return self @@ -7154,60 +7361,50 @@ def with_url(self, value: str) -> Self: self.url = value return self - class ComFileMicrosoftGraphComponent(CardElement): """Displays information about a file resource.""" - - type: Literal["Component"] = "Component" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Component'] = 'Component' """ Must be **Component**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - - name: Literal["graph.microsoft.com/file"] = "graph.microsoft.com/file" + name: Literal['graph.microsoft.com/file'] = 'graph.microsoft.com/file' """ Must be **graph.microsoft.com/file**. """ - properties: Optional[FileProperties] = None """ The properties of the file. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -7255,25 +7452,25 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class CalendarEventAttendee(SerializableObject): """Represents a calendar event attendee.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ name: Optional[str] = None """ The name of the attendee. """ - email: Optional[str] = None """ The email address of the attendee. """ - title: Optional[str] = None """ The title of the attendee. """ - type: Optional[str] = None """ The type of the attendee. """ - status: Optional[str] = None """ The status of the attendee. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_name(self, value: str) -> Self: self.name = value return self @@ -7294,46 +7491,39 @@ def with_status(self, value: str) -> Self: self.status = value return self - class CalendarEventProperties(SerializableObject): """The properties of a calendar event.""" - + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ id: Optional[str] = None """ The ID of the event. """ - title: Optional[str] = None """ The title of the event. """ - start: Optional[str] = None """ The start date and time of the event. """ - end: Optional[str] = None """ The end date and time of the event. """ - status: Optional[str] = None """ The status of the event. """ - locations: Optional[List[str]] = None """ The locations of the event. """ - online_meeting_url: Optional[str] = None """ The URL of the online meeting. """ - is_all_day: Optional[bool] = None """ Indicates if the event is all day. """ - extension: Optional[str] = None """ The extension of the event. """ - url: Optional[str] = None """ The URL of the event. """ - attendees: Optional[List[CalendarEventAttendee]] = None """ The attendees of the event. """ - organizer: Optional[CalendarEventAttendee] = None """ The organizer of the event. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self @@ -7382,60 +7572,50 @@ def with_organizer(self, value: CalendarEventAttendee) -> Self: self.organizer = value return self - class ComEventMicrosoftGraphComponent(CardElement): """Displays information about a calendar event.""" - - type: Literal["Component"] = "Component" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['Component'] = 'Component' """ Must be **Component**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - - requires: Optional[HostCapabilities] = Field(default_factory=HostCapabilities) + requires: Optional[Union[HostCapabilities, Dict[str, Any]]] = Field(default_factory=HostCapabilities) """ A list of capabilities the element requires the host application to support. If the host application doesn't support at least one of the listed capabilities, the element is not rendered (or its fallback is rendered if provided). """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - separator: Optional[bool] = None """ Controls whether a separator line should be displayed above the element to visually separate it from the previous element. No separator will be displayed for the first element in a container, even if this property is set to true. """ - - height: Optional[ElementHeight] = None + height: Optional[ElementHeight] = "auto" """ The height of the element. When set to stretch, the element will use the remaining vertical space in its container. """ - horizontal_alignment: Optional[HorizontalAlignment] = None """ Controls how the element should be horizontally aligned. """ - - spacing: Optional[Spacing] = None + spacing: Optional[Spacing] = "Default" """ Controls the amount of space between this element and the previous one. No space will be added for the first element in a container. """ - target_width: Optional[TargetWidth] = None """ Controls for which card width the element should be displayed. If targetWidth isn't specified, the element is rendered at all card widths. Using targetWidth makes it possible to author responsive cards that adapt their layout to the available horizontal space. For more details, see [Responsive layout](topic:responsive-layout). """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - - name: Literal["graph.microsoft.com/event"] = "graph.microsoft.com/event" + name: Literal['graph.microsoft.com/event'] = 'graph.microsoft.com/event' """ Must be **graph.microsoft.com/event**. """ - properties: Optional[CalendarEventProperties] = None """ The properties of the event. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self - def with_requires(self, value: HostCapabilities) -> Self: + def with_requires(self, value: Union[HostCapabilities, Dict[str, Any]]) -> Self: self.requires = value return self @@ -7483,64 +7663,51 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class TextRun(CardElement): """A block of text inside a RichTextBlock element.""" - - type: Literal["TextRun"] = "TextRun" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['TextRun'] = 'TextRun' """ Must be **TextRun**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - text: Optional[str] = None """ The text to display. A subset of markdown is supported. """ - size: Optional[TextSize] = None """ The size of the text. """ - weight: Optional[TextWeight] = None """ The weight of the text. """ - color: Optional[TextColor] = None """ The color of the text. """ - is_subtle: Optional[bool] = None """ Controls whether the text should be renderer using a subtler variant of the select color. """ - font_type: Optional[FontType] = None """ The type of font to use for rendering. """ - italic: Optional[bool] = None """ Controls if the text should be italicized. """ - strikethrough: Optional[bool] = None """ Controls if the text should be struck through. """ - highlight: Optional[bool] = None """ Controls if the text should be highlighted. """ - underline: Optional[bool] = None """ Controls if the text should be underlined. """ - - select_action: Optional[Action] = None + select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the text is tapped or clicked. Action.ShowCard is not supported. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self @@ -7609,46 +7776,39 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - class IconRun(CardElement): """An inline icon inside a RichTextBlock element.""" - - type: Literal["IconRun"] = "IconRun" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['IconRun'] = 'IconRun' """ Must be **IconRun**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - name: Optional[str] = None """ The name of the inline icon to display. """ - - size: Optional[SizeEnum] = None + size: Optional[SizeEnum] = "Default" """ The size of the inline icon. """ - - style: Optional[IconStyle] = None + style: Optional[IconStyle] = "Regular" """ The style of the inline icon. """ - - color: Optional[TextColor] = None + color: Optional[TextColor] = "Default" """ The color of the inline icon. """ - - select_action: Optional[Action] = None + select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the inline icon is tapped or clicked. Action.ShowCard is not supported. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self @@ -7693,64 +7853,39 @@ def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self - -class ThemedUrl(SerializableObject): - """Defines a theme-specific URL.""" - - theme: Optional[ThemeName] = None - """ The theme this URL applies to. """ - - url: Optional[str] = None - """ The URL to use for the associated theme. """ - - def with_theme(self, value: ThemeName) -> Self: - self.theme = value - return self - - def with_url(self, value: str) -> Self: - self.url = value - return self - - class ImageRun(CardElement): """An inline image inside a RichTextBlock element.""" - - type: Literal["ImageRun"] = "ImageRun" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['ImageRun'] = 'ImageRun' """ Must be **ImageRun**. """ - id: Optional[str] = None """ A unique identifier for the element or action. Input elements must have an id, otherwise they will not be validated and their values will not be sent to the Bot. """ - lang: Optional[str] = None """ The locale associated with the element. """ - is_visible: Optional[bool] = True """ Controls the visibility of the element. """ - is_sort_key: Optional[bool] = None """ Controls whether the element should be used as a sort key by elements that allow sorting across a collection of elements. """ - url: Optional[str] = None """ The URL (or Base64-encoded Data URI) of the image. Acceptable formats are PNG, JPEG, GIF and SVG. """ - - size: Optional[SizeEnum] = None + size: Optional[SizeEnum] = "Default" """ The size of the inline image. """ - - style: Optional[ImageStyle] = None + style: Optional[ImageStyle] = "Default" """ The style of the inline image. """ - - select_action: Optional[Action] = None + select_action: Optional[SerializeAsAny[Action]] = None """ An Action that will be invoked when the image is tapped or clicked. Action.ShowCard is not supported. """ - themed_urls: Optional[List[ThemedUrl]] = None """ A set of theme-specific image URLs. """ - grid_area: Optional[str] = None """ The area of a Layout.AreaGrid layout in which an element should be displayed. """ - fallback: Optional[Union[SerializeAsAny[CardElement], FallbackElement]] = None """ An alternate element to render if the type of this one is unsupported or if the host application doesn't support all the capabilities specified in the requires property. """ + def with_key(self, value: str) -> Self: + self.key = value + return self + def with_id(self, value: str) -> Self: self.id = value return self @@ -7794,3 +7929,132 @@ def with_grid_area(self, value: str) -> Self: def with_fallback(self, value: Union[CardElement, FallbackElement]) -> Self: self.fallback = value return self + +class ImBackSubmitActionData(SerializableObject): + """Represents Teams-specific data in an Action.Submit to send an Instant Message back to the Bot.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['imBack'] = 'imBack' + """ Must be **imBack**. """ + value: Optional[str] = None + """ The value that will be sent to the Bot. """ + + def with_key(self, value: str) -> Self: + self.key = value + return self + + def with_value(self, value: str) -> Self: + self.value = value + return self + +class TabInfo(SerializableObject): + """Represents information about the iFrame content, rendered in the collab stage popout window.""" + name: Optional[str] = None + """ The name for the content. This will be displayed as the title of the window hosting the iFrame. """ + content_url: Optional[str] = None + """ The URL to open in an iFrame. """ + entity_id: Optional[str] = None + """ The unique entity id for this content (e.g., random UUID). """ + website_url: Optional[str] = None + """ An optional website URL to the content, allowing users to open this content in the browser (if they prefer). """ + + def with_name(self, value: str) -> Self: + self.name = value + return self + + def with_content_url(self, value: str) -> Self: + self.content_url = value + return self + + def with_entity_id(self, value: str) -> Self: + self.entity_id = value + return self + + def with_website_url(self, value: str) -> Self: + self.website_url = value + return self + +class CollabStageInvokeDataValue(SerializableObject): + """Data for invoking a collaboration stage action.""" + type: Literal['tab/tabInfoAction'] = 'tab/tabInfoAction' + """ Must be **tab/tabInfoAction**. """ + tab_info: Optional[TabInfo] = None + """ Provides information about the iFrame content, rendered in the collab stage popout window. """ + + def with_tab_info(self, value: TabInfo) -> Self: + self.tab_info = value + return self + +class InvokeSubmitActionData(SerializableObject): + """Represents Teams-specific data in an Action.Submit to make an Invoke request to the Bot.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['invoke'] = 'invoke' + """ Must be **invoke**. """ + value: Optional[Union[Dict[str, Any], CollabStageInvokeDataValue]] = None + """ The object to send to the Bot with the Invoke request. Can be strongly typed as one of the below values to trigger a specific action in Teams. """ + + def with_key(self, value: str) -> Self: + self.key = value + return self + + def with_value(self, value: Union[Dict[str, Any], CollabStageInvokeDataValue]) -> Self: + self.value = value + return self + +class MessageBackSubmitActionData(SerializableObject): + """Represents Teams-specific data in an Action.Submit to send a message back to the Bot.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['messageBack'] = 'messageBack' + """ Must be **messageBack**. """ + text: Optional[str] = None + """ The text that will be sent to the Bot. """ + display_text: Optional[str] = None + """ The optional text that will be displayed as a new message in the conversation, as if the end-user sent it. `displayText` is not sent to the Bot. """ + value: Optional[Dict[str, Any]] = None + """ Optional additional value that will be sent to the Bot. For instance, `value` can encode specific context for the action, such as unique identifiers or a JSON object. """ + + def with_key(self, value: str) -> Self: + self.key = value + return self + + def with_text(self, value: str) -> Self: + self.text = value + return self + + def with_display_text(self, value: str) -> Self: + self.display_text = value + return self + + def with_value(self, value: Dict[str, Any]) -> Self: + self.value = value + return self + +class SigninSubmitActionData(SerializableObject): + """Represents Teams-specific data in an Action.Submit to sign in a user.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['signin'] = 'signin' + """ Must be **signin**. """ + value: Optional[str] = None + """ The URL to redirect the end-user for signing in. """ + + def with_key(self, value: str) -> Self: + self.key = value + return self + + def with_value(self, value: str) -> Self: + self.value = value + return self + +class TaskFetchSubmitActionData(SerializableObject): + """Represents Teams-specific data in an Action.Submit to open a task module.""" + key: Optional[str] = None + """ Defines an optional key for the object. Keys are seldom needed, but in some scenarios, specifying keys can help maintain visual state in the host application. """ + type: Literal['task/fetch'] = 'task/fetch' + """ Must be **task/fetch**. """ + + def with_key(self, value: str) -> Self: + self.key = value + return self diff --git a/packages/cards/src/microsoft_teams/cards/utilities/__init__.py b/packages/cards/src/microsoft_teams/cards/utilities/__init__.py new file mode 100644 index 000000000..70bf547e8 --- /dev/null +++ b/packages/cards/src/microsoft_teams/cards/utilities/__init__.py @@ -0,0 +1,9 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from .open_dialog_data import OpenDialogData +from .submit_data import SubmitData + +__all__ = ["OpenDialogData", "SubmitData"] diff --git a/packages/cards/src/microsoft_teams/cards/utilities/open_dialog_data.py b/packages/cards/src/microsoft_teams/cards/utilities/open_dialog_data.py new file mode 100644 index 000000000..8287a8d74 --- /dev/null +++ b/packages/cards/src/microsoft_teams/cards/utilities/open_dialog_data.py @@ -0,0 +1,42 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Any, Dict + +from ..core import SubmitActionData, TaskFetchSubmitActionData + +RESERVED_KEYWORD = "dialog_id" + + +class OpenDialogData(SubmitActionData): + """ + Represents the data required to open a dialog in Microsoft Teams using a submit action. + + This class extends `SubmitActionData` and is used to construct the payload for opening a dialog, + including a reserved dialog identifier and any additional data. + + Example: + >>> data = OpenDialogData("myDialogId", {"foo": "bar"}) + >>> # Use `data` as the payload for a Teams card submit action to open a dialog. + + Args: + dialog_identifier (str): The unique identifier for the dialog to open. + extra_data (Dict[str, Any] | None): Optional additional data to include in the payload. + """ + + def __init__(self, dialog_identifier: str, extra_data: Dict[str, Any] | None = None): + """ + Initialize an OpenDialogData instance. + + Args: + dialog_identifier (str): The unique identifier for the dialog to open. + extra_data (Dict[str, Any] | None): Optional additional data to include in the payload. + """ + super().__init__() + self.with_msteams(TaskFetchSubmitActionData().model_dump()) + if extra_data: + for k, v in extra_data.items(): + setattr(self, k, v) + setattr(self, RESERVED_KEYWORD, dialog_identifier) diff --git a/packages/cards/src/microsoft_teams/cards/utilities/submit_data.py b/packages/cards/src/microsoft_teams/cards/utilities/submit_data.py new file mode 100644 index 000000000..3d2b6c526 --- /dev/null +++ b/packages/cards/src/microsoft_teams/cards/utilities/submit_data.py @@ -0,0 +1,46 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Any, Dict, Optional + +from ..core import SubmitActionData + +RESERVED_KEYWORD = "action" + + +class SubmitData(SubmitActionData): + """ + Utility class for creating submit data with action-based routing. + + This class extends the base ``SubmitActionData`` (``microsoft_teams.cards.core.SubmitActionData``) + with a convenience constructor that accepts an ``action`` identifier, which is used by + ``@app.on_dialog_submit("action")`` for routing. The base class is a plain data container + with no routing support. + + Args: + action: The action identifier that determines which handler processes the submission. + data: Optional additional data to include with the submission. + + Example: + >>> submit_data = SubmitData(action="submit_user_form", data={"user_id": "123"}) + >>> submit_action = SubmitAction(title="Submit").with_data(submit_data) + """ + + def __init__( + self, + action: Optional[str] = None, + data: Optional[Dict[str, Any]] = None, + **kwargs: Any, + ): + # If action is provided, use convenience constructor + if action is not None: + super().__init__(**kwargs) + if data: + for k, v in data.items(): + setattr(self, k, v) + setattr(self, RESERVED_KEYWORD, action) + else: + # Otherwise, use standard Pydantic initialization for model_validate + super().__init__(**kwargs) diff --git a/packages/cards/tests/test_core_serialization.py b/packages/cards/tests/test_core_serialization.py index 99c85f81c..821256d1e 100644 --- a/packages/cards/tests/test_core_serialization.py +++ b/packages/cards/tests/test_core_serialization.py @@ -13,12 +13,13 @@ ExecuteAction, QueryData, SubmitAction, - SubmitActionData, + SubmitData, TaskFetchSubmitActionData, TeamsCardProperties, TextBlock, ToggleInput, ) +from microsoft_teams.cards.core import SubmitActionData def test_adaptive_card_serialization(): @@ -30,9 +31,7 @@ def test_adaptive_card_serialization(): ToggleInput(label="Notify me", id="notify"), ActionSet( actions=[ - ExecuteAction(title="Submit") - .with_data(SubmitActionData(ms_teams={"action": "submit_basic"})) - .with_associated_inputs("auto") + ExecuteAction(title="Submit").with_data(SubmitData("submit_basic")).with_associated_inputs("auto") ] ), ], @@ -59,8 +58,8 @@ def test_action_set_serialization(): """Test ActionSet with multiple actions serializes correctly.""" action_set = ActionSet( actions=[ - ExecuteAction(title="Execute").with_data(SubmitActionData(ms_teams={"action": "execute"})), - SubmitAction(title="Submit").with_data(SubmitActionData(ms_teams={"action": "submit"})), + ExecuteAction(title="Execute").with_data(SubmitData("execute")), + SubmitAction(title="Submit").with_data(SubmitData("submit")), ] ) @@ -171,18 +170,18 @@ def test_single_object_fallback_serialization(): assert parsed["fallback"]["title"] == "Fallback Submit" -def test_ms_teams_serializes_to_msteams(): - """Test that ms_teams field serializes to 'msteams' instead of 'msTeams'.""" - # Test AdaptiveCard.ms_teams serialization +def test_msteams_serializes_to_msteams(): + """Test that msteams field serializes to 'msteams' instead of 'msTeams'.""" + # Test AdaptiveCard.msteams serialization card = AdaptiveCard(version="1.5", body=[]) - card.ms_teams = TeamsCardProperties(width="full") + card.msteams = TeamsCardProperties(width="full") json_str = card.model_dump_json(exclude_none=True, by_alias=True) parsed = json.loads(json_str) - # Verify ms_teams serializes to 'msteams' not 'msTeams' - assert "msteams" in parsed, "ms_teams should serialize to 'msteams'" - assert "msTeams" not in parsed, "ms_teams should not serialize to 'msTeams'" + # Verify msteams serializes to 'msteams' not 'msTeams' + assert "msteams" in parsed, "msteams should serialize to 'msteams'" + assert "msTeams" not in parsed, "msteams should not serialize to 'msTeams'" assert parsed["msteams"]["width"] == "full" # Test deserialization from 'msteams' @@ -193,15 +192,15 @@ def test_ms_teams_serializes_to_msteams(): "msteams": {"width": "full"}, } card = AdaptiveCard.model_validate(card_data) - assert card.ms_teams is not None - assert card.ms_teams.width == "full" + assert card.msteams is not None + assert card.msteams.width == "full" -def test_submit_action_data_ms_teams_serialization(): - """Test that SubmitActionData.ms_teams serializes to 'msteams' correctly.""" - # Create SubmitActionData with custom fields and ms_teams - action_data = SubmitActionData.model_validate({"opendialogtype": "simple_form"}) - action_data.ms_teams = TaskFetchSubmitActionData().model_dump() +def test_submit_action_data_msteams_serialization(): + """Test that SubmitData.msteams serializes to 'msteams' correctly.""" + # Create SubmitData with custom fields and msteams + action_data = SubmitData.model_validate({"opendialogtype": "simple_form"}) + action_data.msteams = TaskFetchSubmitActionData().model_dump() # Create a SubmitAction with the data action = SubmitAction(title="Test Action").with_data(action_data) @@ -212,16 +211,16 @@ def test_submit_action_data_ms_teams_serialization(): # Verify structure assert "data" in parsed - assert "msteams" in parsed["data"], "SubmitActionData.ms_teams should serialize to 'msteams'" - assert "msTeams" not in parsed["data"], "SubmitActionData.ms_teams should not serialize to 'msTeams'" + assert "msteams" in parsed["data"], "SubmitActionData.msteams should serialize to 'msteams'" + assert "msTeams" not in parsed["data"], "SubmitActionData.msteams should not serialize to 'msTeams'" assert parsed["data"]["msteams"]["type"] == "task/fetch" assert parsed["data"]["opendialogtype"] == "simple_form" # Test round-trip deserialization deserialized_action = SubmitAction.model_validate(parsed) assert isinstance(deserialized_action.data, SubmitActionData) - assert deserialized_action.data.ms_teams is not None - assert deserialized_action.data.ms_teams["type"] == "task/fetch" + assert deserialized_action.data.msteams is not None + assert deserialized_action.data.msteams["type"] == "task/fetch" def test_choices_data_serializes_to_choices_dot_data(): diff --git a/packages/cards/tests/test_im_back_action.py b/packages/cards/tests/test_im_back_action.py deleted file mode 100644 index 6f16056b4..000000000 --- a/packages/cards/tests/test_im_back_action.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -from microsoft_teams.cards import IMBackAction, SubmitActionData - - -def test_im_back_action_initialization(): - action = IMBackAction(value="Test Value") - assert isinstance(action.data, SubmitActionData) - assert action.data.ms_teams is not None - assert action.data.ms_teams["value"] == "Test Value" diff --git a/packages/cards/tests/test_invoke_action.py b/packages/cards/tests/test_invoke_action.py deleted file mode 100644 index b09e678b2..000000000 --- a/packages/cards/tests/test_invoke_action.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -from microsoft_teams.cards import InvokeAction, SubmitActionData - - -def test_invoke_action_initialization(): - action = InvokeAction({"test": "Test Value"}) - assert isinstance(action.data, SubmitActionData) - assert action.data.ms_teams is not None - assert action.data.ms_teams["value"]["test"] == "Test Value" diff --git a/packages/cards/tests/test_message_back_action.py b/packages/cards/tests/test_message_back_action.py deleted file mode 100644 index 321891c5c..000000000 --- a/packages/cards/tests/test_message_back_action.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -from microsoft_teams.cards import MessageBackAction, SubmitActionData - - -def test_message_back_action_initialization(): - action = MessageBackAction(text="Message Back Test", value="Test Value", display_text="Test Text") - assert isinstance(action.data, SubmitActionData) - assert action.data.ms_teams is not None - assert action.data.ms_teams["value"] == "Test Value" - assert action.data.ms_teams["text"] == "Message Back Test" - assert action.data.ms_teams["displayText"] == "Test Text" diff --git a/packages/cards/tests/test_sign_in_action.py b/packages/cards/tests/test_sign_in_action.py deleted file mode 100644 index 00d3d64fe..000000000 --- a/packages/cards/tests/test_sign_in_action.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -from microsoft_teams.cards import SignInAction, SubmitActionData - - -def test_sign_in_action_initialization(): - action = SignInAction(value="Test Value") - assert isinstance(action.data, SubmitActionData) - assert action.data.ms_teams is not None - assert action.data.ms_teams["value"] == "Test Value" diff --git a/packages/cards/tests/test_task_fetch_action.py b/packages/cards/tests/test_task_fetch_action.py deleted file mode 100644 index f5bada16f..000000000 --- a/packages/cards/tests/test_task_fetch_action.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the MIT License. -""" - -from microsoft_teams.cards import SubmitActionData, TaskFetchAction - - -def test_invoke_action_initialization(): - action = TaskFetchAction({"test": "Test Value"}) - assert isinstance(action.data, SubmitActionData) - assert action.data.ms_teams is not None - # ms_teams should contain the task/fetch type - assert action.data.ms_teams["type"] == "task/fetch" - # The actual data goes at the root of SubmitActionData - assert action.data.model_dump()["test"] == "Test Value" diff --git a/packages/devtools/src/microsoft_teams/devtools/routes/v3/conversations/activities/create.py b/packages/devtools/src/microsoft_teams/devtools/routes/v3/conversations/activities/create.py index 159c5d4b3..8f76193a5 100644 --- a/packages/devtools/src/microsoft_teams/devtools/routes/v3/conversations/activities/create.py +++ b/packages/devtools/src/microsoft_teams/devtools/routes/v3/conversations/activities/create.py @@ -38,8 +38,8 @@ async def create_activity_endpoint(request: Request, response: Response): id=body.get("id", str(uuid4())), service_url=f"http://localhost:{context.port}", channel_id="msteams", - from_=Account(id="devtools", name="devtools", role="bot"), - recipient=Account(id="", name="", role="user"), + from_=Account(id="devtools", name="devtools"), + recipient=Account(id="", name=""), conversation=ConversationAccount( id=request.path_params["conversationId"], conversation_type="personal", diff --git a/packages/mcpplugin/src/microsoft_teams/mcpplugin/transport.py b/packages/mcpplugin/src/microsoft_teams/mcpplugin/transport.py index 83ece8d84..a426d679a 100644 --- a/packages/mcpplugin/src/microsoft_teams/mcpplugin/transport.py +++ b/packages/mcpplugin/src/microsoft_teams/mcpplugin/transport.py @@ -7,9 +7,9 @@ from contextlib import asynccontextmanager from typing import Awaitable, Callable, Dict, Mapping, Optional, Union -import httpx from mcp.client.sse import sse_client from mcp.client.streamable_http import streamable_http_client +from mcp.shared._httpx_utils import create_mcp_http_client ValueOrFactory = Union[str, Callable[[], Union[str, Awaitable[str]]]] @@ -31,12 +31,8 @@ async def create_streamable_http_transport( else: resolved_headers[key] = str(value) - if resolved_headers: - async with httpx.AsyncClient(headers=resolved_headers) as http_client: - async with streamable_http_client(url, http_client=http_client) as (read_stream, write_stream, _): - yield read_stream, write_stream - else: - async with streamable_http_client(url) as (read_stream, write_stream, _): + async with create_mcp_http_client(headers=resolved_headers or None) as http_client: + async with streamable_http_client(url, http_client=http_client) as (read_stream, write_stream, _): yield read_stream, write_stream diff --git a/packages/mcpplugin/tests/test_transport.py b/packages/mcpplugin/tests/test_transport.py index dd71c743a..21959c399 100644 --- a/packages/mcpplugin/tests/test_transport.py +++ b/packages/mcpplugin/tests/test_transport.py @@ -39,10 +39,17 @@ def test_sse_passes_url_and_headers(self): class TestCreateStreamableHttpTransport: """Tests for create_streamable_http_transport.""" - async def test_no_headers_skips_httpx_async_client(self): - """Without headers, httpx.AsyncClient is never instantiated.""" + async def test_no_headers_calls_create_mcp_http_client(self): + """Without headers, create_mcp_http_client is called with None headers.""" read_stream = MagicMock() write_stream = MagicMock() + mock_http_client_instance = MagicMock() + captured: dict = {} + + @asynccontextmanager + async def mock_create_mcp_http_client(headers=None): + captured["headers"] = headers + yield mock_http_client_instance @asynccontextmanager async def mock_streamable_http_client(url, http_client=None): @@ -53,19 +60,28 @@ async def mock_streamable_http_client(url, http_client=None): "microsoft_teams.mcpplugin.transport.streamable_http_client", mock_streamable_http_client, ), - patch("microsoft_teams.mcpplugin.transport.httpx.AsyncClient") as mock_async_client, + patch( + "microsoft_teams.mcpplugin.transport.create_mcp_http_client", + mock_create_mcp_http_client, + ), ): async with create_streamable_http_transport("http://example.com"): pass - mock_async_client.assert_not_called() + assert captured["headers"] is None async def test_static_string_headers_creates_http_client(self): - """Static string headers are passed verbatim to httpx.AsyncClient.""" + """Static string headers are passed to create_mcp_http_client.""" read_stream = MagicMock() write_stream = MagicMock() mock_http_client_instance = MagicMock() + captured: dict = {} + + @asynccontextmanager + async def mock_create_mcp_http_client(headers=None): + captured["headers"] = headers + yield mock_http_client_instance @asynccontextmanager async def mock_streamable_http_client(url, http_client=None): @@ -76,17 +92,17 @@ async def mock_streamable_http_client(url, http_client=None): "microsoft_teams.mcpplugin.transport.streamable_http_client", mock_streamable_http_client, ), - patch("microsoft_teams.mcpplugin.transport.httpx.AsyncClient") as mock_async_client, + patch( + "microsoft_teams.mcpplugin.transport.create_mcp_http_client", + mock_create_mcp_http_client, + ), ): - mock_async_client.return_value.__aenter__ = AsyncMock(return_value=mock_http_client_instance) - mock_async_client.return_value.__aexit__ = AsyncMock(return_value=False) - headers = {"Authorization": "Bearer token", "X-Version": "1.0"} async with create_streamable_http_transport("http://example.com", headers) as (r, w): assert r is read_stream assert w is write_stream - mock_async_client.assert_called_once_with(headers={"Authorization": "Bearer token", "X-Version": "1.0"}) + assert captured["headers"] == {"Authorization": "Bearer token", "X-Version": "1.0"} async def test_sync_callable_header_is_resolved(self): """Sync callable header values are called and resolved to strings.""" @@ -98,11 +114,11 @@ def get_token(): with ( patch("microsoft_teams.mcpplugin.transport.streamable_http_client") as mock_streamable, - patch("microsoft_teams.mcpplugin.transport.httpx.AsyncClient") as mock_async_client, + patch("microsoft_teams.mcpplugin.transport.create_mcp_http_client") as mock_http_client, ): mock_http_instance = MagicMock() - mock_async_client.return_value.__aenter__ = AsyncMock(return_value=mock_http_instance) - mock_async_client.return_value.__aexit__ = AsyncMock(return_value=False) + mock_http_client.return_value.__aenter__ = AsyncMock(return_value=mock_http_instance) + mock_http_client.return_value.__aexit__ = AsyncMock(return_value=False) @asynccontextmanager async def fake_streamable(url, http_client=None): @@ -114,7 +130,7 @@ async def fake_streamable(url, http_client=None): async with create_streamable_http_transport("http://example.com", headers): pass - mock_async_client.assert_called_once_with(headers={"Authorization": "Bearer sync-token"}) + mock_http_client.assert_called_once_with(headers={"Authorization": "Bearer sync-token"}) async def test_async_callable_header_is_awaited_and_resolved(self): """Async callable header values are awaited and resolved to strings.""" @@ -126,11 +142,11 @@ async def get_async_token(): with ( patch("microsoft_teams.mcpplugin.transport.streamable_http_client") as mock_streamable, - patch("microsoft_teams.mcpplugin.transport.httpx.AsyncClient") as mock_async_client, + patch("microsoft_teams.mcpplugin.transport.create_mcp_http_client") as mock_http_client, ): mock_http_instance = MagicMock() - mock_async_client.return_value.__aenter__ = AsyncMock(return_value=mock_http_instance) - mock_async_client.return_value.__aexit__ = AsyncMock(return_value=False) + mock_http_client.return_value.__aenter__ = AsyncMock(return_value=mock_http_instance) + mock_http_client.return_value.__aexit__ = AsyncMock(return_value=False) @asynccontextmanager async def fake_streamable(url, http_client=None): @@ -142,7 +158,7 @@ async def fake_streamable(url, http_client=None): async with create_streamable_http_transport("http://example.com", headers): pass - mock_async_client.assert_called_once_with(headers={"Authorization": "Bearer async-token"}) + mock_http_client.assert_called_once_with(headers={"Authorization": "Bearer async-token"}) async def test_mixed_header_types_all_resolved(self): """Mix of static string, sync callable, and async callable headers are all resolved.""" @@ -157,11 +173,11 @@ async def async_fn(): with ( patch("microsoft_teams.mcpplugin.transport.streamable_http_client") as mock_streamable, - patch("microsoft_teams.mcpplugin.transport.httpx.AsyncClient") as mock_async_client, + patch("microsoft_teams.mcpplugin.transport.create_mcp_http_client") as mock_http_client, ): mock_http_instance = MagicMock() - mock_async_client.return_value.__aenter__ = AsyncMock(return_value=mock_http_instance) - mock_async_client.return_value.__aexit__ = AsyncMock(return_value=False) + mock_http_client.return_value.__aenter__ = AsyncMock(return_value=mock_http_instance) + mock_http_client.return_value.__aexit__ = AsyncMock(return_value=False) @asynccontextmanager async def fake_streamable(url, http_client=None): @@ -177,7 +193,7 @@ async def fake_streamable(url, http_client=None): async with create_streamable_http_transport("http://example.com", headers): pass - mock_async_client.assert_called_once_with( + mock_http_client.assert_called_once_with( headers={ "X-Static": "static-value", "X-Sync": "sync-value", @@ -192,11 +208,11 @@ async def test_non_string_header_values_are_coerced_to_string(self): with ( patch("microsoft_teams.mcpplugin.transport.streamable_http_client") as mock_streamable, - patch("microsoft_teams.mcpplugin.transport.httpx.AsyncClient") as mock_async_client, + patch("microsoft_teams.mcpplugin.transport.create_mcp_http_client") as mock_http_client, ): mock_http_instance = MagicMock() - mock_async_client.return_value.__aenter__ = AsyncMock(return_value=mock_http_instance) - mock_async_client.return_value.__aexit__ = AsyncMock(return_value=False) + mock_http_client.return_value.__aenter__ = AsyncMock(return_value=mock_http_instance) + mock_http_client.return_value.__aexit__ = AsyncMock(return_value=False) @asynccontextmanager async def fake_streamable(url, http_client=None): @@ -208,7 +224,7 @@ async def fake_streamable(url, http_client=None): async with create_streamable_http_transport("http://example.com", headers): pass - mock_async_client.assert_called_once_with(headers={"X-Port": "8080"}) + mock_http_client.assert_called_once_with(headers={"X-Port": "8080"}) async def test_yields_read_and_write_streams(self): """The context manager correctly yields (read_stream, write_stream) to callers."""