Introducción
En entornos reales de Microsoft Fabric no trabajamos con un único Lakehouse ni con un solo workspace. Lo habitual es convivir con múltiples workspaces, lakehouses, warehouses y bases de datos SQL, que van creciendo con el tiempo y se reutilizan desde distintos notebooks.
El problema aparece cuando ese crecimiento no va acompañado de una forma ordenada de acceder a los artefactos. Muy rápidamente los notebooks empiezan a llenarse de:
- rutas
abfss://...hardcodeadas - IDs de workspaces y lakehouses copiados a mano
- dependencias implícitas que solo entiende quien escribió el código
Todo funciona… hasta que el mantenimiento y la evolución del entorno se vuelven una pesadilla.
Este artículo muestra cómo centralizar el acceso a los artefactos de Microsoft Fabric desde un único notebook, utilizando la API de Fabric para descubrir dinámicamente:
- los workspaces disponibles,
- sus lakehouses,
- warehouses
- y bases de datos SQL
y exponer esa información como variables globales reutilizables y como un catálogo en memoria accesible desde otros notebooks.
El objetivo no es gestionar permisos ni seguridad, ni abstraer Fabric detrás de una capa compleja. El objetivo es mucho más práctico:
Dejar de repetir rutas, IDs y nombres en cada notebook y disponer de un único punto de referencia para acceder a los artefactos de Fabric.
A lo largo del artículo construiremos este notebook paso a paso, empezando por una versión básica y evolucionándolo hasta una solución robusta que tenga en cuenta las limitaciones reales de la API de Microsoft Fabric.
Descubriendo artefactos de Microsoft Fabric desde un notebook
Como primer paso, vamos a construir un código básico que realiza lo siguiente:
- Se autentica contra la API de Microsoft Fabric usando el contexto del propio notebook.
- Recupera la lista de workspaces visibles para el usuario.
- Para cada workspace:
- Recupera los lakehouses
- Recupera los warehouses
- Recupera las bases de datos SQL
- Expone la información como variables globales (IDs y rutas principales).
- Construye un diccionario en memoria con todos los artefactos descubiertos.
Con esto ya conseguimos algo muy útil: tener disponibles, desde cualquier notebook, los IDs y paths de OneLake sin necesidad de hardcodearlos cada vez que necesitemos acceder a algún artefacto.
El codigo es el siguiente:
import requests
print("Fabric - Descubrimiento de artefactos")
# Autenticación contra la API de Fabric usando el contexto del notebook
token = mssparkutils.credentials.getToken("pbi")
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
# Recuperar la lista de workspaces visibles
print("Recuperando workspaces")
workspaces_url = "https://api.fabric.microsoft.com/v1/workspaces"
response = requests.get(workspaces_url, headers=headers)
if response.status_code != 200:
raise Exception(
f"Error al recuperar workspaces: {response.status_code} - {response.text}"
)
workspaces = response.json().get("value", [])
# Catálogo central en memoria
FABRIC_CATALOG = {
"lakehouses": {},
"warehouses": {},
"sqldatabases": {},
"metadata": {
"total_workspaces": len(workspaces)
}
}
total_lakehouses = 0
total_warehouses = 0
total_sqldatabases = 0
# Procesar cada workspace
for ws in workspaces:
workspace_name = ws["displayName"]
workspace_id = ws["id"]
# Lakehouses del workspace
lakehouses_url = (
f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/lakehouses"
)
lh_response = requests.get(lakehouses_url, headers=headers)
if lh_response.status_code == 200:
lakehouses = lh_response.json().get("value", [])
for lh in lakehouses:
name = lh["displayName"]
id_ = lh["id"]
var_name = ''.join(
c for c in name.replace(" ", "_").replace("-", "_")
if c.isalnum() or c == "_"
).upper()
tables_path = (
f"abfss://{workspace_id}"
f"@onelake.dfs.fabric.microsoft.com/{id_}/Tables"
)
files_path = (
f"abfss://{workspace_id}"
f"@onelake.dfs.fabric.microsoft.com/{id_}/Files"
)
globals()[f"{var_name}_WORKSPACE_ID"] = workspace_id
globals()[f"{var_name}_LAKEHOUSE_ID"] = id_
globals()[f"{var_name}_PATH"] = tables_path
globals()[f"{var_name}_FILES_PATH"] = files_path
FABRIC_CATALOG["lakehouses"][var_name] = {
"workspace_name": workspace_name,
"workspace_id": workspace_id,
"lakehouse_name": name,
"lakehouse_id": id_,
"tables_path": tables_path,
"files_path": files_path
}
total_lakehouses += 1
# Warehouses del workspace
warehouses_url = (
f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/warehouses"
)
wh_response = requests.get(warehouses_url, headers=headers)
if wh_response.status_code == 200:
warehouses = wh_response.json().get("value", [])
for wh in warehouses:
name = wh["displayName"]
id_ = wh["id"]
var_name = ''.join(
c for c in name.replace(" ", "_").replace("-", "_")
if c.isalnum() or c == "_"
).upper()
globals()[f"{var_name}_WORKSPACE_ID"] = workspace_id
globals()[f"{var_name}_WAREHOUSE_ID"] = id_
FABRIC_CATALOG["warehouses"][var_name] = {
"workspace_name": workspace_name,
"workspace_id": workspace_id,
"warehouse_name": name,
"warehouse_id": id_
}
total_warehouses += 1
# Bases de datos SQL del workspace
sqldb_url = (
f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/sqldatabases"
)
db_response = requests.get(sqldb_url, headers=headers)
if db_response.status_code == 200:
databases = db_response.json().get("value", [])
for db in databases:
name = db["displayName"]
id_ = db["id"]
var_name = ''.join(
c for c in name.replace(" ", "_").replace("-", "_")
if c.isalnum() or c == "_"
).upper()
globals()[f"{var_name}_WORKSPACE_ID"] = workspace_id
globals()[f"{var_name}_SQLDB_ID"] = id_
FABRIC_CATALOG["sqldatabases"][var_name] = {
"workspace_name": workspace_name,
"workspace_id": workspace_id,
"database_name": name,
"database_id": id_
}
total_sqldatabases += 1
# Resumen final
print("Resumen de artefactos descubiertos")
print(f"Workspaces: {len(workspaces)}")
print(f"Lakehouses: {total_lakehouses}")
print(f"Warehouses: {total_warehouses}")
print(f"Bases de datos SQL: {total_sqldatabases}")
print("Carga finalizada")
Una vez ejecutado este codigo, recibiremos un resultado similar al siguiente:

