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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
76 changes: 38 additions & 38 deletions .github/skills/add-connector/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,11 @@ from typing import Optional, Any, Dict, List
from urllib.parse import quote
import json

from azure.connectors.sdk import (
ConnectorClientBase,
ConnectorClientOptions,
TokenProvider,
ManagedIdentityTokenProvider,
ConnectorException,
)
from azure.core.credentials import AzureKeyCredential
from azure.core.credentials_async import AsyncTokenCredential
from azure.core.pipeline.transport import AsyncHttpTransport

from azure.connectors.sdk import ConnectorClientBase, ConnectorException


# Type Definitions
Expand Down Expand Up @@ -111,17 +109,15 @@ Create `tests/test_{connector_name}.py` following this template:

"""Unit tests for {ClientClassName}."""

import pytest
from unittest.mock import AsyncMock, patch

import pytest
from azure.core.credentials import AzureKeyCredential
from azure.connectors.{connector_name} import (
{ClientClassName},
# Import all public dataclasses from the connector
)
from azure.connectors.sdk import (
ConnectorClientOptions,
ManagedIdentityTokenProvider,
ConnectorException,
)
from azure.connectors.sdk import ConnectorException
from tests.conftest import MockResponse


Expand All @@ -130,55 +126,59 @@ class Test{ClientClassName}Initialization:

def test_init_with_valid_url_and_defaults(self):
"""Test initialization with valid URL and default parameters."""
client = {ClientClassName}("https://example.azure.com/connections/test")
client = {ClientClassName}(
"https://example.azure.com/connections/test",
AzureKeyCredential("test-key"),
)

assert client._connection_runtime_url == "https://example.azure.com/connections/test"
assert client.connector_name == "{connector_name}"
assert isinstance(client._http_client._token_provider, ManagedIdentityTokenProvider)
assert isinstance(client._http_client._credential, AzureKeyCredential)

def test_init_with_trailing_slash(self):
"""Test that trailing slash is removed from URL."""
client = {ClientClassName}("https://example.azure.com/connections/test/")
client = {ClientClassName}(
"https://example.azure.com/connections/test/",
AzureKeyCredential("test-key"),
)

assert client._connection_runtime_url == "https://example.azure.com/connections/test"

def test_init_with_custom_token_provider(self, mock_token_provider):
"""Test initialization with custom token provider."""
def test_init_with_custom_credential(self, mock_credential):
"""Test initialization with custom Azure Core credential."""
client = {ClientClassName}(
"https://example.azure.com/connections/test",
token_provider=mock_token_provider
credential=mock_credential
)

assert client._http_client._token_provider is mock_token_provider
assert client._http_client._credential is mock_credential

def test_init_with_custom_options(self, mock_token_provider):
"""Test initialization with custom options."""
options = ConnectorClientOptions(timeout_seconds=60.0, max_retry_attempts=5)
def test_init_with_custom_settings(self, mock_credential):
"""Test initialization with custom pipeline settings."""
client = {ClientClassName}(
"https://example.azure.com/connections/test",
token_provider=mock_token_provider,
options=options
credential=mock_credential,
timeout_seconds=60.0,
max_retry_attempts=5,
)

assert client._options is options
assert client._options.timeout_seconds == 60.0
assert client._options.max_retry_attempts == 5
assert client._http_client._timeout_seconds == 60.0

def test_init_with_empty_url_raises_error(self):
"""Test that empty URL raises ValueError."""
with pytest.raises(ValueError, match="connection_runtime_url cannot be None or empty"):
{ClientClassName}("")
{ClientClassName}("", AzureKeyCredential("test-key"))

def test_init_with_none_url_raises_error(self):
"""Test that None URL raises ValueError."""
with pytest.raises(ValueError, match="connection_runtime_url cannot be None or empty"):
{ClientClassName}(None)
{ClientClassName}(None, AzureKeyCredential("test-key"))

def test_connector_name_property(self, mock_token_provider):
def test_connector_name_property(self, mock_credential):
"""Test connector_name property returns '{connector_name}'."""
client = {ClientClassName}(
"https://example.azure.com/connections/test",
token_provider=mock_token_provider
credential=mock_credential
)

