APM

>Agent Skill

@microsoft/rest-api-patterns

skilldevops

Guide for implementing REST API wrapper functions. Use this when adding new API wrappers or troubleshooting API calls.

pythonapi-designdocumentation
apm::install
$apm install @microsoft/rest-api-patterns
apm::skill.md
---
name: rest-api-patterns
description: Guide for implementing REST API wrapper functions. Use this when adding new API wrappers or troubleshooting API calls.
---

# REST API Patterns

This skill covers the patterns and utilities for implementing REST API wrapper functions in Semantic Link Labs.

## When to Use This Skill

Use this skill when you need to:
- Implement new REST API wrappers
- Understand the `_base_api` helper function
- Handle pagination, long-running operations, or errors
- Debug API-related issues

---

## Finding API Documentation

Before implementing a wrapper, use the API search tool to find the relevant documentation:

### Using `search_public_api_doc.py`

```bash
# Navigate to the scripts directory
cd .claude/skills/rest-api-patterns/scripts

# Search both Fabric and Power BI APIs
python search_public_api_doc.py "dataset refresh"

# Search Fabric APIs only
python search_public_api_doc.py "create item" --source fabric

# Search Power BI APIs only
python search_public_api_doc.py "gateway" --source powerbi

# Limit results
python search_public_api_doc.py "workspace" --limit 10
```

### Example Output

```
🔍 Searching for: 'dataset refresh' in Fabric + Power BI
================================================================================
📥 Fetching Microsoft Fabric TOC...
   ✅ Loaded 15 top-level categories from Microsoft Fabric
📥 Fetching Power BI TOC...
   ✅ Loaded 20 top-level categories from Power BI

Found 5 results:

1. [POWERBI] Datasets - Refresh Dataset In Group (score: 95.0)
   URL: https://learn.microsoft.com/en-us/rest/api/power-bi/datasets/refresh-dataset-in-group
   Path: Datasets > Refresh Dataset In Group

2. [POWERBI] Datasets - Get Refresh History In Group (score: 90.0)
   URL: https://learn.microsoft.com/en-us/rest/api/power-bi/datasets/get-refresh-history-in-group
   Path: Datasets > Get Refresh History In Group
```

### Requirements

The script requires `rapidfuzz` and `requests`:

```bash
pip install rapidfuzz requests
```

### API Documentation References

