Creación y configuración de un mapa de Fabric mediante Python y la API REST

En este artículo se muestra cómo crear un elemento de mapa Fabric y aplicar una definición de mapa mediante Python. Muestra los patrones de orden de operaciones admitidos e incluye el control de errores básico.

La creación de un mapa de Fabric de forma programática requiere dos componentes.

  • Definición de mapa (map.json)
  • Elemento mapa en un área de trabajo de Fabric

En función del patrón que elija, puede hacer lo siguiente:

  • Cree primero el elemento de mapa y asigne la definición más adelante, o
  • Crear el elemento de mapa con la definición insertada en una única solicitud

En este artículo se muestran ambos enfoques.

Sugerencia

En la mayoría de los escenarios de automatización, el enfoque recomendado es crear el mapa con su definición insertada. Esto garantiza que el mapa esté totalmente configurado en el momento de la creación y evite llamadas API adicionales.

Nota:

Este artículo se centra en la creación y configuración del mapa (plano de control). Los datos a los que hace referencia el mapa (como archivos GeoJSON o iconos SVG) ya deben existir en OneLake u otro origen de datos compatible.

Prerrequisitos

  • Acceso a un área de trabajo de Fabric
  • Microsoft Entra ID identidad de usuario o aplicación con permisos de área de trabajo
  • Python 3.9 o posterior
  • Un token de acceso de OAuth 2.0 para la API REST de Fabric

Definición de mapa de ejemplo (map.json)

En el ejemplo siguiente se muestra una carga simplificada map.json que:

  • Usa el mapa base predeterminado
  • Lee los datos de GeoJSON de un origen de datos de Lakehouse.
  • Representa los datos como una capa vectorial

La map.json definición es una configuración declarativa que describe:

  • dataSources (por ejemplo, recursos de Lakehouse o Eventhouse que proporcionan datos)
  • layerSources (los archivos, tablas o funciones que definen los datos de cada capa)
  • layerSettings (la configuración de visualización de cada capa, como colores, iconos o estilos)

La definición describe qué debe representar el mapa, en lugar de definir los pasos de procedimiento para la representación.

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 obtener más información sobre los componentes clave de un map.json, vea MapDetails.

Patrón 1: Crear un mapa y asignar la definición

Use este patrón cuando necesite crear primero un elemento de mapa de Fabric y aplicar la definición de mapa más adelante. Este enfoque es útil cuando la definición de mapa se genera dinámicamente, se origina a partir de varios archivos o se actualiza incrementalmente después de que el elemento de mapa ya exista. En este patrón, la llamada API inicial crea el elemento de mapa solo con metadatos y una llamada posterior asigna una definición de map.json válida según el esquema para configurar la cartografía base, los orígenes de datos y las capas.

Nota:

Este patrón se usa normalmente para escenarios avanzados en los que la definición evoluciona con el tiempo o se ensambla dinámicamente.

Paso 1: Crear el elemento de mapa (solo metadatos)

Esta llamada no incluye una definición 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"]

En este momento, el elemento de mapa existe pero no se aplica ninguna definición.

Paso 2: Definir el mapa (map.json)

Prepare una carga de map_json válida, como el ejemplo mostrado anteriormente.

Paso 3: Asignar la definición al 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 patrón para crear y configurar el mapa en una sola operación.

Este enfoque es adecuado para implementaciones declarativas, como canalizaciones de CI/CD o flujos de trabajo de infraestructura como código.

Requisito de Base64

Al incluir una definición en llamadas REST, la carga debe estar codificada en Base64 y proporcionarse como parte de definición.

Importante

Si map.json se incluye en cualquier solicitud REST, debe incluirse como una carga codificada en Base64 en definition.parts[] con payloadType: "InlineBase64".

Asistente de Base64

Use este asistente para convertir un map.json Python dict en la cadena Base64 requerida 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")

Crea un mapa con definición en línea

La solicitud incluye:

  • displayName (metadatos del elemento de mapa)
  • definition.parts[] donde map.json está Base64-encoded y se marca 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"]

Resumen: patrón 1 frente a patrón 2 (regla base64)

Ambos patrones de creación de mapas se basan en última instancia en el mismo contrato de definición de mapa, pero difieren en cuándo se proporciona la definición y cuándo se requiere la codificación Base64. En esta sección se vuelve a examinar el patrón 1 y el patrón 2 en paralelo para aclarar una fuente común de confusión: cada vez que map.json se incluye en una llamada REST (ya sea durante la creación o la actualización), se debe proporcionar como parte de la definición codificada en Base64. Comprender esta distinción ayuda a garantizar que los flujos de trabajo de automatización de mapas apliquen el formato de carga correcto en cada paso y eviten errores sutiles de formato de solicitud.

Patrón 1: Crear mapa → luego asignar la definición

  • Crear llamada de mapa: solo metadatos (sin definición, por lo que no hay Base64 en tiempo de creación).
  • Asignar definición: utilice Update Map Definition con definition.parts[] y payloadType: "InlineBase64" (por lo tanto, Base64 es necesario en el momento de la actualización).

Patrón 2: Creación de un mapa con la definición incluida

  • La llamada para crear un mapa incluye la definición: Debe proporcionar definition.parts[] y codificar map.json en Base64 mediante payloadType: "InlineBase64".

Regla general: Si map.json se incluye en cualquier solicitud o respuesta REST, se lleva como una carga base64 en una definition.parts[] entrada con payloadType: "InlineBase64".

Creación de una función auxiliar para recuperar el identificador de mapa

Al crear un mapa mediante la API rest de Fabric, la solicitud puede devolver una respuesta aceptada 202. Esto indica que el mapa se está aprovisionando de forma asincrónica como una operación de larga duración (LRO), en lugar de crearse inmediatamente. En este caso, la respuesta no incluye el identificador de mapa y es posible que el endpoint de finalización de LRO no devuelva un resultado utilizable. Además, incluso después de que se complete la operación, es posible que el mapa recién creado no aparezca inmediatamente al llamar a la API Listar Mapas debido a la propagación interna.

Para obtener el identificador de mapa de forma confiable, debe consultar la lista de mapas y volver a intentarlo hasta que el nuevo mapa sea visible. La siguiente función auxiliar implementa este patrón de reintento y garantiza que el flujo de automatización sea resistente a retrasos de aprovisionamiento asincrónicos.

# ---------------------------------------------------------
# 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")

A continuación, en lugar de obtener el ID del mapa como se muestra en el ejemplo anterior (map_id = created["id"]), pruebe:

    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
        )  

Pasos siguientes