assert client.connector_name == "{connector_name}"
Expand All @@ -188,24 +188,24 @@ class Test{ClientClassName}Lifecycle:
"""Tests for {ClientClassName} lifecycle methods."""

@pytest.mark.asyncio
async def test_close(self, mock_token_provider):
async def test_close(self, mock_credential):
"""Test close method calls http_client.close."""
client = {ClientClassName}(
"https://example.azure.com/connections/test",
token_provider=mock_token_provider
credential=mock_credential
)

with patch.object(client._http_client, 'close', new_callable=AsyncMock) as mock_close:
await client.close()
mock_close.assert_called_once()

@pytest.mark.asyncio
async def test_context_manager(self, mock_token_provider):
async def test_context_manager(self, mock_credential):
"""Test async context manager functionality."""
with patch.object({ClientClassName}, 'close', new_callable=AsyncMock) as mock_close:
async with {ClientClassName}(
"https://example.azure.com/connections/test",
token_provider=mock_token_provider
credential=mock_credential
) as client:
assert isinstance(client, {ClientClassName})

Expand All @@ -219,12 +219,12 @@ class Test{ClientClassName}Lifecycle:
# """Tests for {method_name}_async method."""
#
# @pytest.mark.asyncio
# async def test_success(self, mock_token_provider):
# async def test_success(self, mock_credential):
# """Test successful request."""
# ...
#
# @pytest.mark.asyncio
# async def test_error_response_raises_exception(self, mock_token_provider):
# async def test_error_response_raises_exception(self, mock_credential):
# """Test that error response raises ConnectorException."""
# ...
```
Expand Down
20 changes: 14 additions & 6 deletions .github/skills/connection-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,21 @@ az rest --method GET --uri "$runtimeUrl/v2/datasets" --resource "https://apihub.

## Next Steps

- **SDK usage:** Use the connection runtime URL with the SDK's typed async clients:
- **SDK usage:** Use the connection runtime URL with a caller-owned async Azure credential:
```python
from azure.connectors.office365 import Office365Client
from azure.connectors.sdk import ManagedIdentityTokenProvider

async with Office365Client(runtime_url, ManagedIdentityTokenProvider()) as client:
await client.send_email_async(to="...", subject="...", body="...")
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.",
)
)
```
- **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).
80 changes: 30 additions & 50 deletions .github/skills/trigger-registration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,74 +295,54 @@ 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 any Python application — Azure Functions, Flask, FastAPI, Django, scripts, etc.
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`.

### Example: Send an email

```python
import asyncio
from azure.connectors.office365 import Office365Client
from azure.connectors.sdk import ManagedIdentityTokenProvider

async def main():
# Connection runtime URL from Connector Namespace
connection_url = "https://..."
token_provider = ManagedIdentityTokenProvider()

async with Office365Client(connection_url, token_provider) as client:
await client.send_email_async(
to="recipient@example.com",
subject="Hello from Python SDK",
body="<p>Sent from any Python app!</p>",
)
from azure.identity.aio import DefaultAzureCredential

from azure.connectors.office365 import Office365Client, SendEmailInput

asyncio.run(main())

async def main(connection_url: str):
async with DefaultAzureCredential() as credential:
async with Office365Client(connection_url, credential) as client:
await client.send_email_async(
input=SendEmailInput(
to="recipient@example.com",
subject="Hello from Python SDK",
body="<p>Sent from any Python app.</p>",
)
)
```

### Example: List SharePoint items

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

from azure.connectors.sharepointonline import SharepointonlineClient

async def list_items():
async with SharepointonlineClient(connection_url, token_provider) as client:
items = await client.get_items_async(
dataset="https://contoso.sharepoint.com/sites/MySite",
table="MyList"
)
for item in items.get("value", []):
print(f"Item: {item.get('Title')}")
```

### Example: List Teams
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(
dataset="https://contoso.sharepoint.com/sites/MySite",
table="MyList",
)
]

```python
from azure.connectors.teams import TeamsClient

