Monday, August 11, 2025

Real-Time Progress Updates with Azure Functions and SignalR Using Python

Real-Time Progress Updates with Azure Functions and SignalR Using Python

https://learn.microsoft.com/en-us/azure/azure-signalr/signalr-quickstart-azure-functions-python?pivots=python-mode-decorators

Real-Time Progress Updates with Azure Functions and SignalR Using Python

This article demonstrates how to build a serverless Python application using Azure Functions and Azure SignalR Service to deliver real-time progress updates to clients. The solution includes HTTP endpoints, negotiation for SignalR connections, and timer triggers that broadcast messages and simulate progress updates for long-running operations.

Project File Structure

├── function_app.py
├── host.json
├── local.settings.json
├── requirements.txt
├── content/
│   └── index.html

Required Files

1. function_app.py

Main Azure Functions app with SignalR integration and timer triggers.

2. host.json

Azure Functions host configuration.

{
    "version": "2.0"
}

3. local.settings.json

Local development settings (update AzureSignalRConnectionString with your value).

{
    "IsEncrypted": false,
    "Values": {
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",
        "FUNCTIONS_WORKER_RUNTIME": "python",
        "AzureSignalRConnectionString": "<your-signalr-connection-string>"
    }
}

4. requirements.txt

Python dependencies for Azure Functions and HTTP requests.

azure-functions
requests

5. content/index.html

Sample HTML page for the index endpoint.

<html>

<body>
  <h1>Azure SignalR Serverless Sample</h1>
  <div id="messages"></div>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/3.1.7
    /signalr.min.js"></script>
  <script>
    let messages = document.querySelector('#messages');
    const apiBaseUrl = window.location.origin;
    const connection = new signalR.HubConnectionBuilder()
        .withUrl(apiBaseUrl + '/api')
        .configureLogging(signalR.LogLevel.Information)
        .build();
      connection.on('newMessage', (message) => {
        document.getElementById("messages").innerHTML = message;
      });

      // Display progress updates from the InsertTables timer
      connection.on('ProgressUpdate', (percent, message) => {
        const div = document.createElement('div');
        div.textContent = `${percent}% - ${message}`;
        messages.appendChild(div);
      });

      connection.start()
        .catch(console.error);
  </script>
</body>

</html>

Overview

  • Azure Functions: Used to create serverless endpoints and timer triggers.
  • Azure SignalR Service: Enables real-time communication between server and clients.
  • Python: Implements the logic for HTTP triggers, negotiation, and progress simulation.

Key Features

  1. HTTP Trigger: Responds to HTTP requests with a personalized message.
  2. Index Endpoint: Serves a static HTML page.
  3. SignalR Negotiation: Provides connection info for SignalR clients.
  4. Broadcast Timer: Periodically fetches GitHub star count and broadcasts it.
  5. Insert Tables Timer: Simulates inserting tables and sends progress updates to SignalR clients.

Complete Code

import azure.functions as func
import logging
import azure.functions as func
import os
import requests
import json 
import time

app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)

etag = ''
start_count = 0

