Skip to content
Open
  •  
  •  
  •  
47 changes: 25 additions & 22 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Overview

This repository contains the Python SDK for Azure Connectors. Code must follow Python best practices (PEP 8, PEP 484) and the team's conventions for async/await patterns, type safety, and API design.
This repository contains the Python SDK for Azure Connectors. Code must follow Python best practices (PEP 8, PEP 484) and the team's conventions for synchronous and asynchronous APIs, type safety, and API design.

## Client Refresh and Review

Expand All @@ -13,6 +13,9 @@ This repository contains the Python SDK for Azure Connectors. Code must follow P
- Capture known response data directly in semantic variables, without redundant `result` aliases or duplicated conditions. Check the actual returned shape after regeneration; an item list is not a response dictionary.
- Preserve nullable-response guards such as `response and response.get(...)`; they are not duplicate conditions. Test non-pageable consumers with `None`, empty dictionaries, and populated responses separately. Do not simplify conditions by matching text prefixes.
- Execute changed samples with empty and nonempty mocked results and assert their output, including absence of caught-and-printed errors. Pagination tests must exercise continuation requests, later-page items, and termination, including an empty initial page with a next link.
- Generate clients as atomic pairs: synchronous clients and shared declarations in `azure.connectors.<connector>`, and asynchronous clients in `azure.connectors.aio.<connector>`.
- Keep operation names and signatures aligned across modes without `_async` suffixes. Pageable operations return `ItemPaged` or `AsyncItemPaged`; long-running operation support remains deferred.
- Import generated clients from their connector modules. Do not add generated-client barrel exports to package `__init__.py` files.

## Quick Reference: Coding Style Rules