async def list_teams():
async with TeamsClient(connection_url, token_provider) as client:
teams = await client.get_all_teams_async()
for team in teams.get("value", []):
print(f"Team: {team.get('displayName')}")
for item in items:
print(f"Item: {item.get('Title')}")
```

### Authentication Options

```python
from azure.connectors.sdk import ManagedIdentityTokenProvider, ConnectionStringTokenProvider

# System-assigned managed identity (recommended for Azure)
token_provider = ManagedIdentityTokenProvider()

# User-assigned managed identity
token_provider = ManagedIdentityTokenProvider(client_id="your-client-id")

# Azure Identity credentials (DefaultAzureCredential, AzureCliCredential, etc.)
from azure.identity.aio import DefaultAzureCredential
credential = DefaultAzureCredential()
client = Office365Client(connection_url, credential)
```

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.
### E2E Validated Connectors

| Connector | Package | Status |
Expand Down
35 changes: 27 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,44 +14,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Breaking Changes

- **Generated clients now use the asynchronous Azure Core pipeline** — every client requires a caller-owned `AsyncTokenCredential` or `AzureKeyCredential`. The legacy token-provider classes and `ConnectorClientOptions` have been removed; retry, timeout, transport, and policy settings are passed directly to the client. Operations also expose keyword-only `timeout`, `headers`, `client_request_id`, and `response_hook` controls.
- **Automatic retries are now method-safe by default** — `GET`, `HEAD`, `OPTIONS`, and `TRACE` retain configured retries. `POST`, `PUT`, `PATCH`, `DELETE`, and unknown methods make one attempt for transient responses and transport errors. Set `retry_unsafe_http_methods=True` directly on a generated client only when replay is acceptable; retry count, timeout, and backoff settings are also direct client keyword arguments. ([#85](https://github.com/Azure/connectors-python-sdk/issues/85))
- Jira `list_issues_async` now exposes the live optional `fields` query parameter
instead of sending an internal `fields=*all` default. Pass `fields` and
`next_page_token` by keyword; `fields` precedes the token in the generated signature.

- This regeneration propagates the existing async-iterator paging contract to
32 additional connectors, extending [Fix Dataverse list pagination (#84)](https://github.com/Azure/connectors-python-sdk/pull/84).
- This regeneration propagates the async-iterator paging contract to 32 additional
connectors, extending [Fix Dataverse list pagination (#84)](https://github.com/Azure/connectors-python-sdk/pull/84).
Pageable operations return items rather than response dictionaries. Consume them
with `async for`, or collect them with
`[item async for item in client.get_items_async(...)]`; samples use this contract.

- **Automatic retries are now method-safe by default** — `GET`, `HEAD`, `OPTIONS`, and `TRACE` retain configured retries. `POST`, `PUT`, `PATCH`, `DELETE`, and unknown methods make one attempt for transient responses and `aiohttp.ClientError`. Set `ConnectorClientOptions(retry_unsafe_http_methods=True)` per client only when replay is acceptable; existing retry count, timeout, and backoff settings remain available. ([#85](https://github.com/Azure/connectors-python-sdk/issues/85))

- Seismic Planner `CustomPropertyValues.localizations` now exposes `Dict[str, CustomPropertyDataDisplay]` instead of `Dict[str, Any]`. Callers can access typed values directly, such as `localizations["en-US"].name`. ([Azure/Connectors-NET-SDK#262](https://github.com/Azure/Connectors-NET-SDK/issues/262), AzureUX-BPM PR 17131877)

- Microsoft Dataverse `get_items_async` now returns an `AsyncIterator[dict[str, Any]]`. Callers must use `async for` instead of awaiting a single response dictionary; iteration preserves and follows `@odata.nextLink` until all rows are returned. ([Azure/Connectors-NET-SDK#208](https://github.com/Azure/Connectors-NET-SDK/issues/208), AzureUX-BPM PR 17086991)

- Regenerated all 98 supported connector clients from the current CodefulSdkGenerator and managed connector contracts; 62 clients contain public contract changes.
- Regenerated all 99 supported connector clients from current live managed connector metadata using the Azure Core contract generator.
- Corrected generated parameter types across connector operations, including pagination values, numeric identifiers, and Boolean options. Callers must now pass the annotated `int` and `bool` values instead of string representations.
- Added current discovery and schema operations for Azure Automation, Azure Blob Storage, Azure Data Factory, Azure VM, Excel Online (Business), Microsoft Teams, and Microsoft Defender for Endpoint.

### Changed

- Made `azure-identity` a required runtime dependency so Azure Identity credentials and the shipped samples work with the default package installation.
- Updated all connector samples to match generated method signatures and added literal-type validation to the sample contract checks.
- Added focused success, route, request-body, response, and error coverage for newly generated operations.
- Clarified that Azure Functions SDK-type deserialization is owned by the Azure Functions connector extension rather than generated connector clients.

### Fixed

- **Request timeout enforcement**: `timeout_seconds` and per-operation `timeout`
now bound the complete network operation, including retries, response loading,
and body reads across Azure Core async transports. Zero and negative values
disable SDK and transport deadlines.
- **Async transport compatibility**: Empty buffered responses now produce normal
snapshots and connector errors without re-reading consumed streams. Injected
transports receive only supported timeout options and close through their
asynchronous lifecycle.
- **Explicit Azure Core policy chains**: `per_call_policies` and
`per_retry_policies` now compose with an explicit `policies` chain instead of
being silently discarded. Per-retry extensions require an `AsyncRetryPolicy`,
while chains without retries no longer forward retry-only options to the
transport.
- **Per-operation request controls**: Explicit headers and client request IDs now
override client defaults without allowing default authorization or content-type
headers to replace SDK-owned values.
- **Unsafe retry opt-in**: `retry_unsafe_http_methods=True` now includes custom
extension methods as well as standard unsafe HTTP verbs.
- **Connector samples**: Samples now close caller-owned Azure credentials and use
async iteration for pageable operations.
- Generated caller-supplied and internal-default query values escape reserved
characters, including `/`, so paths, URLs, and collection values remain within
their intended query parameters.

- **Microsoft Dataverse**: Corrected `create_attachment_async()` to accept GUID/string row IDs instead of integers.

## [0.5.0b1] - 2026-08-31

### Breaking Changes

- **Generated clients now use the asynchronous Azure Core pipeline** — every client requires a caller-owned `AsyncTokenCredential` or `AzureKeyCredential`. The legacy token-provider classes and `ConnectorClientOptions` have been removed; retry, timeout, transport, and policy settings are passed directly to the client. Operations also expose keyword-only `timeout`, `headers`, `client_request_id`, and `response_hook` controls.
- Regenerated Microsoft To Do request models now use the current contract names `CreateToDoList`, `CreateToDo`, and `UpdateToDo` instead of the deprecated `V2`-suffixed names. Its polling triggers are now available through `TRIGGER_OPERATIONS`, not callable client methods.
- Generated Python operation names now preserve Swagger `operationId` spelling by default while grouping acronym runs in snake_case. Connector-scoped corrections fix malformed Google Tasks and PDF.co operation names. This renames methods in DocuSign, GitHub, Google Tasks, PDF.co, Salesforce, SigningHub, Slack, Word Online (Business), and Zoho Sign.
- Regenerated Google Tasks and Slack trigger routes are available through `TRIGGER_OPERATIONS`, not callable client methods. Slack also no longer exposes the deprecated `create_group_async` operation.
Expand Down Expand Up @@ -278,4 +298,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[0.2.0b1]: https://github.com/Azure/Connectors-Python-SDK/compare/v0.1.0dev2...v0.2.0b1
[0.1.0dev2]: https://github.com/Azure/Connectors-Python-SDK/compare/v0.1.0dev1...v0.1.0dev2
[0.1.0dev1]: https://github.com/Azure/Connectors-Python-SDK/releases/tag/v0.1.0dev1

Loading
Loading