| API | Base URL | Documentation |
|-----|----------|---------------|
| Fabric REST API | `https://api.fabric.microsoft.com/v1/` | [Fabric REST API](https://learn.microsoft.com/rest/api/fabric/) |
| Power BI REST API | `https://api.powerbi.com/v1.0/myorg/` | [Power BI REST API](https://learn.microsoft.com/rest/api/power-bi/) |

---

## API Client Architecture

Semantic Link Labs uses `sempy.fabric.FabricRestClient` as the underlying HTTP client, wrapped by the `_base_api` helper function.

### Key Components

| Component | Purpose |
|-----------|---------|
| `_base_api` | Main helper for all API calls |
| `FabricRestClient` | HTTP client from sempy |
| `pagination` | Handles paginated responses |
| `lro` | Handles long-running operations |

---

## The `_base_api` Function

Located in `src/sempy_labs/_helper_functions.py`, this is the standard way to make API calls.

### Function Signature

```python
def _base_api(
    request: str,                      # API endpoint path
    client: str = "fabric",            # Client type
    method: str = "get",               # HTTP method
    payload: Optional[str] = None,     # Request body
    status_codes: Optional[int] = 200, # Expected status codes
    uses_pagination: bool = False,     # Enable pagination
    lro_return_json: bool = False,     # Wait for LRO, return JSON
    lro_return_status_code: bool = False,  # Wait for LRO, return status
    lro_return_df: bool = False,       # Wait for LRO, return DataFrame
):
```

### Client Types

| Client | Use Case | Authentication |
|--------|----------|----------------|
| `fabric` | Standard Fabric API | Default notebook credentials |
| `fabric_sp` | Fabric API with SP support | Service Principal or default |
| `azure` | Azure Resource Manager | Service Principal |
| `graph` | Microsoft Graph | Service Principal |
| `onelake` | OneLake storage | Storage token |

### Return Types

**The `_base_api` function returns different types depending on the parameters used:**

| Parameters | Return Type | How to Access Data |
|------------|-------------|-------------------|
| Default (no special flags) | `Response` object | Call `.json()` to get dict |
| `uses_pagination=True` | `list[dict]` | Iterate over list, each item has `.get("value", [])` |
| `lro_return_json=True` | `dict` | Access directly, already parsed JSON |
| `lro_return_status_code=True` | `int` | HTTP status code |
| `lro_return_df=True` | `DataFrame` | Use directly |

**⚠️ COMMON MISTAKE:** Forgetting to call `.json()` on the response for simple GET requests.

```python
# ❌ WRONG - response is a Response object, not a dict
response = _base_api(request=f"/v1/workspaces/{workspace_id}/items/{item_id}")
name = response.get("displayName")  # AttributeError: 'Response' object has no attribute 'get'

# ✅ CORRECT - call .json() to get the dict
response = _base_api(request=f"/v1/workspaces/{workspace_id}/items/{item_id}").json()
name = response.get("displayName")  # Works!
```

---

## Common API Patterns

### Simple GET Request

```python
from sempy_labs._helper_functions import _base_api

response = _base_api(
    request=f"/v1/workspaces/{workspace_id}/items",
    client="fabric_sp",
)

data = response.json()
```

### POST Request with Payload

```python
payload = {
    "displayName": name,
    "description": description,
}

response = _base_api(
    request=f"/v1/workspaces/{workspace_id}/items",
    method="post",
    payload=payload,
    status_codes=[201, 202],
    client="fabric_sp",
)
```

### DELETE Request

```python
_base_api(
    request=f"/v1/workspaces/{workspace_id}/items/{item_id}",
    method="delete",
    client="fabric_sp",
)
```

### PATCH Request

```python
payload = {
    "displayName": new_name,
}

_base_api(
    request=f"/v1/workspaces/{workspace_id}/items/{item_id}",
    method="patch",
    payload=payload,
    client="fabric_sp",
)
```

---

## Handling Pagination

For APIs that return paginated results:

```python
from sempy_labs._helper_functions import _base_api, _create_dataframe

columns = {
    "Id": "string",
    "Name": "string",
}
df = _create_dataframe(columns=columns)

# Get all pages
responses = _base_api(
    request=f"/v1/workspaces/{workspace_id}/items",
    uses_pagination=True,
    client="fabric_sp",
)

# Process all responses
rows = []
for r in responses:
    for item in r.get("value", []):
        rows.append({
            "Id": item.get("id"),
            "Name": item.get("displayName"),
        })

if rows:
    df = pd.DataFrame(rows)

return df
```

---

## Handling Long-Running Operations (LRO)

Some APIs return 202 Accepted and require polling for completion.

### Return JSON When Complete

```python
result = _base_api(
    request=f"/v1/workspaces/{workspace_id}/items/{item_id}/getDefinition",
    method="post",
    lro_return_json=True,
    client="fabric_sp",
)

# Result contains the final JSON response
definition = result.get("definition")
```

### Return Status Code

```python
status = _base_api(
    request=f"/v1/workspaces/{workspace_id}/items",
    method="post",
    payload=payload,
    lro_return_status_code=True,
    client="fabric_sp",
)

# status is the final HTTP status code
if status == 200:
    print("Operation completed successfully")
```

---

## Error Handling

### Expected Status Codes

Specify expected status codes to avoid exceptions:

```python
# Accept 200, 201, or 202 as success
response = _base_api(
    request=url,
    method="post",
    payload=payload,
    status_codes=[200, 201, 202],
    client="fabric_sp",
)
```

### FabricHTTPException

When status code doesn't match, `FabricHTTPException` is raised:

```python
from sempy.fabric.exceptions import FabricHTTPException

try:
    response = _base_api(
        request=f"/v1/workspaces/{workspace_id}/items/{item_id}",
        client="fabric_sp",
    )
except FabricHTTPException as e:
    if e.response.status_code == 404:
        print(f"Item not found")
    else:
        raise
```

---

## Building URLs with Parameters

Use the `_build_url` helper for query parameters:

```python
from sempy_labs._helper_functions import _build_url

url = "/v1/admin/workspaces"
params = {
    "capacityId": capacity_id,
    "state": "Active",
}

url = _build_url(url, params)
# Result: "/v1/admin/workspaces?capacityId=xxx&state=Active"

responses = _base_api(
    request=url,
    uses_pagination=True,
    client="fabric_sp",
)
```

---

## API Endpoint Patterns

### Fabric Core API

```python
# List items in workspace
f"/v1/workspaces/{workspace_id}/items"

# Get specific item
f"/v1/workspaces/{workspace_id}/items/{item_id}"

# Item operations
f"/v1/workspaces/{workspace_id}/items/{item_id}/getDefinition"
f"/v1/workspaces/{workspace_id}/items/{item_id}/updateDefinition"
```

### Fabric Admin API

```python
# Admin workspaces
"/v1/admin/workspaces"

# Admin items
"/v1/admin/items"

# Capacities
"/v1/admin/capacities"
```

### Power BI REST API

```python
# Groups (workspaces)
f"/v1.0/myorg/groups/{workspace_id}/..."

# Datasets
f"/v1.0/myorg/groups/{workspace_id}/datasets/{dataset_id}/..."

# Reports
f"/v1.0/myorg/groups/{workspace_id}/reports/{report_id}/..."
```

### Azure Resource Manager

```python
# Fabric capacities
f"https://management.azure.com/subscriptions/{subscription_id}/providers/Microsoft.Fabric/capacities"

# Resource groups
f"https://management.azure.com/subscriptions/{subscription_id}/resourceGroups/{resource_group}"
```

---

## Authentication Headers

For non-Fabric clients (Azure, Graph), use `_get_headers`:

```python
from sempy_labs._authentication import _get_headers
import sempy_labs._authentication as auth

headers = _get_headers(auth.token_provider.get(), audience="azure")

response = requests.get(
    url,
    headers=headers,
)
```

---

## Creating Result DataFrames

Use `_create_dataframe` for consistent empty DataFrames:

```python
from sempy_labs._helper_functions import _create_dataframe

columns = {
    "Id": "string",
    "Name": "string",
    "Type": "string",
    "Created Date": "datetime",
    "Size": "int",
}

df = _create_dataframe(columns=columns)
```

### Updating DataFrame Types

```python
from sempy_labs._helper_functions import _update_dataframe_datatypes

column_map = {
    "Created Date": "datetime",
    "Size": "int",
    "Is Active": "bool",
}

_update_dataframe_datatypes(df, column_map)
```

---

## Complete Example

```python
from sempy._utils._log import log
from sempy_labs._helper_functions import (
    resolve_workspace_name_and_id,
    _base_api,
    _create_dataframe,
    _build_url,
)
import sempy_labs._icons as icons
from typing import Optional
from uuid import UUID
import pandas as pd


@log
def list_my_items(
    item_type: Optional[str] = None,
    workspace: Optional[str | UUID] = None,
) -> pd.DataFrame:
    """
    Lists items in a workspace.

    This is a wrapper function for the following API: `Items - List Items <https://learn.microsoft.com/rest/api/fabric/core/items/list-items>`_.

    Service Principal Authentication is supported.

    Parameters
    ----------
    item_type : str, default=None
        Filter by item type.
    workspace : str | uuid.UUID, default=None
        The Fabric workspace name or ID.
        Defaults to None which resolves to the workspace of the attached lakehouse.

    Returns
    -------
    pandas.DataFrame
        A pandas dataframe showing items in the workspace.
    """

    (workspace_name, workspace_id) = resolve_workspace_name_and_id(workspace)

    columns = {
        "Id": "string",
        "Name": "string",
        "Type": "string",
    }
    df = _create_dataframe(columns=columns)

    url = f"/v1/workspaces/{workspace_id}/items"
    params = {}
    if item_type:
        params["type"] = item_type
    if params:
        url = _build_url(url, params)

    responses = _base_api(
        request=url,
        uses_pagination=True,
        client="fabric_sp",
    )

    rows = []
    for r in responses:
        for item in r.get("value", []):
            rows.append({
                "Id": item.get("id"),
                "Name": item.get("displayName"),
                "Type": item.get("type"),
            })

    if rows:
        df = pd.DataFrame(rows)

    return df
```

---

## Debugging API Calls

### Print Response Details

```python
response = _base_api(
    request=url,
    client="fabric_sp",
)

print(f"Status: {response.status_code}")
print(f"Headers: {response.headers}")
print(f"Body: {response.json()}")
```

### Check Request Being Made

Add temporary debug prints:

```python
print(f"Making request to: {url}")
print(f"Payload: {payload}")
```

---

## API Documentation References

| API | Documentation |
|-----|---------------|
| Fabric Core | [https://learn.microsoft.com/rest/api/fabric/core/](https://learn.microsoft.com/rest/api/fabric/core/) |
| Fabric Admin | [https://learn.microsoft.com/rest/api/fabric/admin/](https://learn.microsoft.com/rest/api/fabric/admin/) |
| Power BI | [https://learn.microsoft.com/rest/api/power-bi/](https://learn.microsoft.com/rest/api/power-bi/) |
| Azure Fabric | [https://learn.microsoft.com/rest/api/microsoftfabric/](https://learn.microsoft.com/rest/api/microsoftfabric/) |
| Graph | [https://learn.microsoft.com/graph/api/overview](https://learn.microsoft.com/graph/api/overview) |