Expand Down Expand Up @@ -397,31 +400,30 @@ When adding a new connector client to the SDK, use the **add-connector** skill (

### Steps

1. **Create the connector module** in `src/azure/connectors/`:
- File name should match the connector API name (e.g., `office365.py`, `sharepointonline.py`)
1. **Generate the connector pair**:
- Root: `src/azure/connectors/<connector>.py`
- Async: `src/azure/connectors/aio/<connector>.py`

2. **Implement the client class** following existing patterns:
```python
from azure.connectors.sdk import ConnectorClient

class Office365Client(ConnectorClient):
"""Client for Office 365 connector operations."""

async def send_email(self, ...) -> EmailResponse:
...
```
2. **Verify matching client classes** following existing patterns:

3. **Export from `__init__.py`** — add the new client to `src/azure/connectors/__init__.py`
```python
from azure.connectors.office365 import Office365Client, SendEmailInput

from azure.connectors.aio.office365 import Office365Client as AsyncOffice365Client
```

3. **Keep package initializers minimal** — do not add generated-client barrel exports

4. **Update `README.md`** — add the connector to the "Validated Connectors" table with status (`✅ E2E Validated` or `🔄 SDK Generated`) and test count

5. **Add unit tests** — create `test_<connector>.py` in `tests/` following the pattern of existing tests:
- Constructor tests
- Method tests with mocked responses
- Error handling tests
- Type serialization round-trips
5. **Add unit tests** — create `test_<connector>.py` in `tests/` following existing paired tests:
- Sync and aio constructors and lifecycle
- Matching operation names and signatures
- Mocked success and error behavior in both modes
- Shared model identity and serialization round-trips
- `ItemPaged` and `AsyncItemPaged` behavior when applicable

6. **Create sample usage file** — create `sample_connector_usage_<connector>.py` in `samples/sample_connector_usage/` following the pattern of existing samples (port from .NET if available)
6. **Create paired samples** — add the sync file under `samples/sample_connector_usage/` and the aio file under `samples/sample_connector_usage/aio/`

7. **Update `samples/sample_connector_usage/README.md`** — add the new sample to the samples table

Expand All @@ -440,9 +442,10 @@ When adding a new connector client to the SDK, use the **add-connector** skill (
- [ ] Docstrings present on class and all public methods
- [ ] Existing connector tests still pass (no regressions)
- [ ] New connector tests cover: constructor, mocked success, mocked error, exception handling
- [ ] Module exported in `__init__.py`
- [ ] Root and aio modules expose matching unsuffixed operations
- [ ] Models and trigger metadata are root-owned and shared with aio
- [ ] README.md connector table updated
- [ ] Sample file created and compiles without errors
- [ ] Sync and aio sample files compile without errors
- [ ] Samples README updated
- [ ] CHANGELOG.md updated
- [ ] Connection setup skill updated
77 changes: 44 additions & 33 deletions .github/skills/add-connector/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: add-connector
description: 'Add a new connector to the Azure Connectors Python SDK. USE WHEN: adding a new connector client, scaffolding connector files, creating unit tests, updating __init__.py exports, updating README status table, creating sample usage files. Covers the full workflow from connector file creation through test validation. NOT FOR: modifying existing connectors, connection setup, or trigger registration.'
description: 'Add a new connector to the Azure Connectors Python SDK. USE WHEN: adding a generated root/aio connector pair, creating paired unit tests and samples, or updating connector documentation. Covers the full workflow from generation through validation. NOT FOR: modifying existing connectors, connection setup, or trigger registration.'
argument-hint: 'Provide the connector name (e.g., "dynamics365", "servicenow")'
---

Expand All @@ -17,8 +17,8 @@ Automates the complete workflow for adding a new connector to the Azure Connecto

## Prerequisites

- The connector module file (e.g., `dynamics365.py`) is provided or needs to be created
- The connector follows the `ConnectorClientBase` pattern
- The connector can be generated as root and aio modules from one contract
- Both clients follow the matching `ConnectorClientBase` runtime patterns
- A .NET sample exists to port to Python (optional)

## Procedure
Expand All @@ -39,9 +39,12 @@ Collect the following details from the user:

### Step 2: Create/Verify Connector Module

Ensure the connector file exists at `src/azure/connectors/{connector_name}.py`.
Generate both files from one CodefulSdkGenerator invocation:

The connector must follow this structure:
- `src/azure/connectors/{connector_name}.py`
- `src/azure/connectors/aio/{connector_name}.py`

The root connector owns shared declarations and the synchronous client:

```python
# Copyright (c) Microsoft Corporation. All rights reserved.
Expand All @@ -56,9 +59,8 @@ from typing import Optional, Any, Dict, List
from urllib.parse import quote
import json

from azure.core.credentials import AzureKeyCredential
from azure.core.credentials_async import AsyncTokenCredential
from azure.core.pipeline.transport import AsyncHttpTransport
from azure.core.credentials import AzureKeyCredential, TokenCredential
from azure.core.pipeline.transport import HttpTransport

from azure.connectors.sdk import ConnectorClientBase, ConnectorException

Expand All @@ -74,21 +76,18 @@ class {ClientClassName}(ConnectorClientBase):
def connector_name(self) -> str:
return "{connector_name}"

# ... async methods ...
# ... synchronous unsuffixed methods ...
```

### Step 3: Update `__init__.py` Exports

Add the connector import to `src/azure/connectors/__init__.py`:
The aio module imports and re-exports root models, aliases, enums, and
`TRIGGER_OPERATIONS`. Its client uses `AsyncTokenCredential`,
`AsyncHttpTransport`, and the same unsuffixed operations. Pageable operations
return `ItemPaged` from root and `AsyncItemPaged` from aio.

```python
try:
from .{connector_name} import {ClientClassName}
except (ImportError, NameError):
{ClientClassName} = None # type: ignore[assignment,misc]
```
### Step 3: Keep Package Initializers Minimal

Also add to the `__all__` list if present.
Do not add generated clients to `azure.connectors` or `azure.connectors.aio`
package exports. Consumers import clients from each connector module.

### Step 4: Update README Connector Table

Expand All @@ -113,9 +112,9 @@ from unittest.mock import AsyncMock, patch

import pytest
from azure.core.credentials import AzureKeyCredential
from azure.connectors.aio.{connector_name} import {ClientClassName}
from azure.connectors.{connector_name} import (
{ClientClassName},
# Import all public dataclasses from the connector
{RequestModelName},
)
from azure.connectors.sdk import ConnectorException
from tests.conftest import MockResponse
Expand Down Expand Up @@ -216,7 +215,7 @@ class Test{ClientClassName}Lifecycle:
# Follow the pattern:
#
# class Test{MethodName}:
# """Tests for {method_name}_async method."""
# """Tests for {method_name} on the aio client."""
#
# @pytest.mark.asyncio
# async def test_success(self, mock_credential):
Expand All @@ -230,16 +229,26 @@ class Test{ClientClassName}Lifecycle:
```

**Required test categories:**

1. **Initialization tests** — constructor, URL handling, options, error cases
2. **Lifecycle tests** — close(), context manager
3. **Method tests** — one class per public async method with:
- Success case with mocked response
- Parameter handling tests
- Error response handling
3. **Method tests** — exercise matching root and aio operations with:
- Success cases with synchronous and asynchronous mocked responses
- Matching parameter and request-control signatures
- Error response handling in both modes
- Shared declaration identity
- Paging behavior for `ItemPaged` and `AsyncItemPaged`, when applicable

### Step 6: Create Sample Usage File

Create `samples/sample_connector_usage/sample_connector_usage_{connector_name}.py`:
Create both sample files:

- `samples/sample_connector_usage/sample_connector_usage_{connector_name}.py`
- `samples/sample_connector_usage/aio/sample_connector_usage_{connector_name}.py`

The following template shows the aio variant. The root variant uses
`azure.identity.DefaultAzureCredential`, imports its client from the root
connector module, uses `with`, and calls the same operations without `await`.

```python
# Copyright (c) Microsoft Corporation. All rights reserved.
Expand Down Expand Up @@ -268,9 +277,9 @@ import asyncio
import os
from azure.identity.aio import DefaultAzureCredential
from azure.connectors import ConnectorException
from azure.connectors.aio.{connector_name} import {ClientClassName}
from azure.connectors.{connector_name} import (
{ClientClassName},
# Import relevant dataclasses for the examples
{RequestModelName},
)

# Connection runtime URL format:
Expand All @@ -290,7 +299,7 @@ async def example_1_basic_operation():
async with {ClientClassName}(CONNECTION_RUNTIME_URL, credential) as client:
try:
# Call a basic read operation
result = await client.{example_method}_async()
result = await client.{example_method}()

print(f"Result: {{result}}")

Expand All @@ -309,7 +318,7 @@ async def example_2_advanced_operation():
async with {ClientClassName}(CONNECTION_RUNTIME_URL, credential) as client:
try:
# Call a write or complex operation
# result = await client.{another_method}_async(...)
# result = await client.{another_method}(...)
print("Advanced operation completed")

except ConnectorException as ex:
Expand Down Expand Up @@ -338,10 +347,11 @@ if __name__ == "__main__":
```

**When porting from .NET:**

1. Find the corresponding .NET sample
2. Translate each example method to Python async/await syntax
2. Create matching synchronous and asynchronous examples
3. Convert C# types to Python dataclasses
4. Use `async with` for client lifecycle
4. Use `with` for root and `async with` for aio client lifecycle
5. Handle exceptions with `ConnectorException`

### Step 7: Update Samples README
Expand Down Expand Up @@ -392,6 +402,7 @@ Execute the full test suite to validate:
```

**Required checks:**

- [ ] All tests pass
- [ ] No import errors
- [ ] Code quality tests pass (imports, structure)
Expand Down
41 changes: 25 additions & 16 deletions .github/skills/connection-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,21 +178,30 @@ az rest --method GET --uri "$runtimeUrl/v2/datasets" --resource "https://apihub.

## Next Steps

- **SDK usage:** Use the connection runtime URL with a caller-owned async Azure credential:
```python
from azure.identity.aio import DefaultAzureCredential

from azure.connectors.office365 import Office365Client, SendEmailInput

async with DefaultAzureCredential() as credential:
async with Office365Client(runtime_url, credential) as client:
await client.send_email_async(
input=SendEmailInput(
to="recipient@example.com",
subject="Hello",
body="Sent with Azure Connectors.",
)
)
```
### SDK Usage

Use the connection runtime URL with a caller-owned Azure credential. Root
clients are synchronous:

```python
from azure.identity import DefaultAzureCredential

from azure.connectors.office365 import Office365Client, SendEmailInput

with DefaultAzureCredential() as credential:
with Office365Client(runtime_url, credential) as client:
client.send_email(
input=SendEmailInput(
to="recipient@example.com",
subject="Hello",
body="Sent with Azure Connectors.",
)
)
```

Asynchronous applications import the client from
`azure.connectors.aio.office365`, use `azure.identity.aio`, and await the same
unsuffixed `send_email` operation. Models remain in the root module.

- **Azure Functions triggers:** To register connector triggers (e.g., OnNewEmail, OnNewFile) for Azure Functions, use the [trigger-registration skill](../trigger-registration/SKILL.md).
- **Azure Functions signatures:** For a complete mapping of trigger operations to Azure Functions signatures, see [Operations to Functions Signature Match](https://github.com/Azure/azure-functions-connector-extension/blob/main/docs/operations-functions-match.md).
14 changes: 8 additions & 6 deletions .github/skills/trigger-registration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,20 +295,21 @@ az functionapp log tail -g $resourceGroup -n $functionAppName

## Using the SDK for Actions (Beyond Triggers)

The `azure-connectors` package provides typed async clients for calling connector actions directly from Python applications. Generated clients require a caller-owned asynchronous Azure Identity credential or an `AzureKeyCredential`.
The `azure-connectors` package provides matching synchronous and asynchronous clients for calling connector actions directly from Python applications. Root clients use synchronous Azure Identity credentials; aio clients use asynchronous credentials. Both modes also accept `AzureKeyCredential`.

### Example: Send an email

```python
from azure.identity.aio import DefaultAzureCredential

from azure.connectors.office365 import Office365Client, SendEmailInput
from azure.connectors.aio.office365 import Office365Client
from azure.connectors.office365 import SendEmailInput


async def main(connection_url: str):
async with DefaultAzureCredential() as credential:
async with Office365Client(connection_url, credential) as client:
await client.send_email_async(
await client.send_email(
input=SendEmailInput(
to="recipient@example.com",
subject="Hello from Python SDK",
Expand All @@ -322,15 +323,15 @@ async def main(connection_url: str):
```python
from azure.identity.aio import DefaultAzureCredential

from azure.connectors.sharepointonline import SharepointonlineClient
from azure.connectors.aio.sharepointonline import SharepointonlineClient


async def list_items(connection_url: str):
async with DefaultAzureCredential() as credential:
async with SharepointonlineClient(connection_url, credential) as client:
items = [
item
async for item in client.get_items_async(
async for item in client.get_items(
dataset="https://contoso.sharepoint.com/sites/MySite",
table="MyList",
)
Expand All @@ -342,7 +343,8 @@ async def list_items(connection_url: str):

### Authentication Options

Use `DefaultAzureCredential` or `ManagedIdentityCredential` from `azure.identity.aio` for token authentication. Use `AzureKeyCredential` from `azure.core.credentials` when the connection runtime expects an API key. The caller owns asynchronous credentials and must close them.
Use credentials from `azure.identity` for root clients and `azure.identity.aio` for aio clients. Use `AzureKeyCredential` from `azure.core.credentials` when the connection runtime expects an API key. The caller owns credentials and must close them through the matching lifecycle.

### E2E Validated Connectors

| Connector | Package | Status |
Expand Down
Loading
Loading