Criar e configurar um mapa Fabric usando Python e API REST

Este artigo mostra como criar um item de mapa Fabric e aplicar uma definição de mapa usando Python. Demonstra tanto padrões de ordem de operações suportados como inclui o tratamento básico de erros.

Para criar um mapa Fabric programaticamente, precisas de dois componentes:

  • Uma definição de mapa (map.json)
  • Um item de mapa num espaço de trabalho Fabric

Dependendo do padrão que escolher, pode:

  • Crie primeiro o item do mapa e atribua a definição mais tarde, ou
  • Crie o item do mapa com a definição em linha num único pedido

Este artigo demonstra ambas as abordagens.

Tip

Na maioria dos cenários de automação, utilize-se a abordagem que cria o mapa com a sua definição em linha. Esta abordagem garante que o mapa está totalmente configurado na altura da criação e evita chamadas adicionais de API.

Observação

Este artigo foca-se na criação e configuração de mapas (plano de controlo). Deve já ter dados referenciados pelo mapa (como ficheiros GeoJSON ou ícones SVG) no OneLake ou noutra fonte de dados suportada.

Pré-requisitos

  • Acesso a um espaço de trabalho Fabric
  • Identidade de aplicação ou utilizador Microsoft Entra ID com permissões de espaço de trabalho
  • Python 3.9 ou posterior
  • Um token de acesso OAuth 2.0 para a API Fabric REST

Exemplo de definição de mapa (map.json)

O exemplo seguinte apresenta uma carga útil simplificada map.json que:

  • Usa o mapa base padrão
  • Lê dados GeoJSON de uma origem de dados lakehouse
  • Renderiza os dados como uma camada vetorial

A map.json definição é uma configuração declarativa que descreve:

  • dataSources (por exemplo, recursos lakehouse ou eventhouse que fornecem dados)
  • layerSources (os ficheiros, tabelas ou funções que definem os dados para cada camada)
  • layerSettings (as definições de visualização para cada camada, como cores, ícones ou estilo)

A definição descreve o que o mapa deve renderizar, em vez de definir passos processuais para a renderização.

map_json = {
  "$schema": "https://developer.microsoft.com/json-schemas/fabric/item/map/definition/2.0.0/schema.json",
  "basemap": {
    "options": {
      "style": "road"
    }
  },
  "dataSources": [
    {
      "itemType": "Lakehouse",
      "workspaceId": "<workspace-id>",
      "itemId": "<lakehouse-item-id>"
    }
  ],
  "layerSources": [
    {
      "id": "points-source",
      "name": "Points-GeoJSON",
      "type": "geojson",
      "itemId": "<lakehouse-item-id>",
      "relativePath": "Files/data/points.geojson",
      "refreshIntervalMs": 0
    }
  ],
  "layerSettings": [
    {
      "id": "points-layer",
      "name": "Points",
      "sourceId": "points-source",
      "options": {
        "type": "vector",
        "visible": True,
        "color": "#0078D4",
        "pointLayerType": "bubble",
        "bubbleOptions": {
          "color": "#0078D4"
        }
      }
    }
  ]
}

Para mais informações sobre os componentes-chave num map.json, veja MapDetails.

Padrão 1: Criar um mapa e depois atribuir a definição

Use este padrão quando precisar de criar primeiro um item de mapa Fabric e aplicar a definição do mapa mais tarde. Esta abordagem é útil quando a definição do mapa é gerada dinamicamente, proveniente de múltiplos ficheiros ou atualizada incrementalmente depois de o item do mapa já existir. Neste padrão, a chamada inicial à API cria o item do mapa apenas com metadados, e uma chamada subsequente atribui uma definição map.json válida para o esquema para configurar o mapa base, as fontes de dados e as camadas.

Observação

Este padrão é tipicamente usado em cenários avançados onde a definição evolui ao longo do tempo ou é montada dinamicamente.

Passo 1: Criar o item do mapa (apenas metadados)

Esta chamada não inclui uma definição de mapa:

import httpx

BASE_URL = "https://api.fabric.microsoft.com"
TOKEN = "<access-token>"
WORKSPACE_ID = "<workspace-id>"
map_name = "Automated Map (Pattern 1)"