Llegados a este punto, toda la información de los artefactos de Fabric queda disponible en el diccionario FABRIC_CATALOG. Este catálogo vive en memoria y nos permite consultar de forma estructurada los lakehouses, warehouses y bases de datos SQL que se han encontrado, sin necesidad de recorrer Workspaces ni llamar de nuevo a la API.
Esto resulta especialmente útil cuando queremos:
- listar qué lakehouses tenemos disponibles,
- inspeccionar sus rutas de OneLake,
- o construir lógica dinámica basada en los artefactos existentes.
Por ejemplo, para mostrar los Lakehouses cargados, podemos ejecutar lo siguiente:
# Mostrar los nombres de los Lakehouses cargados en el catálogo
for lakehouse_key in FABRIC_CATALOG["lakehouses"]:
print(lakehouse_key)Si ejecutas el código anterior en un entorno con pocos artefactos, lo más probable es que el resultado sea el esperado. Sin embargo, en entornos más grandes puede ocurrir algo curioso: no aparecen todos los lakehouses (o warehouses, o bases de datos) que sabemos que existen y a los que tenemos acceso.
Este comportamiento no suele estar relacionado con permisos ni con errores en el código. El notebook se autentica correctamente y la API responde sin fallos aparentes. Aun así, el número de artefactos encontrados no siempre coincide con la realidad del entorno.
La causa es menos evidente, pero muy común cuando se empieza a trabajar con la API de Microsoft Fabric: las respuestas de la API están paginadas. Por defecto, muchas llamadas devuelven solo una primera página de resultados, incluso cuando existen más elementos disponibles. Si el código no gestiona explícitamente esa paginación, simplemente deja de ver todo lo que queda fuera de la primera respuesta.
En otras palabras, el código funciona… pero solo para la primera página.
Este detalle pasa desapercibido en entornos pequeños, pero se convierte en un problema real a medida que el número de workspaces y artefactos crece.
Para que el notebook sea realmente fiable, es necesario adaptar el código para recorrer todas las páginas que devuelve la API de Fabric y no solo la primera. En el siguiente apartado veremos cómo detectar y seguir el enlace @odata.nextLink que la API proporciona cuando existen más resultados, y cómo encapsular esta lógica para reutilizarla de forma limpia en todas las llamadas.
Leyendo todas las páginas de la API de Fabric
Como hemos visto, la API de Microsoft Fabric no siempre devuelve todos los resultados en una única respuesta. Cuando el número de elementos es elevado, la API responde de forma paginada y solo devuelve una parte del total.
En estos casos, la respuesta JSON incluye una propiedad llamada: "@odata.nextLink"
Este campo contiene la URL de la siguiente página de resultados. Si el código no comprueba ni sigue este enlace, el proceso se detiene tras la primera página y el resultado queda incompleto.
Por tanto, para recuperar todos los artefactos, el patrón correcto es siempre el mismo:
- Llamar a la URL inicial.
- Procesar los resultados devueltos.
- Comprobar si existe
@odata.nextLink. - Si existe, volver a llamar a esa URL.
- Repetir el proceso hasta que no haya más páginas.
Esta lógica es transversal a todas las llamadas que estamos haciendo (workspaces, lakehouses, warehouses y bases de datos SQL), por lo que tiene sentido encapsularla en una función reutilizable. El siguiente código implementa esa lógica de forma genérica. Dada una URL inicial de la API de Fabric, devuelve todos los elementos, independientemente del número de páginas:
def get_all_pages(url, headers):
"""
Recupera todos los elementos de un endpoint paginado de la API de Fabric
siguiendo el enlace @odata.nextLink cuando existe.
"""
results = []
while url:
response = requests.get(url, headers=headers)
if response.status_code != 200:
raise Exception(
f"Error en la llamada a la API: {response.status_code} - {response.text}"
)
data = response.json()
# Añadir los resultados de la página actual
results.extend(data.get("value", []))
# Obtener la siguiente página, si existe
url = data.get("@odata.nextLink")
return results
La función get_all_pages debe colocarse después de la autenticación y antes de empezar a recuperar los workspaces, ya que será utilizada por todas las llamadas posteriores.
Cuando el entorno crece: cuidado con el throttling
Una vez solucionado el problema de la paginación, el notebook empieza a comportarse como esperamos: descubre todos los workspaces y, dentro de cada uno, todos los lakehouses, warehouses y bases de datos SQL disponibles. Sin embargo, en entornos medianos o grandes suele aparecer un nuevo problema, menos evidente pero igual de importante: el throttling de la API.
Este problema se manifiesta normalmente en forma de errores 429 (Too Many Requests), acompañados de mensajes indicando que la solicitud ha sido bloqueada temporalmente por el servicio. El código deja de ejecutarse.
Este comportamiento no es un fallo del notebook ni un problema de permisos. Es el resultado directo de cómo funciona la Fabric REST API.
Por qué ocurre el error 429 en Microsoft Fabric
Cuando ejecutamos el notebook, el número de llamadas a la API crece rápidamente:
- una o varias llamadas para listar los workspaces (con paginación),
- por cada workspace, varias llamadas adicionales para:
- lakehouses,
- warehouses,
- bases de datos SQL
- y, a su vez, cada una de estas llamadas puede estar paginada.
En entornos con muchos workspaces y artefactos, el volumen total de peticiones en muy poco tiempo puede ser elevado. La API de Fabric aplica límites para proteger el servicio y, cuando se superan, responde con un 429, bloqueando temporalmente nuevas solicitudes.
Desde el punto de vista del notebook, todo esto ocurre de forma silenciosa hasta que, de repente, una llamada deja de responder correctamente.
Control de throttling y reintentos en la API de Fabric
Para evitar los errores 429 es necesario centralizar la lógica de llamadas a la API en una función que:
- detecte respuestas 429 (Too Many Requests),
- espere antes de reintentar,
- aplique una espera incremental si el problema persiste,
- y continúe el proceso sin interrumpir la ejecución del notebook.
La idea es sencilla: el resto del código no debe saber que existe el throttling. Toda la complejidad queda encapsulada en una única función. Lo que tendríamos que hacer es modificar nuestra función get_all_pages de la siguiente manera:
import time
import random
def get_all_pages(url, headers, max_retries=5, sleep_between_pages=0.2):
"""
Recupera todos los elementos de un endpoint paginado de la API de Fabric.
Gestiona errores 429 aplicando reintentos con espera incremental.
"""
results = []
while url:
retries = 0
while True:
response = requests.get(url, headers=headers)
if response.status_code == 200:
break
if response.status_code == 429 and retries < max_retries:
wait_time = (2 ** retries) + random.uniform(0.5, 1.5)
time.sleep(wait_time)
retries += 1
continue
raise Exception(
f"Error en la llamada a la API: {response.status_code} - {response.text}"
)
data = response.json()
results.extend(data.get("value", []))
url = data.get("@odata.nextLink")
time.sleep(sleep_between_pages)
return results
Aunque este enfoque reduce considerablemente los errores de este tipo, en entornos con muchos workspaces o un volumen elevado de artefactos puede seguir produciéndose algún 429 – RequestBlocked. Esto ocurre porque el servicio aplica límites dinámicos de consumo y puede bloquear temporalmente las llamadas aunque estemos gestionando la paginación correctamente.
En estos casos, lo recomendable no es simplemente aumentar el número máximo de reintentos, sino implementar una estrategia de espera progresiva y pequeños sleep controlados entre llamadas. De este modo respetamos los límites del servicio, evitamos saturar la API y garantizamos un comportamiento verdaderamente robusto y reutilizable en escenarios enterprise. El código de nuestra función get_all_pages quedaría de la siguiente manera:
def get_all_pages(url, headers, max_retries=5, sleep_between_pages=0.2):
"""
Recupera todas las páginas de un endpoint OData de Fabric
gestionando correctamente los bloqueos 429
"""
results = []
while url:
retries = 0
while True:
resp = requests.get(url, headers=headers)
# Petición correcta
if resp.status_code == 200:
break
# Si el servicio nos bloquea temporalmente (429)
if resp.status_code == 429 and retries < max_retries:
# Si la API nos indica cuánto esperar, usamos ese valor
retry_after = resp.headers.get("Retry-After")
if retry_after:
wait = int(retry_after)
else:
# Espera progresiva (exponential backoff)
wait = (2 ** retries)
time.sleep(wait)
retries += 1
continue
# Cualquier otro error lo lanzamos
raise Exception(f"API error {resp.status_code}: {resp.text}")
data = resp.json()
results.extend(data.get("value", []))
url = data.get("@odata.nextLink")
# Pequeña pausa entre páginas para evitar ráfagas de llamadas
time.sleep(sleep_between_pages)
return results
Con estas modificaciones, el notebook deja de fallar de forma intermitente y pasa a comportarse de manera estable incluso en entornos con muchos workspaces y artefactos. Seguimos accediendo exactamente a la misma información, pero ahora las llamadas a la API se realizan de forma controlada, respetando los límites del servicio y gestionando automáticamente los bloqueos temporales. El resultado es un proceso predecible, reutilizable y válido para ejecutarse como notebook base dentro de una arquitectura compleja de Microsoft Fabric.
Otras optimizaciones posibles
Una vez que el notebook es capaz de «ver» todos los artefactos de forma fiable, el siguiente paso natural no siempre es añadir más lógica, sino reducir lo que no necesitamos. En muchos entornos, no todos los workspaces ni todos los tipos de artefactos son relevantes para todos los casos de uso.
Por ejemplo, no todos los proyectos usan todos los artefactos de Fabric. Si en el proyecto en el que estamos trabajando no se utilizan bases de datos SQL, no tiene sentido hacer llamadas adicionales para escanearlas.
Algo similar ocurre si, por ejemplo, no estamos interesados en escanear todos los workspaces de nuestro tenant. En este caso podríamos aplicar un filtro para solo tener en cuenta, por ejemplo, aquellos workspaces que empiezan por "DEV_":
workspaces = [
ws for ws in workspaces
if ws["displayName"].startswith("DEV_")
]Otra optimización posible, especialmente útil en entornos donde el notebook se ejecuta varias veces dentro de la misma sesión, es cachear el catálogo de artefactos una vez generado.
En su forma actual, el notebook:
- siempre llama a la API de Fabric
- siempre reconstruye el catálogo completo
En muchos escenarios esto no es estrictamente necesario, ya que la estructura de workspaces y artefactos no cambia con frecuencia durante una sesión de trabajo. En estos casos, se puede comprobar si el catálogo ya existe y reutilizarlo, evitando llamadas innecesarias a la API.
Un ejemplo sencillo de este enfoque sería el siguiente:
if "FABRIC_CATALOG" in globals():
print("Usando catálogo de Fabric cacheado en la sesión")
else:
print("Generando catálogo de Fabric desde la API")
FABRIC_CATALOG = {}
# aquí iría la lógica de descubrimiento de artefactosConclusiones
En este artículo hemos visto cómo centralizar el acceso a los artefactos de Microsoft Fabric y eliminar la necesidad de repetir rutas, IDs y configuraciones en cada notebook. Partiendo de una versión básica, hemos ido incorporando las mejoras necesarias para que el enfoque funcione correctamente en entornos reales: descubrimiento dinámico de artefactos, gestión de la paginación de la API y control del throttling cuando el número de llamadas empieza a crecer.
El resultado es un notebook base que puede ejecutarse una sola vez por sesión y reutilizarse desde cualquier otro notebook del entorno. A partir de ese momento, basta con incluirlo mediante %run "NB_FabricCatalog" para disponer de todas las variables globales y del catálogo de artefactos en memoria, sin necesidad de volver a realizar el proceso ni de duplicar lógica.
Esto cambia de forma significativa la manera de trabajar con Fabric. Los notebooks dejan de estar acoplados a rutas abfss://... escritas a mano o a IDs copiados de otros sitios, y pasan a apoyarse en un único punto de entrada común. Por ejemplo, si queremos acceder a la tabla de un lakehouse que se encuentra en otro workspace, basta con utilizar la ruta correspondiente construida en el catálogo de la siguiente manera, manteniendo el código limpio, fácil de mantener y centrado en la lógica de negocio:
spark.read.format("delta").load(
f"{LH_EJEMPLO_BRONZE_PATH}/dbo/NombreTabla"
)Es importante subrayar que este notebook no pretende ser una solución rígida, sino una pieza base sobre la que construir. Decidir qué workspaces se incluyen, qué tipos de artefactos se buscan o qué información se expone forma parte del diseño del entorno, no de una limitación técnica. Las optimizaciones vistas permiten adaptar el mismo enfoque a realidades muy distintas.
Con estas prácticas, el mismo patrón puede escalar desde un entorno pequeño hasta una arquitectura de Microsoft Fabric mucho más compleja, manteniendo siempre un único punto de acceso a los artefactos. El beneficio es claro: menos código repetido, menos dependencias frágiles y una base más coherente sobre la que seguir construyendo.
En definitiva, no se trata de automatizar por automatizar, sino de introducir orden y consistencia en cómo accedemos a los artefactos de Fabric. En entornos que crecen y evolucionan con el tiempo, ese orden acaba marcando la diferencia.







Deja un comentario