@app.route(route="http_trigger")
def http_trigger(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    name = req.params.get('name')
    if not name:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            name = req_body.get('name')

    if name:
        return func.HttpResponse(f"Hello, {name}. This HTTP triggered function executed successfully.")
    else:
        return func.HttpResponse(
             "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.",
             status_code=200
        )
    

@app.route(route="index", auth_level=func.AuthLevel.ANONYMOUS)
def index(req: func.HttpRequest) -> func.HttpResponse:
    f = open(os.path.dirname(os.path.realpath(__file__)) + '/content/index.html')
    return func.HttpResponse(f.read(), mimetype='text/html')


@app.route(route="negotiate", auth_level=func.AuthLevel.ANONYMOUS, methods=["POST"])
@app.generic_input_binding(arg_name="connectionInfo", type="signalRConnectionInfo", hubName="serverless", connectionStringSetting="AzureSignalRConnectionString")
def negotiate(req: func.HttpRequest, connectionInfo) -> func.HttpResponse:
    return func.HttpResponse(connectionInfo)


@app.timer_trigger(schedule="*/1 * * * *", arg_name="myTimer",
              run_on_startup=False,
              use_monitor=False)
@app.generic_output_binding(arg_name="signalRMessages", type="signalR", hubName="serverless", connectionStringSetting="AzureSignalRConnectionString")
def broadcast(myTimer: func.TimerRequest, signalRMessages: func.Out[str]) -> None:
    global etag
    global start_count
    headers = {'User-Agent': 'serverless', 'If-None-Match': etag}
    res = requests.get('https://api.github.com/repos/azure/azure-functions-python-worker', headers=headers)
    if res.headers.get('ETag'):
        etag = res.headers.get('ETag')

    if res.status_code == 200:
        jres = res.json()
        start_count = jres['stargazers_count']

    signalRMessages.set(json.dumps({
        'target': 'newMessage',
        'arguments': [ 'Current star count of https://api.github.com/repos/azure/azure-functions-python-worker is: ' + str(start_count) ]
    }))


# New timer trigger: simulate inserting 10 tables and emit progress updates to SignalR
@app.timer_trigger(schedule="*/1 * * * *", arg_name="tablesTimer",
              run_on_startup=False,
              use_monitor=False)
@app.generic_output_binding(arg_name="signalRMessages", type="signalR", hubName="serverless", connectionStringSetting="AzureSignalRConnectionString")
def insert_tables_timer(tablesTimer: func.TimerRequest, signalRMessages: func.Out[str]) -> None:
    logging.info('InsertTables timer started.')

    total_tables = 10
    messages = []

    for i in range(1, total_tables + 1):
        # Simulate time-consuming insert
        time.sleep(1)

        percent = (i * 100) // total_tables
        msg = f"Table {i} of {total_tables} inserted."
        messages.append({
            'target': 'ProgressUpdate',
            'arguments': [percent, msg]
        })

    # Final success message
    messages.append({
        'target': 'ProgressUpdate',
        'arguments': [100, 'All tables inserted successfully.']
    })

    # Note: SignalR output binding in Python sends messages when the function completes.
    # We therefore batch all progress updates in a single payload.
    signalRMessages.set(json.dumps(messages))
    logging.info('InsertTables timer completed and progress sent to SignalR.')

How It Works

  • The broadcast function fetches the latest star count from GitHub and sends it to all connected SignalR clients every minute.
  • The insert_tables_timer function simulates a long-running operation (inserting tables) and sends progress updates to SignalR clients in real time.
  • All SignalR messages are sent when the function completes, so progress is batched and delivered together.

Next Steps

  • Deploy the function app to Azure.
  • Configure the Azure SignalR Service and connection string.
  • Build a client (e.g., web app) to receive and display real-time updates.

This approach is ideal for scenarios where you need to inform users about the progress of background tasks, such as data imports or batch processing, using serverless technologies and real-time messaging.

OutPut:
Run Function App: func host start
Open this url: http://localhost:7071/api/index


----------------------------------------------------------------------

index.html
<html>

<body>
  <h1>Azure SignalR Serverless Sample</h1>
  <div id="messages"></div>
  <button id="startProgress">Start Progress</button>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/3.1.7
    /signalr.min.js"></script>
  <script>
    let messages = document.querySelector('#messages');
    const apiBaseUrl = window.location.origin;
   
    // Generate a new GUID when page loads
    function generateGUID() {
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
      });
    }
   
    const sessionGuid = generateGUID();
    console.log('Generated GUID:', sessionGuid);
   
    const connection = new signalR.HubConnectionBuilder()
        .withUrl(apiBaseUrl + '/api')
        .configureLogging(signalR.LogLevel.Information)
        .build();
      connection.on('newMessage', (message) => {
        document.getElementById("messages").innerHTML = message;
      });

      // Display progress updates using the generated GUID
      connection.on(sessionGuid, (percent, message) => {
        const div = document.createElement('div');
        div.textContent = `[Manual] ${percent}% - ${message}`;
        div.style.color = 'blue';
        messages.appendChild(div);
      });

      // Display timer progress updates (broadcasted to all clients)
      // connection.on('TimerProgress', (percent, message, sessionId) => {
      //   const div = document.createElement('div');
      //   div.textContent = `[Auto] ${percent}% - ${message}`;
      //   div.style.color = 'green';
      //   messages.appendChild(div);
      // });

      connection.start()
        .then(() => {
          console.log('SignalR connection established');
         
          // Add event listener for the start progress button
          document.getElementById('startProgress').addEventListener('click', () => {
            // Clear previous messages
            messages.innerHTML = '';
           
            // Start the progress
            fetch(apiBaseUrl + '/api/startProgress', {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
              },
              body: JSON.stringify({ guid: sessionGuid })
            }).catch(console.error);
          });
        })
        .catch(console.error);
  </script>
</body>

</html>


function_app.py
import azure.functions as func
import logging
import azure.functions as func
import os
import requests
import json
import time

app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)

etag = ''
start_count = 0