headers = {
    "Authorization": f"Bearer {TOKEN}",
    "Content-Type": "application/json"
}

# Metadata only – NOT map.json
create_map_payload = {
    "displayName": map_name
}

response = httpx.post(
    f"{BASE_URL}/workspaces/{WORKSPACE_ID}/items/maps",
    headers=headers,
    json=create_map_payload,
    timeout=30
)

response.raise_for_status()

map_item = response.json()
map_id = map_item["id"]

Neste ponto, o item do mapa existe, mas não tem definição aplicada.

Passo 2: Defina o mapa (map.json)

Prepare uma carga útil map_json válida, como o exemplo mostrado anteriormente.

Passo 3: Atribuir a definição ao mapa

response = httpx.put(
    f"{BASE_URL}/workspaces/{WORKSPACE_ID}/items/maps/{map_id}/definition",
    headers=headers,
    json={
        "definition": {
            "parts": [
                {
                    "path": "map.json",
                    "payload": map_json,
                    "payloadType": "InlineJson"
                }
            ]
        }
    },
    timeout=30
)

response.raise_for_status()

Use este padrão para criar e configurar o mapa numa única operação.

Esta abordagem é bem adequada para implementações declarativas, como pipelines CI/CD ou fluxos de trabalho de infraestrutura como código.

Requisito Base64

Ao incluir uma definição em chamadas REST, a carga útil deve ser codificada em Base64 e fornecida como parte da definição.

Importante

Se map.json for incluído em qualquer pedido REST, deve ser incluído como uma carga útil codificada em Base64 em definition.parts[] e payloadType: "InlineBase64".

Base64 ajudante

Use este auxiliar para transformar um map.json Python dict na cadeia Base64 exigida por definition.parts[].payload.

import base64
import json

