Thursday, 10 April 2025

Getting data from an API using Python notebook

Getting data from an API using Python notebook

A no-code way to get data from an API is by using a Data Factory pipeline, syncing the response to a Lakehouse, or maybe transforming the response and syncing its output to a Lakehouse.

If the API call is simple, you may wish to use a python notebook to make the call and write the table.
For this you can use the requests module


import requests
import pandas as pd

key = "9b5c209c579fdba640a0ea31da3cb4ae"
lat = 53.3952792
lon = -2.1091572

url = f"https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={key}"

try:
    response = requests.get(url)  
    response.raise_for_status()

    # Show content (which is a byte array)
    print (response.content)

    # Get a section from the contect and represent it as a table
    df_items = pd.json_normalize(response.json()['main'])
    display (df_items)    

    # If we ommit the section filter: e.g. df_items = pd.json_normalize(response.json())
    # we would get columns with dot.notation in the name
    # e.g. coord.lon, coord.lat, main.temp, clouds.all

# Exception handling order is important as some are hierarchical
except requests.exceptions.ConnectionError as err:
    print ("ConnectionError:",err)
except requests.exceptions.Timeout as err:
    print ("Timeout:",err)
except requests.exceptions.TooManyRedirects as err:
    print ("TooManyRedirects:",err)
except requests.exceptions.HTTPError as err:
    print ("HTTPError:",err)
except requests.exceptions.RequestException as err:
    print ("RequestException: Something Else",err)


We can call the fabric api from a fabric notebook without authentication by using the sempy.FabricRestClient module
This also gives us access ot the default lakehouse
FabricRestClient doesn't have aexception handling but we can add it from the sempy.exceptions module

import sempy.fabric as fabric
from sempy.fabric.exceptions import FabricHTTPException, WorkspaceNotFoundException
import pandas as pd

client = fabric.FabricRestClient()

def get_workspace_items(workspace=None):
    df = None
    if workspace is None:
        workspaceId = fabric.get_workspace_id()
    else:
        workspaceId = fabric.resolve_workspace_id(workspace)

    try:
        response = client.get(f"/v1/workspaces/{workspaceId}/items")

        if response.status_code != 200:
            raise FabricHTTPException(response)

        df = pd.json_normalize(response.json()['value'])
    except WorkspaceNotFoundException as e:
        print("Caught a WorkspaceNotFoundException:", e)
    except FabricHTTPException as e:
        print("Caught a FabricHTTPException. Check the API endpoint, authentication.")

    return df

# Get the workspace items from the workspace named "Dev"
df_items = get_workspace_items("Dev")
# Get the workspace items from the current workspace, regardless of its name
df_items = get_workspace_items()

POST request

For both request and fabricRestClient, issuing a post is simply a case of calling .post,
rather than .get, and adding a parameter contaiing the content to be posted

A Fabric post request...
Here I put the status handler outside of the function as the post doesn't necessarily return a 201, and I wanted to make the post function generic.

import sempy.fabric as fabric
from sempy.fabric.exceptions import FabricHTTPException, WorkspaceNotFoundException

client = fabric.FabricRestClient()

def post_operation(payload:dict, workspace=None):
    if workspace is None:
        workspaceId = fabric.get_workspace_id()
    else:
        workspaceId = fabric.resolve_workspace_id(workspace)

    try:
        response = client.post(f"/v1/workspaces/{workspaceId}/items",json= payload)
        return response.status_code

    except WorkspaceNotFoundException as e:
        print("Caught a WorkspaceNotFoundException:", e)
    except FabricHTTPException as e:
        print("Caught a FabricHTTPException. Check the API endpoint, authentication.")


# Test API post operation by creating a lakehouse object
# Create the payload/body for the POST request as a dict
# example; create a lakehouse in the default (current) workspace
payload = {"displayName": "generated_lakehouse_test","type": "Lakehouse"}

status_code = post_operation(payload=payload, workspace=None)

if status_code != 201:
    raise FabricHTTPException(response)

Issue a Kusto query to an eventhouse.
We will need a token for this, using the notebookutils.credentials module
token = notebookutils.credentials.getToken(eventhouse_connection)
This isn't part of the request, so we obtain it outside of the query function and pass it as a parameter


import requests
import json
import pandas as pd
from datetime import datetime

def query_kusto(cluster_url: str, database: str, query: str, token: str):
    """
    Function to query Fabric Kusto db using REST API

    Args:
        cluster_url: Full cluster URL (e.g., 'https://guid.kusto.fabric.microsoft.com')
        database: Database name
        query: KQL query
        token: Bearer token
    """

    headers = {
        'Authorization': f'Bearer {token}',
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    }

    body = {
        'db': database,
        'csl': query
    }

    response = requests.post(
        f'{cluster_url}/v1/rest/query',
        headers=headers,
        json=body
    )

    return response.json()



# Example call
I created an eventhouse with a table created from Microsoft's Bikes sample file.

eventhouse_connection = f'https://xxxxxxxxxx.zz.kusto.fabric.microsoft.com'
database = 'eh_DBTL'
kqlquery = '''
Bikes | take 2
'''
# Get a token to pass to the query function
token = notebookutils.credentials.getToken(eventhouse_connection)

try:
    results = query_kusto(eventhouse_connection, database, kqlquery, token)

    if results.get('Tables'):
        rows = results['Tables'][0]['Rows']
        for row in rows:
            print(row)

# We should add some specific exception handlers
except Exception as e:
    print(f"Error querying Kusto: {str(e)}")


Exception handling

We should hadle specific exceptions differently and have a catch all, plus we can add a custom exception handler.
Here we call the requests module with a ficticious url that will fail
This would normally trigger an HTTPError

import requests

class CustomException(Exception):
    pass

try:
    response = requests.get("https://fjdjdfdfd.com")
    if response.status_code != 200:
        raise CustomException("Unexpected status code: " + str(response.status_code))
   
    print("Request successful!")
   
except CustomException as e:
    print("Custom Exception:", e)
except requests.exceptions.RequestException as e:
    print("An error occurred:", e)

No comments:

Post a Comment

Note: only a member of this blog may post a comment.

What is a Delta table?

Microsoft Fabric uses Delta tables in its Lakehouses. Delta Tables are a data management layer  providing  ACID transactions , schema enforc...