@app.route(route="http_trigger")
def http_trigger(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    name = req.params.get('name')
    if not name:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            name = req_body.get('name')

    if name:
        return func.HttpResponse(f"Hello, {name}.
            This HTTP triggered function executed successfully.")
    else:
        return func.HttpResponse(
             "This HTTP triggered function executed successfully. Pass a name in the
    query string or in the request body for a personalized response.",
             status_code=200
        )
   

@app.route(route="index", auth_level=func.AuthLevel.ANONYMOUS)
def index(req: func.HttpRequest) -> func.HttpResponse:
    f = open(os.path.dirname(os.path.realpath(__file__)) + '/content/index.html')
    return func.HttpResponse(f.read(), mimetype='text/html')


@app.route(route="negotiate", auth_level=func.AuthLevel.ANONYMOUS, methods=["POST"])
@app.generic_input_binding(arg_name="connectionInfo", type="signalRConnectionInfo",
    hubName="serverless", connectionStringSetting="AzureSignalRConnectionString")
def negotiate(req: func.HttpRequest, connectionInfo) -> func.HttpResponse:
    return func.HttpResponse(connectionInfo)


@app.route(route="startProgress", auth_level=func.AuthLevel.ANONYMOUS,
    methods=["POST"])
@app.generic_output_binding(arg_name="signalRMessages", type="signalR",
    hubName="serverless", connectionStringSetting="AzureSignalRConnectionString")
def start_progress(req: func.HttpRequest, signalRMessages: func.Out[str]) ->
    func.HttpResponse:
    try:
        req_body = req.get_json()
        if req_body and req_body.get('guid'):
            client_guid = req_body.get('guid')
            logging.info(f'Starting progress for client GUID: {client_guid}')
           
            total_tables = 10
            messages = []

            for i in range(1, total_tables + 1):
                # Simulate time-consuming insert
                time.sleep(1)

                percent = (i * 100) // total_tables
                msg = f"Table {i} of {total_tables} inserted. {client_guid}"
                messages.append({
                    'target': client_guid,
                    'arguments': [percent, msg]
                })

            # Final success message
            messages.append({
                'target': client_guid,
                'arguments': [100, 'All tables inserted successfully.']
            })

            signalRMessages.set(json.dumps(messages))
            return func.HttpResponse(f"Progress started for GUID: {client_guid}",
    status_code=200)
        else:
            return func.HttpResponse("No GUID provided", status_code=400)
    except Exception as e:
        logging.error(f'Error starting progress: {str(e)}')
        return func.HttpResponse("Error starting progress", status_code=500)


# @app.timer_trigger(schedule="*/10 * * * * *", arg_name="myTimer",
run_on_startup=False, use_monitor=False)
# @app.generic_output_binding(arg_name="signalRMessages1", type="signalR",
hubName="serverless", connectionStringSetting="AzureSignalRConnectionString")
# def broadcast(myTimer: func.TimerRequest, signalRMessages1: func.Out[str]) -> None:
#     global etag
#     global start_count
#     headers = {'User-Agent': 'serverless', 'If-None-Match': etag}
#     res = requests.get('https://api.github.com/repos/azure
/azure-functions-python-worker', headers=headers)
#     if res.headers.get('ETag'):
#         etag = res.headers.get('ETag')

#     if res.status_code == 200:
#         jres = res.json()
#         start_count = jres['stargazers_count']

#     signalRMessages1.set(json.dumps({
#         'target': 'newMessage',
#         'arguments': [ 'Current star count of https://api.github.com/repos
/azure/azure-functions-python-worker is: ' + str(start_count) ]
#     }))


# # Timer trigger: simulate inserting 10 tables and emit progress updates to SignalR
# # This version broadcasts to all clients - each client can choose to display or
ignore
# @app.timer_trigger(schedule="*/15 * * * * *", arg_name="tablesTimer",
run_on_startup=False, use_monitor=False)
# @app.generic_output_binding(arg_name="signalRMessages2", type="signalR",
hubName="serverless", connectionStringSetting="AzureSignalRConnectionString")
# def insert_tables_timer(tablesTimer: func.TimerRequest, signalRMessages2:
func.Out[str]) -> None:
#     logging.info('InsertTables timer started.')

#     # Generate a unique session ID for this timer run
#     import uuid
#     timer_session_id = str(uuid.uuid4())
   
#     total_tables = 10
#     messages = []

#     for i in range(1, total_tables + 1):
#         # Simulate time-consuming insert
#         time.sleep(1)

#         percent = (i * 100) // total_tables
#         msg = f"Timer Session: Table {i} of {total_tables} inserted."
#         messages.append({
#             'target': 'TimerProgress',
#             'arguments': [percent, msg, timer_session_id]
#         })

#     # Final success message
#     messages.append({
#         'target': 'TimerProgress',
#         'arguments': [100, 'Timer Session: All tables inserted successfully.',
timer_session_id]
#     })

#     signalRMessages2.set(json.dumps(messages))
#     logging.info(f'InsertTables timer completed. Session ID: {timer_session_id}')



OutPut:
Run Function App: func host start
Open this url: http://localhost:7071/api/index




No comments:

Post a Comment

Featured Post

Real-Time Progress Updates with Azure Functions and SignalR Using Python

Real-Time Progress Updates with Azure Functions and SignalR Using Python https://learn.microsoft.com/en-us/azure/azure-signalr/signalr-quick...

Popular posts