def to_inline_base64(obj: dict) -> str:
    """
    Serialize a JSON-compatible dict and return a Base64-encoded UTF-8 string.
    Use this for definition.parts[].payload when payloadType is InlineBase64.
    """
    raw = json.dumps(obj, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
    return base64.b64encode(raw).decode("utf-8")

Criar mapa com definição em linha

O pedido inclui:

  • displayName (metadados dos itens do mapa)
  • definition.parts[] onde map.json é Base64-encoded e marcado como InlineBase64
import httpx

BASE_URL = "https://api.fabric.microsoft.com/v1"
TOKEN = "<access-token>"
WORKSPACE_ID = "<workspace-id>"
map_name = "Automated Map (Pattern 2)"

headers = {
    "Authorization": f"Bearer {TOKEN}",
    "Content-Type": "application/json",
}

# Schema-valid map.json (example fields abbreviated here)
map_json = {
    "$schema": "https://developer.microsoft.com/json-schemas/fabric/item/map/definition/2.0.0/schema.json",
    "basemap": {"options": {"style": "road"}},
    "dataSources": [
        {
            "itemType": "KqlDatabase",
            "workspaceId": "<workspace-id>",
            "itemId": "<eventhouse-or-kql-db-item-id>"
        }
    ],
    "layerSources": [
        {
            "id": "work-orders-src",
            "name": "Work orders (Eventhouse)",
            "type": "kusto",
            "itemId": "<eventhouse-or-kql-db-item-id>",
            "refreshIntervalMs": 5000
        }
    ],
    "layerSettings": [
        {
            "id": "work-orders-layer",
            "name": "Work orders",
            "sourceId": "work-orders-src",
            "options": {"type": "vector", "visible": True, "color": "#0078D4"}
        }
    ]
}

payload = {
    "displayName": map_name,
    "definition": {
        "parts": [
            {
                "path": "map.json",
                "payload": to_inline_base64(map_json),
                "payloadType": "InlineBase64"
            }
        ]
    }
}
with httpx.Client(timeout=60) as client:

response = client.post(
    f"{BASE_URL}/workspaces/{WORKSPACE_ID}/maps",
    headers=headers,
    json=payload,
    timeout=30
)
response.raise_for_status()
created = response.json()
map_id = created["id"]

Resumo: Padrão 1 vs Padrão 2 (regra Base64)

Ambos os padrões de criação de mapas dependem, em última análise, do mesmo contrato de definição de mapa, mas diferem quanto ao momento em que a definição é fornecida e quando é necessária a codificação Base64. Esta secção reexamina o Padrão 1 e o Padrão 2 lado a lado para esclarecer uma fonte comum de confusão: Sempre que map.json é incluído numa chamada REST — seja durante a criação ou atualização — deve ser fornecido como uma parte de definição codificada em Base64. Compreender esta distinção ajuda a garantir que os fluxos de trabalho de automação de mapas aplicam o formato correto da carga útil em cada passo e evitam erros subtis no formato do pedido.

Padrão 1: Criar o mapa → depois atribuir definição

  • Chamada de Create Map: apenas metadados (sem definição, portanto sem Base64 na altura da criação).
  • Atribuir definição: usar Update Map Definition com definition.parts[] e payloadType: "InlineBase64" (portanto, o Base64 é necessário na altura da atualização).

Padrão 2: Criar mapa com definição incluída

  • A chamada Create Map inclui a definição: Forneça definition.parts[] e codifique em Base64 map.json utilizando payloadType: "InlineBase64".

Regra geral: Se incluir map.json em qualquer pedido ou resposta REST, leve-o como uma carga útil Base64 numa definition.parts[] entrada com payloadType: "InlineBase64".

Criar função auxiliar para recuperar o ID do mapa

Quando cria um mapa usando a API Fabric REST, o pedido pode devolver uma resposta 202 Aceite. Esta resposta indica que o mapa está a ser provisionado de forma assíncrona como uma operação de longa duração (LRO), em vez de ser criado imediatamente. Neste caso, a resposta não inclui o ID do mapa, e o endpoint de conclusão do LRO pode não devolver um resultado utilizável. Mesmo após a conclusão da operação, o mapa recém-criado pode não aparecer imediatamente ao chamar a API de Mapas de Listas devido à propagação do backend.

Para obter o ID do mapa de forma fiável, deve consultar a lista de mapas e tentar novamente até que o novo mapa fique visível. A seguinte função auxiliar implementa este padrão de repetição e garante que o seu fluxo de automação seja resiliente a atrasos no provisionamento assíncrono.

# ---------------------------------------------------------
# Get Map ID
# ---------------------------------------------------------

def resolve_map_id(client, list_url, headers, map_name, max_attempts=10, delay=5):
    """
    Resolve the ID of a newly created Fabric map.

    Why this function is needed:
    - Creating a map may return HTTP 202 (Accepted), indicating an asynchronous
      long-running operation (LRO) rather than immediate creation.
    - LRO responses for map creation don't always return a resource ID.
    - Even after the operation completes, the new map may not be immediately
      visible in the List Maps API due to backend propagation delays.

    What this function does:
    - Repeatedly calls the List Maps API.
    - Searches for a map matching the provided display name.
    - Retries for a configurable number of attempts with a delay between calls.

    Parameters:
        client        : Authenticated HTTP client
        list_url      : Maps list endpoint
        headers       : Authorization headers for Fabric API
        map_name      : Display name of the map to locate
        max_attempts  : Maximum number of retry attempts
        delay         : Delay (seconds) between retries

    Returns:
        The map ID (string) once the map becomes visible.

    Raises:
        RuntimeError if the map is not found after all retry attempts.
    """

    for attempt in range(max_attempts):
        print(f"Resolving map (attempt {attempt + 1}/{max_attempts})...")

        resp = client.get(list_url, headers=headers)
        resp.raise_for_status()

        items = resp.json().get("value", [])

        match = next(
            (m for m in items if m.get("displayName") == map_name),
            None
        )

        if match:
            print("✅ Map found!")
            return match["id"]

        print("⏳ Map not visible yet. Retrying...")
        time.sleep(delay)

    raise RuntimeError("Map created but still not visible after retries")

Em vez de obter o ID do mapa como no exemplo anterior (map_id = created["id"]), experimente:

    if response.status_code == 201:
        map_id = response.json()["id"]
    elif response.status_code == 202:
        print("LRO completed via 202. Resolving map by name...")
        map_id = resolve_map_id(
            client,
            create_map_url,
            _fabric_headers(),
            map_name
        )  

Passos seguintes